Flutter 动画实战指南:从隐式动画到 Lottie/Rive 高级特效
动画是让 APP "活"起来的灵魂。Flutter 提供了从简单到复杂的完整动画体系:隐式动画(Implicit)一行代码出效果,显式动画(Explicit)精细控制每一帧,再加上第三方库 Lottie / Rive 的加持,几乎可以实现任何你能想到的动效。这篇文章覆盖 Flutter 动画的方方面面。
一、动画体系全景图
Flutter 的动画体系可以分为以下几个层次:
- 隐式动画(Implicit Animation):AnimatedContainer、AnimatedOpacity 等,值变自动触发动画
- 显式动画(Explicit Animation):AnimationController + Tween 手动驱动
- Hero 动画:页面间元素共享转场
- 物理动画:SpringSimulation 模拟真实物理效果
- 第三方动效:Lottie(JSON 动画)、Rive(状态机动画)
二、隐式动画(Implicit Animation)
最简单也最常用的方式——你只需要改变属性值,Flutter 自动在旧值和新值之间做过渡动画。
2.1 AnimatedContainer — 万能过渡容器
// 基础用法:点击切换颜色、大小、圆角
class ImplicitAnimDemo extends StatefulWidget {
@override
_ImplicitAnimDemoState createState() => _ImplicitAnimDemoState();
}
class _ImplicitAnimDemoState extends State<ImplicitAnimDemo> {
bool _isExpanded = false;
Color _color = Colors.blue;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
_color = _isExpanded ? Colors.purple : Colors.blue;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 600),
curve: Curves.elasticOut,
width: _isExpanded ? 300 : 150,
height: _isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(_isExpanded ? 30 : 12),
),
child: const Center(
child: Text('点我变化', style: TextStyle(color: Colors.white, fontSize: 18)),
),
),
);
}
}
2.2 隐式动画组件全家桶
Flutter 内置了丰富的隐式动画组件:
// AnimatedOpacity — 渐显渐隐
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 400),
child: Container(width: 100, height: 100, color: Colors.blue),
)
// AnimatedPadding — 内边距动画
AnimatedPadding(
padding: EdgeInsets.all(_selected ? 24 : 8),
duration: Duration(milliseconds: 300),
child: Card(child: Text('可点击卡片')),
)
// AnimatedAlign — 对齐位置动画
AnimatedAlign(
alignment: _top ? Alignment.topCenter : Alignment.bottomCenter,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
child: Icon(Icons.arrow_upward, size: 40),
)
// AnimatedDefaultTextStyle — 文字样式动画
AnimatedDefaultTextStyle(
style: TextStyle(fontSize: _large ? 32 : 18, color: _large ? Colors.red : Colors.grey),
duration: Duration(milliseconds: 300),
child: Text('动态文字'),
)
// AnimatedCrossFade — 两 Widget 交叉淡入淡出
AnimatedCrossfade(
firstChild: Icon(Icons.pause, size: 60),
secondChild: Icon(Icons.play_arrow, size: 60),
crossFadeState: _playing ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: Duration(milliseconds: 200),
)
// AnimatedSwitcher — 子 Widget 切换时的过渡效果
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
child: Text('第 $_count 次', key: ValueKey($_count)),
)
2.3 TweenAnimationBuilder — 自定义属性的隐式动画
当内置组件不够用时,用 TweenAnimationBuilder 可以对任意属性做动画:
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(seconds: 2),
builder: (context, value, child) {
return Transform.rotate(
angle: value * 6.28, // 一圈 = 2π ≈ 6.28
child: Container(
width: 80,
height: 80,
color: Color.lerp(Colors.blue, Colors.red, value)!,
child: const Icon(Icons.star, color: Colors.white),
),
);
},
)
三、显式动画(Explicit Animation)
当你需要精确控制动画的每一帧时,使用 AnimationController + Tween 组合。这是最强大的动画方式。
3.1 AnimationController 基础
class ExplicitAnimDemo extends StatefulWidget {
@override
_ExplicitAnimDemoState createState() => _ExplicitAnimDemoState();
}
class _ExplicitAnimDemoState extends State<ExplicitAnimDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 创建控制器,vsync 绑定 this 用于提供 Ticker
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
// 定义 Tween:从 0 到 1 的线性插值
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOutCubic,
);
// 监听动画状态
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
// 开始动画
_controller.forward();
}
@override
void dispose() {
_controller.dispose(); // 必须释放!
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: 0.5 + (_animation.value * 0.5), // 0.5x → 1.0x
child: Opacity(
opacity: _animation.value,
child: Container(
width: 120, height: 120,
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(20 * _animation.value),
boxShadow: [
BoxShadow(
color: Colors.deepPurple.withOpacity(0.4 * _animation.value),
blurRadius: 20 * _animation.value,
spreadRadius: 5 * _animation.value,
)
],
),
),
),
),
);
},
);
}
}
3.2 常用 Curve 曲线速查表
Curve 决定了动画的节奏感,选对曲线能让动效提升一个档次:
// 线性(无加速)
Curves.linear
// 标准曲线(推荐默认使用)
Curves.easeIn // 先慢后快
Curves.easeOut // 先快后慢(最常用)
Curves.easeInOut // 头尾慢中间快
// 弹性效果
Curves.bounceOut // 弹跳结束
Curves.elasticOut // 弹簧过冲
// 特殊曲线
Curves.fastOutSlowIn // 快出慢入(Material Design 标准)
Curves.slowMiddle // 中间减速
Curve(debounce: 0.3) // 反弹衰减(自定义)
// 贝塞尔自定义(最灵活)
CubicBezier(0.68, -0.55, 0.27, 1.55) // 过弹效果
CubicBezier(0.25, 0.1, 0.25, 1) // 标准 ease
四、Hero 动画 — 页面间丝滑转场
Hero 动画是 Flutter 最令人印象深刻的特性之一:两个页面的同名 Hero Widget 会自动产生飞入/飞出的过渡效果。
// ===== 页面 A:列表项 =====
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage(heroTag: item.id)),
),
child: Hero(
tag: 'hero-${item.id}', // 必须唯一
child: Image.network(item.imageUrl, width: 100, height: 100, fit: BoxFit.cover),
),
)
// ===== 页面 B:详情页 =====
Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 280,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(item.title),
background: Hero(
tag: 'hero-${item.id}',
child: Image.network(item.imageUrl, fit: BoxFit.cover),
),
),
),
SliverList(delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('详情内容 $index')),
childCount: 10,
)),
],
),
)
// ===== Hero 高级配置 =====
Hero(
tag: 'avatar-hero',
// 自定义飞行的形状(圆形 → 圆形)
createRectTween: (begin, end) => RectTween(begin: begin, end: end),
// 飞行曲线
flightShuttleBuilder: (flightContext, animation, flightDirection) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final double value = animation.value;
return Transform.rotate(
angle: value * math.pi * 2, // 飞行中旋转
child: Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(opacity: value, child: child),
),
);
},
);
},
placeholderBuilder: (context, size, heroWidget) {
return Container(color: Colors.grey[300]);
},
child: CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatar)),
)
五、交错动画(Staggered Animation)
让一组元素依次出现,而不是同时出现,这种"波浪式"入场效果叫交错动画(Staggered Animation),能极大提升视觉质感:
class StaggeredAnimDemo extends StatefulWidget {
@override
_StaggeredAnimDemoState createState() => _StaggeredAnimDemoState();
}
class _StaggeredAnimDemoState extends State<StaggeredAnimDemo>
with TickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1800));
@override
void initState() {
super.initState();
_controller.forward();
}
@override
void dispose() { _controller.dispose(); super.dispose(); }
// 每个元素的延迟不同
Interval _interval(int index, int total) {
final start = index / total;
const duration = 0.6; // 每个动画占总时长的比例
return Interval(start.clamp(0.0, 1.0 - duration), (start + duration).clamp(0.0, 1.0));
}
@override
Widget build(BuildContext context) {
final items = List.generate(6, (i) => i);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: items.map((i) {
return AnimatedBuilder(
animation: _controller.drive(_interval(i, items.length)),
builder: (context, child) {
return Transform.translate(
offset: Offset((1 - _controller.value) * 200, 0), // 从右侧滑入
child: Opacity(
opacity: _controller.value,
child: Container(
margin: EdgeInsets.symmetric(vertical: 8),
width: 280 - i * 8, // 渐变小
height: 64,
decoration: BoxDecoration(
color: Color.lerp(Colors.blue, Colors.purple, i / items.length)!,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text('列表项 ${i + 1}',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
),
),
);
},
);
}).toList(),
);
}
}
六、物理动画(Physics-based Animation)
使用弹簧模拟真实的物理效果,让交互更自然:
import 'package:flutter/physics.dart';
class SpringAnimationDemo extends StatefulWidget {
@override
_SpringAnimationDemoState createState() => _SpringAnimationDemoState();
}
class _SpringAnimationDemoState extends State<SpringAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
// 使用 SpringSimulation 物理模拟
_animation = _controller.drive(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
}
void _onPanDown(DragDownDetails details) {
_controller.stop(); // 用户触摸时停止当前动画
}
void _onPanUpdate(DragUpdateDetails details) {
_controller.value += details.delta.dx / 300; // 跟随手指拖拽
}
void _onPanEnd(DragEndDetails details) {
// 松手后模拟弹簧回弹
_controller.animateWith(SpringSimulation(
spring: SpringDescription(mass: 1, stiffness: 180, ratio: 0.9),
velocity: details.velocity.pixelsPerSecond.dx / 300,
// position: 从当前位置开始
from: _controller.value,
to: 0, // 回到中心
));
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: AnimatedBuilder(
animation: _animation,
builder: (context, _) {
return Align(
alignment: Alignment(-_animation.value, 0), // 左右拖动
child: Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
boxShadow: [BoxShadow(
color: Colors.orange.withOpacity(0.4),
blurRadius: 12 + _animation.value.abs() * 20,
)],
),
child: const Icon(Icons.pan_tool, color: Colors.white),
),
);
},
),
);
}
@override
void dispose() { _controller.dispose(); super.dispose(); }
}
七、Lottie 动画集成
Lottie 是 Airbnb 开源的高性能动画库,设计师可以用 After Effects 制作动画并导出 JSON 文件,Flutter 端直接渲染。
7.1 配置与基础用法
// pubspec.yaml
dependencies:
lottie: ^3.1.0
// 基本使用
import 'package:lottie/lottie.dart';
Lottie.asset(
'assets/animations/loading.json', // 本地 JSON 文件
width: 200,
height: 200,
fit: BoxFit.contain,
// 控制播放
controller: LottieAnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
repeat: true,
autoPlay: true,
),
)
// 网络加载
Lottie.network(
'https://assets2.lottiefiles.com/packages/lottie_animations/...',
width: 150,
height: 150,
)
// 实时控制
final lottieKey = GlobalKey<LottieState>();
Column(
children: [
Lottie.asset('anim/success.json', key: lottieKey, width: 200, height: 200),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(onPressed: () => lottieKey.currentState?.play(), child: Text('播放')),
ElevatedButton(onPressed: () => lottieKey.currentState?.pause(), child: Text('暂停')),
ElevatedButton(onPressed: () => lottieKey.currentState?.stop(), child: Text('停止')),
],
)
],
)
7.2 典型应用场景
- 加载动画:骨架屏、进度指示器、品牌化 loading
- 空状态插画:无数据、搜索无结果、网络错误
- 成功/失败反馈:打勾动画、报错抖动
- 引导页动画:首次使用的步骤演示
- Splash 页面:品牌 Logo 动态展示
八、Rive 动画集成
Rive(原名 Flare)比 Lottie 更进一步——支持状态机(State Machine)和运行时交互。动画可以在运行时通过代码切换状态、混合动画。
// pubspec.yaml
dependencies:
rive: ^0.13.0
import 'package:rive/rive.dart';
RiveAnimation.asset(
'assets/rive/character.riv', // Rive 文件
fit: BoxFit.contain,
stateMachines: ['State Machine 1'],
// 运行时控制状态机输入
onInit: (artboard) {
final smc = artboard.stateMachineByName('State Machine 1');
// 切换到 idle 状态
smc?.inputs.first.value = true;
},
)
// 更常见的使用方式:通过 RiveController
final riveController = RiveAnimationController('idle', 'assets/rive/bot.riv');
@override
void initState() {
super.initState();
riveController.addStatusListener((status) {
if (status == RiveStatus.complete) {
riveController.reset(); // 循环播放
}
});
riveController.active = true; // 开始播放
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// 点击切换到另一个状态
riveController.inputChange({'trigger': true});
},
child: RiveAnimation.simple(
'assets/rive/bot.riv',
controller: riveController,
fit: BoxFit.contain,
),
);
}
九、动画性能优化最佳实践
- 优先使用 const 构造函数:减少不必要的对象重建
- RepaintBoundary 包裹动画区域:隔离重绘范围,避免整树重建
- 避免在动画中使用复杂 Widget:动画中的子树越轻量越好
- 合理设置 duration:太短用户感知不到,太长感觉卡顿(推荐 200ms~600ms)
- saveLayer 与 clip:圆角裁剪会触发 saveLayer,影响性能
- shouldComponentate:显式动画中善用此方法避免不必要的重建
- GPU 加速:transform 和 opacity 触发合成层,不触发布局和绘制,性能最好
- will-change 属性:提前告知浏览器该元素会动画,但不要滥用
- prefers-reduced-motion:尊重用户的系统级动画偏好设置
总结
Flutter 的动画体系层次分明:日常 UI 交互用 Implicit Animation 一行搞定;需要精细控制的场景上 Explicit Animation;页面切换用 Hero;复杂序列用 Staggered Animation;自然交互用 Physics Simulation;设计师给的复杂动效用 Lottie/Rive。根据场景选择合适的方案,你的 APP 动效一定能脱颖而出。