mcpField
F
flutter-animations

flutter-animations

github.com/im5tu设计与 UI643 次安装6 周前更新
代码仓库

关于 flutter-animations

flutter-animations 是面向 Flutter 开发者的动画技能,帮助按场景选择动画方案并实现常见效果。内容涵盖从隐式动画到物理模拟的流程、示例和最佳实践。

主要功能

  1. 01选择合适的动画类型
  2. 02实现隐式与显式动画
  3. 03制作 Hero 页面转场
  4. 04编排交错动画
  5. 05创建基于物理的动画
  6. 06查看示例与最佳实践

适用场景

  1. 01为状态变化添加属性动画
  2. 02在页面间共享元素转场
  3. 03按顺序显示列表或菜单项
  4. 04实现弹簧或拖拽交互效果

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/im5tu/claude --skill flutter-animations

安装后在 Claude Code 中输入/flutter-animations确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/templates/
explicit_animation.dart
hero_transition.dart
implicit_animation.dart
staggered_animation.dart
references/
curves.md
explicit.md
hero.md
implicit.md
physics.md
staggered.md

详细介绍

Flutter 动画

概述

在 Flutter 中,针对不同用例采用合适的方法,创建流畅且高性能的动画。本技能涵盖完整的动画工作流:从选择隐式或显式动画,到实现 Hero 转场和交错动画等复杂效果。

动画类型决策树

根据需求选择合适的动画类型:

隐式动画——适用于:

  • 为单个属性设置动画(颜色、大小、位置)
  • 动画由状态变化触发
  • 不需要精细控制

显式动画——适用于:

  • 需要全面控制动画生命周期
  • 同时为多个属性设置动画
  • 需要响应动画状态变化
  • 创建自定义动画或转场

Hero 动画——适用于:

  • 在两个屏幕间共享一个元素
  • 创建共享元素转场
  • 用户期望元素在路由之间“飞动”

交错动画——适用于:

  • 多个动画按顺序运行或彼此重叠
  • 创建涟漪效果或按顺序显示内容
  • 按顺序为列表项设置动画

基于物理效果的动画——适用于:

  • 动画应具有自然的物理效果
  • 弹簧效果、滚动手势
  • 可拖动交互

隐式动画

隐式动画会在属性变化时自动处理动画,无需控制器。

常见的隐式动画组件

AnimatedContainer——为多个属性设置动画(大小、颜色、装饰、内边距):

AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: _expanded ? 200 : 100,
  height: _expanded ? 200 : 100,
  color: _expanded ? Colors.blue : Colors.red,
  child: const FlutterLogo(),
)

AnimatedOpacity——简单的淡入淡出动画:

AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 300),
  child: const Text('Hello'),
)

TweenAnimationBuilder——无需样板代码即可创建自定义补间动画:

TweenAnimationBuilder<double>(
  tween: Tween<double>(begin: 0, end: 1),
  duration: const Duration(seconds: 1),
  builder: (context, value, child) {
    return Opacity(
      opacity: value,
      child: Transform.scale(
        scale: value,
        child: child,
      ),
    );
  },
  child: const FlutterLogo(),
)

其他隐式动画组件:

  • AnimatedPadding——内边距动画
  • AnimatedPositioned——位置动画(在 Stack 中)
  • AnimatedAlign——对齐动画
  • AnimatedContainer——多个属性
  • AnimatedSwitcher——组件间交叉淡入淡出
  • AnimatedDefaultTextStyle——文本样式动画
最佳实践
  • 简单场景优先使用隐式动画
  • 使用合适的曲线实现自然运动(参见 Curves 类)
  • 设置 curve 和 duration,确保行为可预测
  • 需要时使用 onEnd 回调
  • 避免嵌套隐式动画,以提升性能

显式动画

显式动画通过 AnimationController 提供全面控制。

核心组件

AnimationController——驱动动画:

late AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  );
}

@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

Tween——在起始值和结束值之间进行插值:

animation = Tween<double>(begin: 0, end: 300).animate(_controller);

CurvedAnimation——为动画应用曲线:

animation = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeInOut,
);
AnimatedWidget 模式

最适合可复用的动画组件:

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
    : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}
AnimatedBuilder 模式

最适合带动画的复杂组件:

class GrowTransition extends StatelessWidget {
  const GrowTransition({
    required this.child,
    required this.animation,
    super.key,
  });

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}
监控动画状态
animation.addStatusListener((status) {
  switch (status) {
    case AnimationStatus.completed:
      _controller.reverse();
      break;
    case AnimationStatus.dismissed:
      _controller.forward();
      break;
    default:
      break;
  }
});
多个同步动画
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
    : super(listenable: animation);

  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}
内置显式转场

Flutter 提供可直接使用的转场:

  • FadeTransition——淡入淡出动画
  • ScaleTransition——缩放动画
  • SlideTransition——滑动动画
  • SizeTransition——尺寸动画
  • RotationTransition——旋转动画
  • PositionedTransition——位置动画(在 Stack 中)

示例:

FadeTransition(
  opacity: _animation,
  child: const FlutterLogo(),
)
性能提示
  • 移除组件时释放控制器
  • 使用 AnimatedBuilder 优化重建
  • 避免在动画监听器中调用 setState()(使用 AnimatedWidget/AnimatedBuilder)
  • 调试时使用 timeDilation 减慢动画

Hero 动画

Hero 动画用于创建屏幕之间的共享元素转场。

基础 Hero 动画

源屏幕:

Hero(
  tag: 'hero-image',
  child: Image.asset('images/logo.png'),
)

目标屏幕:

Hero(
  tag: 'hero-image',  // Same tag!
  child: Image.asset('images/logo.png'),
)
完整示例
class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.width,
  });

  final String photo;
  final VoidCallback? onTap;
  final double width;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Image.asset(photo, fit: BoxFit.contain),
          ),
        ),
      ),
    );
  }
}

在屏幕之间导航:

Navigator.of(context).push(
  MaterialPageRoute<void>(
    builder: (context) {
      return Scaffold(
        appBar: AppBar(title: const Text('Detail')),
        body: Center(
          child: PhotoHero(
            photo: 'images/logo.png',
            width: 300.0,
            onTap: () => Navigator.of(context).pop(),
          ),
        ),
      );
    },
  ),
);
径向 Hero 动画

在转场过程中将圆形变为矩形:

class RadialExpansion extends StatelessWidget {
  const RadialExpansion({
    super.key,
    required this.maxRadius,
    this.child,
  }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);

  final double maxRadius;
  final double clipRectSize;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect(child: child),
        ),
      ),
    );
  }
}

搭配 MaterialRectCenterArcTween 使用,以进行基于中心点的插值:

static RectTween _createRectTween(Rect? begin, Rect? end) {
  return MaterialRectCenterArcTween(begin: begin, end: end);
}
Hero 最佳实践
  • 使用唯一且一致的标签(通常是数据对象本身)
  • 让不同路由中的 Hero Widget 树保持相似
  • 将图像包裹在颜色透明的 Material 中,以营造“突出”效果
  • 使用 timeDilation 调试过渡效果
  • 需要时,考虑使用 HeroMode 禁用 Hero 动画

交错动画

以不同的时间安排运行多个动画。

基本交错动画

所有动画共用一个控制器:

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({super.key, required this.controller})
    : opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: controller,
          curve: const Interval(0.0, 0.100, curve: Curves.ease),
        ),
      ),
      width = Tween<double>(begin: 50.0, end: 150.0).animate(
        CurvedAnimation(
          parent: controller,
          curve: const Interval(0.125, 0.250, curve: Curves.ease),
        ),
      );

  final AnimationController controller;
  final Animation<double> opacity;
  final Animation<double> width;

  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Container(
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: 150,
          color: Colors.blue,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: _buildAnimation,
    );
  }
}
基于 Interval 的时间安排

每个动画都有一个介于 0.0 和 1.0 之间的 Interval:

animation = Tween<double>(begin: 0, end: 300).animate(
  CurvedAnimation(
    parent: controller,
    curve: const Interval(
      0.25,  // Start at 25% of controller duration
      0.50,  // End at 50% of controller duration
      curve: Curves.ease,
    ),
  ),
);
常见的补间动画
borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4),
  end: BorderRadius.circular(75),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: const Interval(0.375, 0.500, curve: Curves.ease),
  ),
);
交错菜单动画
class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
  static const _initialDelayTime = Duration(milliseconds: 50);
  static const _itemSlideTime = Duration(milliseconds: 250);
  static const _staggerTime = Duration(milliseconds: 50);
  static const _buttonDelayTime = Duration(milliseconds: 150);
  static const _buttonTime = Duration(milliseconds: 500);

  final _animationDuration =
      _initialDelayTime +
      (_staggerTime * _menuTitles.length) +
      _buttonDelayTime +
      _buttonTime;

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: _animationDuration,
      vsync: this,
    );
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
交错动画最佳实践
  • 使用 Interval 在时间上错开动画
  • 确保控制器的持续时间覆盖所有 Interval
  • 在各个 Interval 中使用曲线,让运动更自然
  • 考虑使用 timeDilation 调试时间安排
  • 让菜单项的延迟逐渐增加,营造涟漪效果

基于物理的动画

使用物理模拟创建自然流畅的动画。

Fling 动画
_controller.fling(
  velocity: 2.0,  // Units per second
);
自定义物理模拟
_controller.animateWith(
  SpringSimulation(
    spring: const SpringDescription(
      mass: 1,
      stiffness: 100,
      damping: 10,
    ),
    start: 0.0,
    end: 1.0,
    velocity: 0.0,
  ),
);
常见物理模拟
  • SpringSimulation - 弹簧物理效果
  • BouncingScrollSimulation - 带弹跳效果的滚动
  • ClampingScrollSimulation - 不带弹跳效果的滚动
  • GravitySimulation - 基于重力的模拟

最佳实践

应该做
  • 在 Widget 销毁时释放 AnimationController
  • 在监听器中使用 AnimatedBuilder/AnimatedWidget,而不是 setState()
  • 选择合适的曲线,让运动更自然
  • 使用 timeDilation 调试动画
  • 考虑性能(避免在动画构建过程中使用开销大的 Widget)
  • 在各种设备上测试动画
  • 支持反向动画,让交互感受更直观
不应该做
  • 忘记释放 AnimationController(会造成内存泄漏)
  • 在 AnimatedBuilder 足以满足需求时,仍在动画监听器中使用 setState()
  • 假设动画会立即完成(应处理 AnimationStatus)
  • 过度使用动画(动画可能会分散用户注意力)
  • 创建感觉“生硬”的动画(应使用平滑的曲线)
  • 忽略无障碍功能(尊重 disableAnimations 偏好设置)

资源

references/

implicit.md - 隐式动画 Widget 的完整参考,包含示例和最佳实践。

explicit.md - 深入介绍显式动画、AnimationController 和相关模式。

hero.md - Hero 动画指南,介绍标准过渡和径向过渡。

staggered.md - 交错动画模式和时间安排策略。

physics.md - 基于物理的动画和模拟。

curves.md - Curves 类参考,以及如何选择合适的曲线。

assets/templates/

常见动画模式的模板代码:

  • implicit_animation.dart - 隐式动画示例
  • explicit_animation.dart - 显式动画设置
  • hero_transition.dart - Hero 动画样板代码
  • staggered_animation.dart - 交错动画模板

评论

支持 Markdown