Skip to content

动画设计规范

最后更新:2026-02-21 版本:v1.0 状态:✅ 强制执行

本文档定义 Reading Vocab Helper 项目的 动画设计规范,包括动画时长、缓动曲线、Motion Springs 等最佳实践。


📋 目录

  1. 动画时长标准
  2. 缓动曲线规范
  3. Motion Springs(Material 3 Expressive)
  4. 常见场景动画模板
  5. 性能最佳实践
  6. 反模式总结

动画时长标准

核心原则:使用标准时长值,确保一致的用户体验

标准时长值

时长级别数值使用场景示例
快速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.fastOutSlowInMaterial 标准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.linearCurves.easeOutCubic线性动画机械感
Curves.elasticOut 用于普通动画Curves.easeOutCubic过度弹跳分散注意力

性能反模式

❌ 错误✅ 正确原因
setState 在 addListenerAnimatedBuilder减少重建
Opacity 包裹大组件FadeTransition 或颜色透明度避免 saveLayer
未使用 const使用 const避免不必要重建

参考资料


维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue