隐式动画
隐式动画(Implicit Animation)是 Flutter 中最简单的动画方式。你只需要告诉框架"我要变成什么样子",框架就会自动帮你把过渡动画做好。
一句话理解隐式动画
想象你调灯光亮度——你只管把旋钮拧到目标位置,灯光会自动渐变过去,你不需要手动控制每一帧的亮度。隐式动画就是这个"自动渐变"的过程。
本章内容
- 隐式动画的工作原理
- 常用隐式动画组件详解
- 动画曲线(Curves)的选择
- 隐式动画的适用场景和局限性
工作原理
隐式动画的核心逻辑只有三步:
1. 你设置一个新值(目标值)
2. 框架发现值变了
3. 框架自动从旧值过渡到新值你不需要创建动画控制器,不需要监听每一帧,不需要手动触发播放。只需要 setState() 修改目标值,剩下的交给框架。
通用参数
所有隐式动画组件都接受这三个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
duration | Duration | 动画持续时间,必填 |
curve | Curve | 动画曲线,默认 Curves.linear |
onEnd | VoidCallback? | 动画结束时的回调(可选) |
import 'package:flutter/material.dart';
class AnimatedContainerParamsExample extends StatefulWidget {
const AnimatedContainerParamsExample({super.key});
@override
State<AnimatedContainerParamsExample> createState() => _AnimatedContainerParamsExampleState();
}
class _AnimatedContainerParamsExampleState extends State<AnimatedContainerParamsExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('隐式动画参数')),
body: Center(
// ─── ★ AnimatedContainer ──────────────
child: AnimatedContainer(
// ─── ★ 动画持续 300ms ──────────────
duration: const Duration(milliseconds: 300),
// ─── ☆ 动画持续 300ms ──────────────
// ─── ★ 先加速后减速 ──────────────
curve: Curves.easeInOut,
// ─── ☆ 先加速后减速 ──────────────
// ─── ★ 动画结束回调 ──────────────
onEnd: () {
// 动画结束时触发
},
// ─── ☆ 动画结束回调 ──────────────
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.orange,
),
// ─── ☆ AnimatedContainer ──────────────
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: const Icon(Icons.play_arrow),
),
);
}
}duration 怎么选?
- 100~200ms:微小反馈(按钮缩放、颜色切换),用户几乎感觉不到"在动画"
- 300~500ms:常规过渡(展开收起、淡入淡出),最常用
- 500ms+:大型变化(页面切换、大幅度移动),慎用,太慢会感觉卡顿
AnimatedContainer
AnimatedContainer 是最常用的隐式动画组件。它可以对 Container 的几乎所有属性做动画:
- 尺寸(
width、height) - 颜色(
color、decoration中的颜色) - 圆角(
borderRadius) - 内外边距(
padding、margin) - 对齐方式(
alignment)
最简单的例子:点击变大变小
class AnimatedBoxDemo extends StatefulWidget {
const AnimatedBoxDemo({super.key});
@override
State<AnimatedBoxDemo> createState() => _AnimatedBoxDemoState();
}
class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
bool _expanded = false; // 状态:是否展开
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded), // 点击切换状态
// ─── ★ AnimatedContainer 点击变大变小 ──────────────
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
// 根据状态设置不同的值
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
),
alignment: Alignment.center,
child: Text(
_expanded ? '点我收起' : '点我展开',
style: const TextStyle(color: Colors.white),
),
),
// ─── ☆ AnimatedContainer 点击变大变小 ──────────────
);
}
}关键理解
AnimatedContainer 和 Container 的区别就一个字——"Animated"前缀。当属性值发生变化时,Container 会瞬间变到新值,而 AnimatedContainer 会平滑过渡。
同时动画多个属性
AnimatedContainer 可以同时对多个属性做动画,只需把它们都写进去:
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
// 以下属性变化时,都会自动做动画
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
padding: _expanded ? const EdgeInsets.all(24) : const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
boxShadow: _expanded
? [BoxShadow(color: Colors.black26, blurRadius: 16, offset: Offset(0, 4))]
: [],
),
)装饰器的注意事项
decoration 整体变化时会做动画,但 BoxDecoration 内部并不是每个属性都支持平滑过渡。例如 border(边框)的切换是瞬间完成的,不会渐变。支持平滑过渡的属性有:color、borderRadius、boxShadow 的偏移和模糊半径。
AnimatedOpacity
AnimatedOpacity 专门控制透明度动画,常用于显示/隐藏元素。
class FadeDemo extends StatefulWidget {
const FadeDemo({super.key});
@override
State<FadeDemo> createState() => _FadeDemoState();
}
class _FadeDemoState extends State<FadeDemo> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ─── ★ AnimatedOpacity 透明度动画 ──────────────
AnimatedOpacity(
// ─── ★ 1.0 完全可见,0.0 完全透明 ──────────────
opacity: _visible ? 1.0 : 0.0,
// ─── ☆ 1.0 完全可见,0.0 完全透明 ──────────────
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: const Center(
child: Text('Hello', style: TextStyle(color: Colors.white)),
),
),
),
// ─── ☆ AnimatedOpacity 透明度动画 ──────────────
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _visible = !_visible),
child: Text(_visible ? '隐藏' : '显示'),
),
],
);
}
}AnimatedOpacity vs Opacity
Opacity(opacity: 0)—— 元素透明但仍然占位,仍然参与布局、仍然接收点击事件AnimatedOpacity(opacity: 0)—— 同上,只是多了过渡动画
如果你希望元素消失后不占位,应该用条件判断直接移除 Widget:
// 不占位的"隐藏"
if (_visible) MyWidget()AnimatedSwitcher
AnimatedSwitcher 用于子组件切换时的过渡动画,比如切换不同的文字、不同的图标。
基本用法
class SwitcherDemo extends StatefulWidget {
const SwitcherDemo({super.key});
@override
State<SwitcherDemo> createState() => _SwitcherDemoState();
}
class _SwitcherDemoState extends State<SwitcherDemo> {
bool _showFirst = true;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ─── ★ AnimatedSwitcher 切换动画 ──────────────
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
// ─── ★ 自定义过渡效果 ──────────────
transitionBuilder: (child, animation) {
// ─── ☆ 自定义过渡效果 ──────────────
return FadeTransition(opacity: animation, child: child);
},
child: _showFirst
? Container(
key: const ValueKey('first'), // ← 必须设置不同的 key!
width: 100,
height: 100,
color: Colors.red,
child: const Center(child: Text('A')),
)
: Container(
key: const ValueKey('second'), // ← 必须设置不同的 key!
width: 100,
height: 100,
color: Colors.blue,
child: const Center(child: Text('B')),
),
),
// ─── ☆ AnimatedSwitcher 切换动画 ──────────────
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: const Text('切换'),
),
],
);
}
}key 是必须的!
AnimatedSwitcher 通过判断 child 是否变化来触发动画。如果新旧 child 类型相同(比如都是 Container),Flutter 会认为它们是"同一个"Widget,不会触发切换动画。
解决方法:给不同的 child 设置不同的 key,通常是 ValueKey('唯一标识')。
自定义过渡效果
transitionBuilder 让你完全控制切换动画的效果:
// 缩放切换
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
child: ...,
)
// 滑动切换
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0), // 从右侧滑入
end: Offset.zero,
).animate(animation);
return SlideTransition(position: offsetAnimation, child: child);
},
child: ...,
)AnimatedCrossFade
AnimatedCrossFade 用于两个组件之间的交叉淡入淡出。与 AnimatedSwitcher 不同的是,它可以同时显示两个组件的过渡:
class CrossFadeDemo extends StatefulWidget {
const CrossFadeDemo({super.key});
@override
State<CrossFadeDemo> createState() => _CrossFadeDemoState();
}
class _CrossFadeDemoState extends State<CrossFadeDemo> {
bool _showFirst = true;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ─── ★ AnimatedCrossFade 交叉淡入淡出 ──────────────
AnimatedCrossFade(
firstChild: Container(
width: 200,
height: 100,
color: Colors.red,
child: const Center(child: Text('第一个')),
),
secondChild: Container(
width: 200,
height: 100,
color: Colors.blue,
child: const Center(child: Text('第二个')),
),
crossFadeState: _showFirst
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
),
// ─── ☆ AnimatedCrossFade 交叉淡入淡出 ──────────────
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: const Text('切换'),
),
],
);
}
}AnimatedSwitcher vs AnimatedCrossFade 怎么选?
- AnimatedSwitcher:切换任意数量的子组件(2个、3个、10个都行),灵活性高
- AnimatedCrossFade:只在固定的两个组件间切换,写法更简单,但不能扩展到三个以上
大多数场景推荐 AnimatedSwitcher,它更通用。
TweenAnimationBuilder — 自定义动画
当内置的 AnimatedXxx 组件不能满足需求时,TweenAnimationBuilder 可以让你对任意值做动画:
class CustomAnimationDemo extends StatelessWidget {
const CustomAnimationDemo({super.key});
@override
Widget build(BuildContext context) {
// ─── ★ TweenAnimationBuilder 自定义动画 ──────────────
return TweenAnimationBuilder<double>(
// ─── ★ 值区间 ──────────────
tween: Tween(begin: 0, end: 1),
// ─── ☆ 值区间 ──────────────
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
builder: (context, value, child) {
// value 就是当前动画值,从 0 渐变到 1
return Opacity(
opacity: value, // 透明度 0 → 1
child: Transform.scale(
scale: 0.5 + value * 0.5, // 缩放 0.5 → 1.0
child: child,
),
);
},
// child 放在这里,避免重复构建
child: const Icon(Icons.star, size: 100, color: Colors.amber),
);
// ─── ☆ TweenAnimationBuilder 自定义动画 ──────────────
}
}什么时候用 TweenAnimationBuilder?
- 内置组件没有对应的
AnimatedXxx版本时(比如Transform.rotate没有AnimatedRotate——当然新版 Flutter 已有AnimatedRotation) - 你需要同时控制多个属性联动时
- 你需要自定义的动画值(非标准属性)
TweenAnimationBuilder 的本质是:你提供一个值的区间(tween),它帮你自动从起点插值到终点,每一帧把当前值传给你,你来决定怎么用这个值。
实战:数字滚动动画
TweenAnimationBuilder<int>(
tween: IntTween(begin: 0, end: 100),
duration: const Duration(seconds: 2),
curve: Curves.easeOut,
builder: (context, value, child) {
return Text(
'$value%', // 数字从 0 滚动到 100
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
);
},
)其他隐式动画组件速查
| 组件 | 动画效果 | 典型场景 |
|---|---|---|
AnimatedContainer | 容器属性变化 | 尺寸、颜色、圆角等过渡 |
AnimatedOpacity | 透明度变化 | 显示/隐藏元素 |
AnimatedPadding | 内边距变化 | 展开/收起内容 |
AnimatedPositioned | 位置变化(在 Stack 中) | 元素移动 |
AnimatedScale | 缩放变化 | 按钮点击反馈 |
AnimatedRotation | 旋转变化 | 刷新图标旋转 |
AnimatedDefaultTextStyle | 文字样式变化 | 字体大小、颜色过渡 |
AnimatedSwitcher | 子组件切换时的过渡 | 切换不同 Widget |
AnimatedCrossFade | 两个组件间的淡入淡出 | 两状态切换 |
TweenAnimationBuilder | 自定义动画值 | 任意值动画 |
Curves 动画曲线
动画曲线决定了动画的速度变化——是匀速、先快后慢、还是弹弹弹?
生活中的类比
| 曲线 | 类比 |
|---|---|
Curves.linear | 电梯匀速上升 |
Curves.easeOut | 汽车刹车——开始快,逐渐减速停下 |
Curves.easeIn | 汽车起步——慢慢开始,越来越快 |
Curves.easeInOut | 汽车起步到刹车——慢→快→慢 |
Curves.bounceOut | 球落地弹跳 |
Curves.elasticOut | 拉橡皮筋松手——超过目标再弹回来 |
常用曲线速查
Curves.linear // 线性(匀速),机械感
Curves.ease // 缓入缓出(默认推荐)
Curves.easeIn // 缓入(慢慢开始)
Curves.easeOut // 缓出(慢慢停止)—— 最常用
Curves.easeInOut // 缓入缓出
Curves.decelerate // 减速
Curves.bounceIn // 弹跳入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性入
Curves.elasticOut // 弹性出怎么选曲线?
- 90% 的情况:用
Curves.easeOut或Curves.easeInOut就够了 - 需要弹跳感:
Curves.bounceOut(比如按钮点击) - 需要弹性感:
Curves.elasticOut(比如下拉刷新回弹) - 不要用
linear:匀速动画看起来很机械,不自然
常见问题
1. 动画没有效果?
最常见原因:忘记了 setState()。隐式动画是靠检测属性变化来触发的,如果你只是改了变量但没有调用 setState(),Widget 不会重建,动画也不会触发。
// ❌ 错误:没有 setState
onTap: () => _expanded = !_expanded
// ✅ 正确:用 setState 包裹
onTap: () => setState(() => _expanded = !_expanded)2. AnimatedSwitcher 切换没有动画?
原因:新旧 child 的类型相同且没有设置不同的 key,Flutter 认为它们是同一个 Widget。
// ❌ 错误:没有 key,不会触发切换动画
child: _showFirst ? Text('A') : Text('B')
// ✅ 正确:设置不同的 key
child: _showFirst ? Text('A', key: ValueKey('a')) : Text('B', key: ValueKey('b'))3. 动画太快或太慢?
调整 duration 参数。一般原则:
- 感觉"闪了一下"→ 太快了,加到 300ms
- 感觉"卡卡的"→ 太慢了,减到 200ms
4. 想让动画循环播放?
隐式动画不支持循环播放。如果需要循环动画,请使用 显式动画 中的 AnimationController.repeat()。
