精通 Flutter 动画开发:从基础显隐动画到自定义 Hero + 物理动画的全场景实战
dart@override// 动画控制器(核心)// 进度动画(0.0-1.0)// 旋转动画(0-2π)// 颜色动画(蓝色→红色→绿色)@override// 初始化动画控制器// 动画时长// vsync:防止动画在后台运行(需混入SingleTickerProviderStateMixin)// 动画范围(0.0-1.0)// 1. 进度动画(线性)// 2. 旋转动画(0-2π)//
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。
动画是 Flutter 应用的 “灵魂”—— 一个流畅的动画能让按钮点击、页面切换、数据加载等交互瞬间变得生动,而糟糕的动画则会让应用显得卡顿、廉价。但很多开发者仅停留在使用系统自带动画的层面,遇到自定义动画、复杂交互动画时就束手无策。本文将从基础的显隐动画入手,逐步实现 Hero 转场动画、物理动画、自定义动画控制器等进阶功能,既有严谨的代码规范,又有生动的场景拆解,让你彻底掌握 Flutter 动画开发的精髓。
一、Flutter 动画核心认知:为什么动画开发容易踩坑?
先理清 Flutter 动画的底层逻辑,避开新手常见误区:
- 动画的核心是 “状态插值 + 帧刷新”:Flutter 动画通过
Animation对象管理插值(从初始值到目标值的渐变),AnimationController控制动画生命周期,AnimatedWidget/AnimatedBuilder实现 UI 刷新; - 动画分类:
- 隐式动画:系统封装的动画(如
AnimatedOpacity/AnimatedContainer),只需修改状态,自动生成动画; - 显式动画:手动控制动画控制器(如
AnimationController+CurvedAnimation),灵活性更高; - 转场动画:页面 / 组件切换动画(如
Hero/PageRouteBuilder); - 物理动画:模拟真实物理规律的动画(如
SpringSimulation/GravitySimulation);
- 隐式动画:系统封装的动画(如
- 性能优化点:避免动画过程中重建 Widget、使用
RepaintBoundary隔离重绘区域、合理设置动画曲线。
本文所有代码基于:
plaintext
Flutter 3.32.0
Dart 3.9.0
二、入门:隐式动画(一键实现基础动效)
隐式动画是 Flutter 最友好的动画方式,无需手动管理控制器,只需修改 Widget 的属性,系统会自动生成过渡动画。我们先实现 “按钮点击显隐 + 容器尺寸 / 颜色渐变” 的基础案例。
2.1 完整代码实现
dart
import 'package:flutter/material.dart';
void main() => runApp(const AnimationDemoApp());
class AnimationDemoApp extends StatelessWidget {
const AnimationDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter动画实战',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ImplicitAnimationPage(),
);
}
}
class ImplicitAnimationPage extends StatefulWidget {
const ImplicitAnimationPage({super.key});
@override
State<ImplicitAnimationPage> createState() => _ImplicitAnimationPageState();
}
class _ImplicitAnimationPageState extends State<ImplicitAnimationPage> {
// 控制容器显隐
bool _isVisible = true;
// 控制容器尺寸
double _containerSize = 200;
// 控制容器颜色
Color _containerColor = Colors.blue;
// 控制容器圆角
double _borderRadius = 16;
// 切换显隐状态
void _toggleVisibility() {
setState(() {
_isVisible = !_isVisible;
});
}
// 随机修改容器属性
void _randomizeContainer() {
setState(() {
_containerSize = 100 + (Math.random() * 200); // 100-300px
_containerColor = Color.fromRGBO(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
1.0,
);
_borderRadius = Random().nextDouble() * 50; // 0-50px
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('隐式动画实战')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// 核心:AnimatedOpacity实现显隐动画
AnimatedOpacity(
// 透明度:显示1.0,隐藏0.0
opacity: _isVisible ? 1.0 : 0.0,
// 动画时长
duration: const Duration(milliseconds: 500),
// 动画曲线(缓入缓出)
curve: Curves.easeInOut,
// 动画结束后的状态(隐藏时不占空间)
child: AnimatedContainer(
// 尺寸动画
width: _containerSize,
height: _containerSize,
// 颜色动画
color: _containerColor,
// 圆角动画
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_borderRadius),
),
// 动画时长(可单独设置)
duration: const Duration(milliseconds: 800),
// 不同属性可设置不同曲线
curve: Curves.bounceOut,
// 容器内的内容
child: const Center(
child: Text(
'隐式动画',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
const SizedBox(height: 40),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _toggleVisibility,
child: Text(_isVisible ? '隐藏容器' : '显示容器'),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: _randomizeContainer,
child: const Text('随机修改容器'),
),
],
),
],
),
),
);
}
}
2.2 核心代码解析
1. 隐式动画核心控件
- AnimatedOpacity:专门用于透明度动画的隐式控件,核心属性:
opacity:目标透明度(0.0-1.0);duration:动画时长;curve:动画曲线(控制动画的速度变化);
- AnimatedContainer:万能的隐式动画控件,支持尺寸、颜色、圆角、边距等几乎所有容器属性的动画,只需修改属性值,自动生成过渡动画;
- 其他常用隐式动画:
AnimatedSize:尺寸动画;AnimatedPositioned:定位动画(需配合Stack);AnimatedSwitcher:组件切换动画;AnimatedTextStyle:文本样式动画。
2. 动画曲线(Curves)
Flutter 内置了丰富的动画曲线,不同曲线对应不同的动画效果:
Curves.easeInOut:缓入缓出(默认,最自然);Curves.bounceOut:回弹效果(类似皮球落地);Curves.elasticOut:弹性效果;Curves.decelerate:减速效果;Curves.linear:线性匀速效果。
3. 关键优化点
- 状态驱动动画:所有动画都由
setState修改状态触发,无需手动控制动画生命周期; - 动画叠加:
AnimatedOpacity嵌套AnimatedContainer实现多属性同时动画,Flutter 会自动处理动画叠加; - 性能优势:隐式动画内部已优化重绘逻辑,只会更新变化的属性,不会重建整个 Widget。
三、进阶:显式动画(手动控制动画生命周期)
隐式动画虽然简单,但灵活性不足(无法控制动画的暂停、反转、重复等)。显式动画通过AnimationController手动控制动画,适合复杂的交互场景。
3.1 实现自定义显式动画(进度条 + 旋转动画)
dart
import 'package:flutter/material.dart';
class ExplicitAnimationPage extends StatefulWidget {
const ExplicitAnimationPage({super.key});
@override
State<ExplicitAnimationPage> createState() => _ExplicitAnimationPageState();
}
class _ExplicitAnimationPageState extends State<ExplicitAnimationPage>
with SingleTickerProviderStateMixin {
// 动画控制器(核心)
late AnimationController _controller;
// 进度动画(0.0-1.0)
late Animation<double> _progressAnimation;
// 旋转动画(0-2π)
late Animation<double> _rotationAnimation;
// 颜色动画(蓝色→红色→绿色)
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
// 初始化动画控制器
_controller = AnimationController(
// 动画时长
duration: const Duration(seconds: 2),
// vsync:防止动画在后台运行(需混入SingleTickerProviderStateMixin)
vsync: this,
// 动画范围(0.0-1.0)
lowerBound: 0.0,
upperBound: 1.0,
);
// 1. 进度动画(线性)
_progressAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.linear),
);
// 2. 旋转动画(0-2π)
_rotationAnimation = Tween<double>(begin: 0.0, end: 2 * 3.1415926).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
// 3. 颜色动画(多颜色渐变)
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).chain(
ColorTween(begin: Colors.red, end: Colors.green),
).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
// 监听动画状态
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画完成后反转(从1.0→0.0)
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
// 动画回到初始状态后重新播放
_controller.forward();
}
});
}
// 控制动画播放/暂停
void _toggleAnimation() {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.forward(); // 从0.0→1.0播放
}
}
// 重置动画
void _resetAnimation() {
_controller.reset(); // 回到初始状态
}
@override
void dispose() {
// 释放动画控制器(必做!防止内存泄漏)
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('显式动画实战')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// 核心:AnimatedBuilder实现自定义动画
AnimatedBuilder(
// 动画对象(可传入多个动画的父控制器)
animation: _controller,
// 构建函数(只重建该区域,性能最优)
builder: (context, child) {
return Column(
children: [
// 旋转+颜色动画容器
Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: 200,
height: 200,
color: _colorAnimation.value,
child: const Center(
child: Text(
'显式动画',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
const SizedBox(height: 30),
// 进度条动画
LinearProgressIndicator(
value: _progressAnimation.value,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
_colorAnimation.value ?? Colors.blue,
),
minHeight: 10,
),
const SizedBox(height: 10),
Text(
'进度:${(_progressAnimation.value * 100).toStringAsFixed(0)}%',
style: const TextStyle(fontSize: 18),
),
],
);
},
),
const SizedBox(height: 40),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _toggleAnimation,
child: Text(_controller.isAnimating ? '暂停动画' : '播放动画'),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: _resetAnimation,
style: ElevatedButton.styleFrom(foregroundColor: Colors.red),
child: const Text('重置动画'),
),
],
),
],
),
),
);
}
}
3.2 显式动画核心逻辑解析
1. 动画控制器(AnimationController)
- 核心作用:控制动画的播放、暂停、反转、重置,管理动画的生命周期;
- vsync:必须传入
TickerProvider(通过混入SingleTickerProviderStateMixin实现),防止动画在页面不可见时继续运行,节省资源; - 生命周期管理:必须在
dispose中释放控制器,否则会导致内存泄漏; - 常用方法:
forward():从初始值播放到目标值;reverse():从目标值反转到初始值;stop():暂停动画;reset():重置到初始状态;repeat():重复播放动画。
2. 动画插值(Tween)
- Tween:定义动画的取值范围,如
Tween<double>(begin: 0.0, end: 1.0)表示从 0 到 1 的渐变; - ColorTween:颜色插值,支持从一个颜色渐变到另一个颜色;
- Chain:多个 Tween 串联,实现多阶段插值(如蓝色→红色→绿色);
- CurvedAnimation:为 Tween 添加动画曲线,控制动画的速度变化。
3. AnimatedBuilder(性能最优的显式动画方式)
- 核心优势:只重建
builder函数内的 Widget,不会重建整个父 Widget; - child 参数:如果有不参与动画的固定子 Widget,可通过
child参数传入,避免重复重建; - 多动画管理:可传入
AnimationController作为animation参数,监听所有基于该控制器的动画。
四、高阶 1:Hero 转场动画(页面间元素无缝过渡)
Hero 动画是 Flutter 中最具特色的转场动画之一,实现两个页面间同一元素的无缝过渡,比如点击商品图片跳转到详情页,图片会平滑地从列表位置过渡到详情页位置。
4.1 实现 Hero 动画(图片详情页转场)
第一步:列表页面(Hero 动画源)
dart
import 'package:flutter/material.dart';
import 'hero_detail_page.dart';
class HeroListPage extends StatelessWidget {
const HeroListPage({super.key});
// 模拟商品列表数据
final List<Map<String, String>> _productList = [
{
'id': '1',
'name': 'Flutter实战教程',
'image': 'https://picsum.photos/200/200?random=1',
},
{
'id': '2',
'name': 'Dart编程指南',
'image': 'https://picsum.photos/200/200?random=2',
},
{
'id': '3',
'name': '动画开发实战',
'image': 'https://picsum.photos/200/200?random=3',
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero动画列表页')),
body: GridView.builder(
padding: const EdgeInsets.all(20),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 两列
crossAxisSpacing: 20,
mainAxisSpacing: 20,
childAspectRatio: 1.0,
),
itemCount: _productList.length,
itemBuilder: (context, index) {
final product = _productList[index];
return GestureDetector(
onTap: () {
// 跳转到详情页
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HeroDetailPage(product: product),
),
);
},
child: Column(
children: [
// 核心:Hero控件(必须设置唯一tag)
Hero(
tag: product['id']!, // 唯一标识,详情页需相同
// flightShuttleBuilder:自定义过渡组件(可选)
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
// 自定义过渡动画(缩放+渐变)
return ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.8, end: 1.2).chain(
Tween<double>(begin: 1.2, end: 1.0),
),
),
child: FadeTransition(
opacity: animation,
child: Image.network(
product['image']!,
fit: BoxFit.cover,
),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
product['image']!,
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 10),
Text(
product['name']!,
style: const TextStyle(fontSize: 16),
),
],
),
);
},
),
);
}
}
第二步:详情页面(Hero 动画目标)
dart
import 'package:flutter/material.dart';
class HeroDetailPage extends StatelessWidget {
final Map<String, String> product;
const HeroDetailPage({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
// 核心:Hero控件(tag必须与列表页一致)
Hero(
tag: product['id']!,
child: Image.network(
product['image']!,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product['name']!,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
const Text(
'这是商品的详细描述信息,Hero动画实现了图片从列表页到详情页的无缝过渡,'
'无需手动控制动画,只需给两个页面的相同元素添加相同tag的Hero控件即可。',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
),
],
),
);
}
}
4.2 Hero 动画核心逻辑解析
1. Hero 动画核心规则
- 唯一 tag:两个页面的
Hero控件必须设置相同的tag(通常用数据的唯一 ID),Flutter 通过 tag 识别需要过渡的元素; - 无需控制器:Hero 动画由 Flutter 自动管理,无需手动创建
AnimationController; - 过渡效果:默认是位置和尺寸的平滑过渡,可通过
flightShuttleBuilder自定义过渡动画。
2. 自定义 Hero 过渡
flightShuttleBuilder:自定义过渡过程中的组件样式,可实现缩放、旋转、渐变等效果;HeroFlightDirection:判断动画方向(push/pop),可根据方向设置不同的过渡效果;transitionOnUserGestures:支持侧滑返回时的 Hero 动画(需配合PageRouteBuilder)。
3. 性能优化
- 图片预加载:如果图片是网络图片,建议提前预加载,避免过渡过程中图片加载导致的卡顿;
- 合理设置尺寸:Hero 动画会计算元素的尺寸和位置变化,避免过渡元素过大导致性能问题;
- RepaintBoundary:如果过渡元素包含复杂的子 Widget,可包裹
RepaintBoundary隔离重绘。
五、高阶 2:物理动画(模拟真实物理规律)
物理动画通过Simulation模拟真实世界的物理规律(如弹簧、重力、摩擦),让动画效果更自然、更贴近真实体验。
5.1 实现弹簧动画(拖拽小球回弹)
dart
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class PhysicsAnimationPage extends StatefulWidget {
const PhysicsAnimationPage({super.key});
@override
State<PhysicsAnimationPage> createState() => _PhysicsAnimationPageState();
}
class _PhysicsAnimationPageState extends State<PhysicsAnimationPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
// 记录拖拽的初始位置
Offset _dragStart = Offset.zero;
// 小球的当前位置
Offset _ballPosition = Offset.zero;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
// 监听动画更新,更新小球位置
_controller.addListener(() {
setState(() {
_ballPosition = _animation.value;
});
});
}
// 处理拖拽开始
void _onDragStart(DragStartDetails details) {
_controller.stop(); // 停止当前动画
_dragStart = details.globalPosition - _ballPosition;
}
// 处理拖拽中
void _onDragUpdate(DragUpdateDetails details) {
setState(() {
_ballPosition = details.globalPosition - _dragStart;
});
}
// 处理拖拽结束(触发物理动画)
void _onDragEnd(DragEndDetails details) {
// 创建弹簧模拟
final spring = SpringSimulation(
SpringDescription(
mass: 1.0, // 质量
stiffness: 100.0, // 刚度(越大越硬)
damping: 15.0, // 阻尼(越大回弹越少)
),
_ballPosition.dx, // 初始位置
MediaQuery.of(context).size.width / 2 - 25, // 目标位置(屏幕中心)
details.velocity.pixelsPerSecond.dx, // 初始速度(拖拽结束时的速度)
);
// 创建位置动画
_animation = _controller.drive(
Tween<Offset>(
begin: _ballPosition,
end: Offset(MediaQuery.of(context).size.width / 2 - 25, _ballPosition.dy),
).animate(
Animation<double>(
controller: _controller,
simulation: spring,
),
),
);
// 播放动画
_controller.forward(from: 0.0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('物理动画(弹簧)')),
body: Stack(
children: [
// 拖拽区域
Positioned(
left: _ballPosition.dx,
top: _ballPosition.dy == 0 ? MediaQuery.of(context).size.height / 2 - 25 : _ballPosition.dy,
child: GestureDetector(
onPanStart: _onDragStart,
onPanUpdate: _onDragUpdate,
onPanEnd: _onDragEnd,
child: Container(
width: 50,
height: 50,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
spreadRadius: 5,
),
],
),
),
),
),
// 提示文本
const Positioned(
bottom: 50,
left: 0,
right: 0,
child: Center(
child: Text(
'拖拽小球体验弹簧物理动画',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
),
),
],
),
);
}
}
5.2 物理动画核心逻辑解析
1. 核心物理模拟(Simulation)
Flutter 提供了多种物理模拟:
- SpringSimulation:弹簧模拟(最常用),核心参数:
mass:质量(越大运动越慢);stiffness:刚度(越大弹簧越硬,回弹越快);damping:阻尼(越大回弹越少,最终停止越快);
- GravitySimulation:重力模拟;
- FrictionSimulation:摩擦模拟;
- ClampingScrollSimulation:滚动阻尼模拟(类似列表滚动)。
2. 拖拽 + 物理动画结合
- 拖拽事件处理:
onPanStart:记录拖拽初始位置;onPanUpdate:实时更新小球位置;onPanEnd:获取拖拽结束时的速度,创建物理模拟并触发动画;
- 速度传递:将拖拽结束时的速度作为物理模拟的初始速度,让动画更贴合真实的拖拽体验。
3. 关键优化点
- 动画与拖拽的衔接:拖拽开始时停止当前动画,避免动画和拖拽冲突;
- 屏幕边界处理:实际开发中需添加边界检测,防止小球超出屏幕;
- 性能优化:物理模拟的计算量较大,避免在动画过程中进行复杂的计算。
六、动画开发避坑指南
- 内存泄漏:
- 显式动画的
AnimationController必须在dispose中释放; - 避免在
initState中创建无限循环的动画,忘记停止;
- 显式动画的
- 性能问题:
- 避免在
AnimatedBuilder的builder函数中创建新的 Widget(如Text('${value}')),可提前缓存; - 复杂动画使用
RepaintBoundary隔离重绘区域; - 避免同时进行多个高开销的动画(如大量元素的旋转 + 缩放);
- 避免在
- 动画卡顿:
- 网络图片提前预加载,避免动画过程中加载图片;
- 减少动画过程中的布局计算(如
LayoutBuilder); - 使用
const构造函数创建静态子 Widget;
- Hero 动画坑点:
- 两个页面的
Herotag 必须完全一致; - 避免 Hero 元素包含复杂的交互控件(如按钮);
- 侧滑返回时的 Hero 动画需手动配置
transitionOnUserGestures;
- 两个页面的
- 物理动画坑点:
- 弹簧参数需反复调试,避免过度回弹或僵硬;
- 拖拽速度的单位是
pixelsPerSecond,需注意单位转换。
七、总结
Flutter 动画开发的学习路径是 “隐式动画→显式动画→转场动画→物理动画”,核心原则是 “按需选型、性能优先、贴近真实”:
- 简单动效(显隐、尺寸变化):使用隐式动画(
AnimatedContainer/AnimatedOpacity),快速高效; - 复杂交互动画(进度条、自定义曲线):使用显式动画(
AnimationController+AnimatedBuilder),灵活可控; - 页面转场:使用 Hero 动画,实现元素无缝过渡;
- 真实交互体验(拖拽、回弹):使用物理动画,模拟真实物理规律。
动画开发的核心不是 “炫技”,而是 “提升用户体验”—— 一个好的动画应该是 “润物细无声” 的,既让交互更生动,又不影响性能和使用。比如按钮点击的微小缩放、列表加载的淡入动画、页面切换的平滑过渡,这些细节能让应用的体验提升一个档次。希望本文的实战案例和原理解析,能让你避开动画开发的 “坑”,写出既严谨又生动的 Flutter 动画代码。
更多推荐


所有评论(0)