flutter-animating-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Implementing Flutter Animations

Flutter动画实现指南

Contents

目录

Core Concepts

核心概念

Manage Flutter animations using the core typed
Animation
system. Do not manually calculate frames; rely on the framework's ticker and interpolation classes.
  • Animation<T>
    : Treat this as an abstract representation of a value that changes over time. It holds state (completed, dismissed) and notifies listeners, but knows nothing about the UI.
  • AnimationController
    : Instantiate this to drive the animation. It generates values (typically 0.0 to 1.0) tied to the screen refresh rate. Always provide a
    vsync
    (usually via
    SingleTickerProviderStateMixin
    ) to prevent offscreen resource consumption. Always
    dispose()
    controllers to prevent memory leaks.
  • Tween<T>
    : Define a stateless mapping from an input range (usually 0.0-1.0) to an output type (e.g.,
    Color
    ,
    Offset
    ,
    double
    ). Chain tweens with curves using
    .animate()
    .
  • Curve
    : Apply non-linear timing (e.g.,
    Curves.easeIn
    ,
    Curves.bounceOut
    ) to an animation using a
    CurvedAnimation
    or
    CurveTween
    .
使用Flutter核心的类型化
Animation
系统管理动画。不要手动计算帧,应依赖框架的ticker和插值类。
  • Animation<T>
    :将其视为随时间变化的值的抽象表示。它包含状态(已完成、已关闭)并通知监听器,但不涉及任何UI相关逻辑。
  • AnimationController
    :实例化该类以驱动动画。它生成与屏幕刷新率绑定的值(通常为0.0到1.0)。必须提供
    vsync
    (通常通过
    SingleTickerProviderStateMixin
    )以避免消耗离屏资源。务必调用
    dispose()
    销毁控制器,防止内存泄漏。
  • Tween<T>
    :定义从输入范围(通常为0.0-1.0)到输出类型(如
    Color
    Offset
    double
    )的无状态映射。可通过
    .animate()
    将补间与曲线链式结合。
  • Curve
    :使用
    CurvedAnimation
    CurveTween
    为动画应用非线性时序(如
    Curves.easeIn
    Curves.bounceOut
    )。

Animation Strategies

动画策略

Apply conditional logic to select the correct animation approach:
  • If animating simple property changes (size, color, opacity) without playback control: Use Implicit Animations (e.g.,
    AnimatedContainer
    ,
    AnimatedOpacity
    ,
    TweenAnimationBuilder
    ).
  • If requiring playback control (play, pause, reverse, loop) or coordinating multiple properties: Use Explicit Animations (e.g.,
    AnimationController
    with
    AnimatedBuilder
    or
    AnimatedWidget
    ).
  • If animating elements between two distinct routes: Use Hero Animations (Shared Element Transitions).
  • If modeling real-world motion (e.g., snapping back after a drag): Use Physics-Based Animations (e.g.,
    SpringSimulation
    ).
  • If animating a sequence of overlapping or delayed motions: Use Staggered Animations (multiple
    Tween
    s driven by a single
    AnimationController
    using
    Interval
    curves).
根据场景选择合适的动画实现方式:
  • 若只需为简单属性变化(尺寸、颜色、透明度)添加动画,无需播放控制:使用隐式动画(如
    AnimatedContainer
    AnimatedOpacity
    TweenAnimationBuilder
    )。
  • 若需要精细的播放控制(播放、暂停、反向、循环)或协调多属性动画:使用显式动画(如结合
    AnimationController
    AnimatedBuilder
    AnimatedWidget
    )。
  • 若要在两个不同路由间为元素添加过渡动画:使用Hero动画(共享元素过渡)。
  • 若要模拟真实世界的运动效果(如拖拽后回弹):使用基于物理的动画(如
    SpringSimulation
    )。
  • 若要实现重叠或延迟的序列动画:使用交错动画(通过单个
    AnimationController
    驱动多个
    Tween
    ,使用
    Interval
    曲线)。

Workflows

实现流程

Implementing Implicit Animations

实现隐式动画

Use this workflow for "fire-and-forget" state-driven animations.
  • Task Progress:
    • Identify the target properties to animate (e.g., width, color).
    • Replace the static widget (e.g.,
      Container
      ) with its animated counterpart (e.g.,
      AnimatedContainer
      ).
    • Define the
      duration
      property.
    • (Optional) Define the
      curve
      property for non-linear motion.
    • Trigger the animation by updating the properties inside a
      setState()
      call.
    • Run validator -> review UI for jank -> adjust duration/curve if necessary.
该流程适用于“触发即忘”的状态驱动型动画。
  • 任务进度
    • 确定需要添加动画的目标属性(如宽度、颜色)。
    • 将静态组件(如
      Container
      )替换为对应的动画组件(如
      AnimatedContainer
      )。
    • 定义
      duration
      属性。
    • (可选)定义
      curve
      属性以实现非线性运动。
    • 通过在
      setState()
      调用中更新属性来触发动画。
    • 运行验证器 -> 检查UI是否存在卡顿 -> 必要时调整时长/曲线。

Implementing Explicit Animations

实现显式动画

Use this workflow when you need granular control over the animation lifecycle.
  • Task Progress:
    • Add
      SingleTickerProviderStateMixin
      (or
      TickerProviderStateMixin
      for multiple controllers) to the
      State
      class.
    • Initialize an
      AnimationController
      in
      initState()
      , providing
      vsync: this
      and a
      duration
      .
    • Define a
      Tween
      and chain it to the controller using
      .animate()
      .
    • Wrap the target UI in an
      AnimatedBuilder
      (preferred for complex trees) or subclass
      AnimatedWidget
      .
    • Pass the
      Animation
      object to the
      AnimatedBuilder
      's
      animation
      property.
    • Control playback using
      controller.forward()
      ,
      controller.reverse()
      , or
      controller.repeat()
      .
    • Call
      controller.dispose()
      in the
      dispose()
      method.
    • Run validator -> check for memory leaks -> ensure
      dispose()
      is called.
