主题
动画设计规范
最后更新:2026-02-21 版本:v1.0 状态:✅ 强制执行
本文档定义 Reading Vocab Helper 项目的 动画设计规范,包括动画时长、缓动曲线、Motion Springs 等最佳实践。
📋 目录
动画时长标准
核心原则:使用标准时长值,确保一致的用户体验
标准时长值
| 时长级别 | 数值 | 使用场景 | 示例 |
|---|---|---|---|
| 快速 | 100-150ms | 微交互、涟漪效果 | 按钮按下反馈 |
| 标准快速 | 200ms | 快速反馈、轻量过渡 | 开关切换、复选框 |
| 标准 | 300ms | 页面过渡、展开/折叠 | 导航切换、列表展开 |
| 中等 | 400ms | 重要动画、强调效果 | 对话框出现、抽屉打开 |
| 慢速 | 500-600ms | 入场动画、引导动画 | 首页加载、空状态 |
| 强调 | 800-1000ms | 庆祝动画、成就解锁 | 完成动画、徽章获得 |
决策树:如何选择时长
动画类型?
├─ 用户触发的即时反馈
│ ├─ 按钮涟漪 → 100-150ms
│ ├─ 开关/复选框 → 200ms
│ └─ 菜单展开 → 200-250ms
│
├─ 页面/视图过渡
│ ├─ 同级页面切换 → 300ms
│ ├─ 进入子页面 → 300-350ms
│ └─ 返回上级页面 → 250-300ms
│
├─ 模态/覆盖层
│ ├─ 对话框出现 → 300-400ms
│ ├─ 底部弹窗 → 300-400ms
│ └─ 全屏覆盖 → 400ms
│
└─ 特殊动画
├─ 空状态图示 → 500-600ms
├─ 成功/完成 → 600-800ms
└─ 引导/教学 → 800-1000ms代码示例
dart
// ✅ 正确:使用标准时长
const Duration kQuickDuration = Duration(milliseconds: 200);
const Duration kStandardDuration = Duration(milliseconds: 300);
const Duration kMediumDuration = Duration(milliseconds: 400);
const Duration kSlowDuration = Duration(milliseconds: 500);
// 按钮按下反馈
AnimatedContainer(
duration: kQuickDuration, // 200ms
// ...
)
// 页面过渡
PageRouteBuilder(
transitionDuration: kStandardDuration, // 300ms
// ...
)
// 对话框出现
showDialog(
transitionDuration: kMediumDuration, // 400ms
// ...
)❌ 避免的做法
dart
// ❌ 错误:非标准时长
Duration(milliseconds: 180) // 应使用 200ms
Duration(milliseconds: 280) // 应使用 300ms
Duration(milliseconds: 450) // 应使用 400ms 或 500ms
// ❌ 错误:时长过短(用户无法感知)
Duration(milliseconds: 50)
// ❌ 错误:时长过长(用户感到迟滞)
Duration(milliseconds: 1500) // 除非是特殊庆祝动画缓动曲线规范
核心原则:使用物理真实的缓动曲线
推荐曲线
| 曲线 | 使用场景 | 特点 |
|---|---|---|
Curves.easeOutCubic | 默认推荐 | 快速开始,缓慢结束(自然) |
Curves.easeInOutCubic | 页面过渡 | 缓慢开始和结束(平滑) |
Curves.easeOut | 下拉刷新、弹出 | 快速响应,自然停止 |
Curves.easeIn | 消失、收起 | 缓慢开始,快速结束 |
Curves.fastOutSlowIn | Material 标准 | Material Design 推荐 |
Curves.decelerate | 惯性滚动 | 自然减速 |
决策树:如何选择曲线
动画意图?
├─ 元素出现/展开
│ └─ Curves.easeOutCubic(推荐)
│ 或 Curves.easeOut
│
├─ 元素消失/收起
│ └─ Curves.easeInCubic
│ 或 Curves.easeIn
│
├─ 页面过渡(双向)
│ └─ Curves.easeInOutCubic(推荐)
│ 或 Curves.fastOutSlowIn
│
├─ 惯性/物理效果
│ └─ Curves.decelerate
│ 或 SpringDescription
│
└─ 弹跳效果
└─ Curves.elasticOut
或 Curves.bounceOut代码示例
dart
// ✅ 推荐:元素出现
AnimatedOpacity(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
opacity: isVisible ? 1.0 : 0.0,
child: content,
)
// ✅ 推荐:页面过渡
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubic,
),
child: page,
);
},
)
// ✅ 推荐:列表项动画
AnimatedList(
itemBuilder: (context, index, animation) {
return SlideTransition(
position: animation.drive(
Tween(begin: Offset(1, 0), end: Offset.zero)
.chain(CurveTween(curve: Curves.easeOutCubic)),
),
child: item,
);
},
)❌ 避免的做法
dart
// ❌ 错误:使用线性曲线(机械感)
AnimatedContainer(
curve: Curves.linear, // 应使用 easeOutCubic
// ...
)
// ❌ 错误:过度弹跳(分散注意力)
AnimatedContainer(
curve: Curves.elasticOut, // 除非确实需要弹跳效果
// ...
)Motion Springs
Material 3 Expressive 新特性:基于物理的弹簧动画
什么是 Motion Springs
Motion Springs 是 Material 3 Expressive 引入的动画系统,使用真实的物理弹簧模型替代传统的缓动曲线,提供更自然、更流畅的动画效果。
Flutter 中使用 Spring
dart
import 'package:flutter/physics.dart';
// 创建弹簧描述
final SpringDescription spring = SpringDescription(
mass: 1.0, // 质量
stiffness: 100, // 刚度(越大越快)
damping: 10, // 阻尼(越大越少弹跳)
);
// 使用 SpringSimulation
final SpringSimulation simulation = SpringSimulation(
spring,
0, // 起始位置
1, // 结束位置
0, // 起始速度
);
// 在 AnimationController 中使用
animationController.animateWith(simulation);预设弹簧配置
dart
// 标准弹簧(推荐用于大多数 UI 动画)
const SpringDescription standardSpring = SpringDescription(
mass: 1.0,
stiffness: 200,
damping: 20,
);
// 快速弹簧(用于小型 UI 元素)
const SpringDescription quickSpring = SpringDescription(
mass: 0.8,
stiffness: 400,
damping: 25,
);
// 柔和弹簧(用于大型过渡)
const SpringDescription gentleSpring = SpringDescription(
mass: 1.2,
stiffness: 100,
damping: 15,
);
// 弹跳弹簧(用于强调效果)
const SpringDescription bouncySpring = SpringDescription(
mass: 1.0,
stiffness: 150,
damping: 8,
);使用场景
| 弹簧类型 | 使用场景 | 效果 |
|---|---|---|
| 标准弹簧 | 卡片翻转、列表重排 | 自然、无弹跳 |
| 快速弹簧 | 按钮缩放、图标变换 | 快速响应 |
| 柔和弹簧 | 页面过渡、大面积变化 | 平滑、优雅 |
| 弹跳弹簧 | 成功反馈、徽章出现 | 活泼、强调 |
常见场景动画模板
1. 页面过渡
dart
// 淡入淡出过渡
class FadePageRoute<T> extends PageRouteBuilder<T> {
FadePageRoute({required this.page})
: super(
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 250),
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
),
child: child,
);
},
);
final Widget page;
}2. 列表项动画
dart
// 列表项滑入
class ListItemAnimation extends StatelessWidget {
const ListItemAnimation({
required this.animation,
required this.child,
});
final Animation<double> animation;
final Widget child;
@override
Widget build(BuildContext context) {
return SlideTransition(
position: animation.drive(
Tween(
begin: const Offset(0.5, 0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOutCubic)),
),
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
}3. 对话框动画
dart
// 缩放+淡入对话框
Future<T?> showAnimatedDialog<T>({
required BuildContext context,
required Widget child,
}) {
return showGeneralDialog<T>(
context: context,
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) => child,
transitionBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
return ScaleTransition(
scale: Tween(begin: 0.8, end: 1.0).animate(curved),
child: FadeTransition(
opacity: curved,
child: child,
),
);
},
);
}4. 开关/切换动画
dart
// 平滑切换
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: currentWidget,
)性能最佳实践
1. 使用 AnimatedBuilder 替代 setState
dart
// ❌ 错误:在动画中使用 setState
class _BadAnimationState extends State<BadAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(...)
..addListener(() {
setState(() {}); // ❌ 每帧都触发整个 Widget 重建
});
}
}
// ✅ 正确:使用 AnimatedBuilder
class _GoodAnimationState extends State<GoodAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child, // ✅ child 不会重建
);
},
child: const ExpensiveWidget(),
);
}
}2. 避免过度使用 Opacity
dart
// ❌ 不推荐:Opacity 会触发 saveLayer
Opacity(
opacity: 0.5,
child: LargeWidget(),
)
// ✅ 推荐:使用颜色透明度
Container(
color: color.withOpacity(0.5),
child: LargeWidget(),
)
// ✅ 推荐:FadeTransition 用于动画
FadeTransition(
opacity: animation,
child: widget,
)3. 使用 RepaintBoundary 隔离重绘
dart
// 复杂动画使用 RepaintBoundary
RepaintBoundary(
child: AnimatedWidget(...),
)4. 使用 const 构造函数
dart
// ✅ 静态子组件使用 const
AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: const Text('静态内容'), // const 避免重建
)反模式总结
时长反模式
| ❌ 错误 | ✅ 正确 | 原因 |
|---|---|---|
Duration(milliseconds: 180) | Duration(milliseconds: 200) | 非标准值 |
Duration(milliseconds: 50) | Duration(milliseconds: 100) | 过短,用户无法感知 |
Duration(milliseconds: 1500) | Duration(milliseconds: 500) | 过长,感到迟滞 |
曲线反模式
| ❌ 错误 | ✅ 正确 | 原因 |
|---|---|---|
Curves.linear | Curves.easeOutCubic | 线性动画机械感 |
Curves.elasticOut 用于普通动画 | Curves.easeOutCubic | 过度弹跳分散注意力 |
性能反模式
| ❌ 错误 | ✅ 正确 | 原因 |
|---|---|---|
setState 在 addListener | AnimatedBuilder | 减少重建 |
Opacity 包裹大组件 | FadeTransition 或颜色透明度 | 避免 saveLayer |
未使用 const | 使用 const | 避免不必要重建 |
参考资料:
维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue