### 详细变更:
1. **文档与配置**:更新AGENTS.md添加命令超时约束,升级Rive依赖至0.14.7并替换平台插件引用
2. **UI优化**:重构AppInfo页面布局、移除图表冗余配置、锁定部分系统设置项
3. **功能增强**:
- 新增工具面板拖拽状态管理与介绍弹窗
- 新增进度页面编辑/重排/清空用户进度功能
- 新增摇一摇路由作用域拦截逻辑
4. **体验优化**:
- 统一外部链接跳转弹窗,添加文件打开确认逻辑
- 修复设备卡片IP溢出、Android权限声明问题
- 后台任务初始化增加协议校验
5. **代码重构**:拆分工具面板配置、拖拽逻辑与动画参数,优化状态管理代码
6. **工具脚本**:新增协议文件上传脚本
110 lines
2.7 KiB
Dart
110 lines
2.7 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — Rive Tab 动画组件
|
||
/// 创建时间: 2026-05-29
|
||
/// 更新时间: 2026-05-29
|
||
/// 作用: 使用Rive动画替代TabIconSprite,实现更丰富的角色动画
|
||
/// 上次更新: 迁移至 Rive 0.14.x API
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:rive/rive.dart';
|
||
|
||
/// Rive Tab 动画图标组件
|
||
///
|
||
/// 通过 Rive StateMachine 控制选中/未选中状态切换,
|
||
/// 作为 TabIconSprite 的可选替代方案。
|
||
/// 需要提供 .riv 资源文件,StateMachine 中需包含
|
||
/// 名为 `isSelected` 的布尔输入。
|
||
class RiveTabIcon extends StatefulWidget {
|
||
const RiveTabIcon({
|
||
required this.isSelected,
|
||
required this.assetPath,
|
||
this.stateMachineName = 'State',
|
||
this.size = 32,
|
||
super.key,
|
||
});
|
||
|
||
/// 是否选中
|
||
final bool isSelected;
|
||
|
||
/// Rive 资源路径 (如 'assets/animations/tab_home.riv')
|
||
final String assetPath;
|
||
|
||
/// StateMachine 名称
|
||
final String stateMachineName;
|
||
|
||
/// 图标尺寸
|
||
final double size;
|
||
|
||
@override
|
||
State<RiveTabIcon> createState() => _RiveTabIconState();
|
||
}
|
||
|
||
class _RiveTabIconState extends State<RiveTabIcon> {
|
||
RiveWidgetController? _controller;
|
||
File? _file;
|
||
bool _isLoading = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadRive();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller?.dispose();
|
||
_file?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadRive() async {
|
||
try {
|
||
final file = await File.asset(
|
||
widget.assetPath,
|
||
riveFactory: Factory.rive,
|
||
);
|
||
if (!mounted) return;
|
||
_file = file;
|
||
_controller = RiveWidgetController(
|
||
file!,
|
||
stateMachineSelector: StateMachineSelector.byName(
|
||
widget.stateMachineName,
|
||
),
|
||
);
|
||
_syncSelection();
|
||
setState(() => _isLoading = false);
|
||
} catch (e) {
|
||
if (mounted) setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
void _syncSelection() {
|
||
_controller?.stateMachine.boolean('isSelected')?.value = widget.isSelected;
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(RiveTabIcon oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.isSelected != widget.isSelected) {
|
||
_syncSelection();
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_isLoading || _controller == null) {
|
||
return SizedBox(
|
||
width: widget.size,
|
||
height: widget.size,
|
||
child: const CupertinoActivityIndicator(radius: 8),
|
||
);
|
||
}
|
||
return SizedBox(
|
||
width: widget.size,
|
||
height: widget.size,
|
||
child: RiveWidget(controller: _controller!),
|
||
);
|
||
}
|
||
}
|