当需要对动画生命周期进行精细控制时使用此流程。
  • 任务进度
    • State
      类添加
      SingleTickerProviderStateMixin
      (多控制器场景使用
      TickerProviderStateMixin
      )。
    • initState()
      中初始化
      AnimationController
      ,传入
      vsync: this
      duration
    • 定义
      Tween
      并通过
      .animate()
      将其与控制器绑定。
    • 使用
      AnimatedBuilder
      (复杂组件树优先)包裹目标UI,或继承
      AnimatedWidget
    • Animation
      对象传递给
      AnimatedBuilder
      animation
      属性。
    • 使用
      controller.forward()
      controller.reverse()
      controller.repeat()
      控制动画播放。
    • dispose()
      方法中调用
      controller.dispose()
    • 运行验证器 -> 检查是否存在内存泄漏 -> 确保已调用
      dispose()

Implementing Hero Transitions

实现Hero过渡

Use this workflow to fly a widget between two routes.
  • Task Progress:
    • Wrap the source widget in a
      Hero
      widget.
    • Assign a unique, data-driven
      tag
      to the source
      Hero
      .
    • Wrap the destination widget in a
      Hero
      widget.
    • Assign the exact same
      tag
      to the destination
      Hero
      .
    • Ensure the widget trees inside both
      Hero
      widgets are visually similar to prevent jarring jumps.
    • Trigger the transition by pushing the destination route via
      Navigator
      .
此流程用于在两个路由间实现组件的飞行动画。
  • 任务进度
    • 使用
      Hero
      组件包裹源组件。
    • 为源
      Hero
      分配唯一的、基于数据的
      tag
    • 使用
      Hero
      组件包裹目标组件。
    • 为目标
      Hero
      分配完全相同
      tag
    • 确保两个
      Hero
      组件内的组件树视觉相似,避免出现突兀的跳转。
    • 通过
      Navigator
      推入目标路由以触发过渡动画。

Implementing Physics-Based Animations

实现基于物理的动画

Use this workflow for gesture-driven, natural motion.
  • Task Progress:
    • Set up an
      AnimationController
      (do not set a fixed duration).
    • Capture gesture velocity using a
      GestureDetector
      (e.g.,
      onPanEnd
      providing
      DragEndDetails
      ).
    • Convert the pixel velocity to the coordinate space of the animating property.
    • Instantiate a
      SpringSimulation
      with mass, stiffness, damping, and the calculated velocity.
    • Drive the controller using
      controller.animateWith(simulation)
      .
此流程适用于手势驱动的自然运动效果。
  • 任务进度
    • 初始化
      AnimationController
      (无需设置固定时长)。
    • 使用
      GestureDetector
      捕获手势速度(如
      onPanEnd
      传入
      DragEndDetails
      )。
    • 将像素速度转换为动画属性的坐标空间。
    • 实例化
      SpringSimulation
      ,传入质量、刚度、阻尼和计算得到的速度。
    • 使用
      controller.animateWith(simulation)
      驱动控制器。

Examples

示例

<details> <summary><b>Example: Explicit Animation (Staggered with AnimatedBuilder)</b></summary>
dart
class StaggeredAnimationDemo extends StatefulWidget {
  
  State<StaggeredAnimationDemo> createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _widthAnimation;
  late Animation<Color?> _colorAnimation;

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

    // Staggered width animation (0.0 to 0.5 interval)
    _widthAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
      ),
    );

    // Staggered color animation (0.5 to 1.0 interval)
    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
      ),
    );

    _controller.forward();
  }

  
  void dispose() {
    _controller.dispose(); // CRITICAL: Prevent memory leaks
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: _widthAnimation.value,
          height: 50.0,
          color: _colorAnimation.value,
        );
      },
    );
  }
}
</details> <details> <summary><b>Example: Custom Page Route Transition</b></summary>
dart
Route createCustomRoute(Widget destination) {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => destination,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(0.0, 1.0); // Start from bottom
      const end = Offset.zero;
      const curve = Curves.easeOut;

      final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
      final offsetAnimation = animation.drive(tween);

      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  );
}

// Usage: Navigator.of(context).push(createCustomRoute(const NextPage()));
</details>
<details> <summary><b>示例:显式动画(结合AnimatedBuilder的交错动画)</b></summary>
dart
class StaggeredAnimationDemo extends StatefulWidget {
  
  State<StaggeredAnimationDemo> createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _widthAnimation;
  late Animation<Color?> _colorAnimation;

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

    // 交错宽度动画(0.0到0.5时间区间)
    _widthAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
      ),
    );

    // 交错颜色动画(0.5到1.0时间区间)
    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
      ),
    );

    _controller.forward();
  }

  
  void dispose() {
    _controller.dispose(); // 关键:防止内存泄漏
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: _widthAnimation.value,
          height: 50.0,
          color: _colorAnimation.value,
        );
      },
    );
  }
}
</details> <details> <summary><b>示例:自定义页面路由过渡</b></summary>
dart
Route createCustomRoute(Widget destination) {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => destination,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(0.0, 1.0); // 从底部开始
      const end = Offset.zero;
      const curve = Curves.easeOut;

      final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
      final offsetAnimation = animation.drive(tween);

      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  );
}

// 使用方式:Navigator.of(context).push(createCustomRoute(const NextPage()));
</details>