flutter-animating-apps
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseImplementing Flutter Animations
Flutter动画实现指南
Contents
目录
Core Concepts
核心概念
Manage Flutter animations using the core typed system. Do not manually calculate frames; rely on the framework's ticker and interpolation classes.
Animation- : 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.
Animation<T> - : Instantiate this to drive the animation. It generates values (typically 0.0 to 1.0) tied to the screen refresh rate. Always provide a
AnimationController(usually viavsync) to prevent offscreen resource consumption. AlwaysSingleTickerProviderStateMixincontrollers to prevent memory leaks.dispose() - : Define a stateless mapping from an input range (usually 0.0-1.0) to an output type (e.g.,
Tween<T>,Color,Offset). Chain tweens with curves usingdouble..animate() - : Apply non-linear timing (e.g.,
Curve,Curves.easeIn) to an animation using aCurves.bounceOutorCurvedAnimation.CurveTween
使用Flutter核心的类型化系统管理动画。不要手动计算帧,应依赖框架的ticker和插值类。
Animation- :将其视为随时间变化的值的抽象表示。它包含状态(已完成、已关闭)并通知监听器,但不涉及任何UI相关逻辑。
Animation<T> - :实例化该类以驱动动画。它生成与屏幕刷新率绑定的值(通常为0.0到1.0)。必须提供
AnimationController(通常通过vsync)以避免消耗离屏资源。务必调用SingleTickerProviderStateMixin销毁控制器,防止内存泄漏。dispose() - :定义从输入范围(通常为0.0-1.0)到输出类型(如
Tween<T>、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., with
AnimationControllerorAnimatedBuilder).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 s driven by a single
TweenusingAnimationControllercurves).Interval
根据场景选择合适的动画实现方式:
- 若只需为简单属性变化(尺寸、颜色、透明度)添加动画,无需播放控制:使用隐式动画(如、
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., ) with its animated counterpart (e.g.,
Container).AnimatedContainer - Define the property.
duration - (Optional) Define the property for non-linear motion.
curve - Trigger the animation by updating the properties inside a call.
setState() - 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 (or
SingleTickerProviderStateMixinfor multiple controllers) to theTickerProviderStateMixinclass.State - Initialize an in
AnimationController, providinginitState()and avsync: this.duration - Define a and chain it to the controller using
Tween..animate() - Wrap the target UI in an (preferred for complex trees) or subclass
AnimatedBuilder.AnimatedWidget - Pass the object to the
Animation'sAnimatedBuilderproperty.animation - Control playback using ,
controller.forward(), orcontroller.reverse().controller.repeat() - Call in the
controller.dispose()method.dispose() - Run validator -> check for memory leaks -> ensure is called.
dispose()
- Add
当需要对动画生命周期进行精细控制时使用此流程。
- 任务进度:
- 为类添加
State(多控制器场景使用SingleTickerProviderStateMixin)。TickerProviderStateMixin - 在中初始化
initState(),传入AnimationController和vsync: this。duration - 定义并通过
Tween将其与控制器绑定。.animate() - 使用(复杂组件树优先)包裹目标UI,或继承
AnimatedBuilder。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 widget.
Hero - Assign a unique, data-driven to the source
tag.Hero - Wrap the destination widget in a widget.
Hero - Assign the exact same to the destination
tag.Hero - Ensure the widget trees inside both widgets are visually similar to prevent jarring jumps.
Hero - Trigger the transition by pushing the destination route via .
Navigator
- Wrap the source widget in a
此流程用于在两个路由间实现组件的飞行动画。
- 任务进度:
- 使用组件包裹源组件。
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 (do not set a fixed duration).
AnimationController - Capture gesture velocity using a (e.g.,
GestureDetectorprovidingonPanEnd).DragEndDetails - Convert the pixel velocity to the coordinate space of the animating property.
- Instantiate a with mass, stiffness, damping, and the calculated velocity.
SpringSimulation - Drive the controller using .
controller.animateWith(simulation)
- Set up an
此流程适用于手势驱动的自然运动效果。
- 任务进度:
- 初始化(无需设置固定时长)。
AnimationController - 使用捕获手势速度(如
GestureDetector传入onPanEnd)。DragEndDetails - 将像素速度转换为动画属性的坐标空间。
- 实例化,传入质量、刚度、阻尼和计算得到的速度。
SpringSimulation - 使用驱动控制器。
controller.animateWith(simulation)
- 初始化
Examples
示例
<details>
<summary><b>Example: Explicit Animation (Staggered with AnimatedBuilder)</b></summary>
</details>
<details>
<summary><b>Example: Custom Page Route Transition</b></summary>
</details>
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,
);
},
);
}
}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>
<summary><b>示例:显式动画(结合AnimatedBuilder的交错动画)</b></summary>
</details>
<details>
<summary><b>示例:自定义页面路由过渡</b></summary>
</details>
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,
);
},
);
}
}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()));