前言:
这一讲对vibecoding没那么重要,可以跳过,精细化过渡可以看看,只是为了整个知识体系的完备度。
一、核心内容
- 核心动画组件:AnimatedContainer(通用属性动画)、AnimatedOpacity(透明度专用)、AnimatedSwitcher(组件切换)、Hero(跨页面共享元素),均为「隐式动画」,无需手动管理控制器;
- 关键规则:AnimatedSwitcher 子组件需设唯一 key、Hero 动画需相同 tag、动画曲线(Curves)可优化动画的速度变化;
- 实战思路:单一动画聚焦核心属性,复合动画可组合多个隐式动画组件,优先满足「流畅性」而非「过度特效」。
本讲聚焦 Flutter 中基础过渡动画的实现,核心是让你掌握「无需手动控制动画控制器」的轻量化动画方案,解决页面元素状态变化(尺寸、透明度)、组件切换、页面间元素共享时的生硬跳转问题,让 UI 交互更流畅自然。
Flutter 基础过渡动画的核心是「状态驱动 + 自动动画」,底层逻辑可拆解为以下结构。
- 隐式动画组件(如 AnimatedContainer)内部封装了
AnimationController和Tween,无需手动管理动画生命周期; - 当组件的「可动画属性」(如 width、opacity)发生变化时,组件自动触发动画,按指定曲线和时长完成属性插值;
- Hero 动画是「跨页面共享元素动画」,核心是通过
tag关联两个页面的相同元素,Flutter 自动计算元素在两个页面的位置/尺寸差值并生成过渡动画。
二、核心技术点:案例、属性、注意事项
1. 隐式动画:AnimatedContainer
功能
监听容器属性(尺寸、颜色、圆角等)变化,自动生成过渡动画。
核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| duration | Duration | 动画时长(必传) |
| curve | Curve | 动画曲线(默认线性) |
| width/height | double | 容器尺寸(变化时触发动画) |
| decoration | Decoration | 背景、圆角、边框(变化时触发动画) |
| onEnd | VoidCallback | 动画结束回调 |
案例代码
import 'package:flutter/material.dart';
void main(){
runApp(const MyApp());
}
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context){
return MaterialApp(
title: "隐式动画",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AnimatedContainerDemo(),
);
}
}
class AnimatedContainerDemo extends StatefulWidget {
const AnimatedContainerDemo({super.key});
@override
State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
// 控制容器状态的变量
double _width = 100;
double _height = 100;
Color _color = Colors.blue;
double _borderRadius = 8;
// 切换容器状态
void _toggleState() {
setState(() {
_width = _width == 100 ? 200 : 100;
_height = _height == 100 ? 150 : 100;
_color = _color == Colors.blue ? Colors.red : Colors.blue;
_borderRadius = _borderRadius == 8 ? 30 : 8;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedContainer 案例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 核心:AnimatedContainer 替代普通 Container
AnimatedContainer(
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(_borderRadius),
),
duration: const Duration(milliseconds: 500), // 动画时长
curve: Curves.easeInOut, // 缓入缓出曲线
onEnd: () => print('动画结束'),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _toggleState,
child: const Text('切换状态'),
)
],
),
),
);
}
}
注意事项
- 必须通过
setState修改属性值才能触发动画(这里涉及到后面的内容,只需要知道,这是让变化更加丝滑就行了); - 仅监听「AnimatedContainer 自身的可动画属性」,子组件变化不会触发容器动画;
- 避免同时修改过多属性导致动画卡顿(建议单动画聚焦1-2个属性)。
2. 隐式动画:AnimatedOpacity
功能
专门用于控制组件透明度变化的隐式动画,比 AnimatedContainer 更轻量。(用处不大)
核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| opacity | double | 透明度(0=完全透明,1=完全不透明,必传) |
| duration | Duration | 动画时长(必传) |
| curve | Curve | 动画曲线 |
| child | Widget | 要控制透明度的子组件 |
案例代码
import 'package:flutter/material.dart';
void main(){
runApp(const MyApp());
}
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context){
return MaterialApp(
title: "AnimatedOpacity",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AnimatedOpacityDemo(),
);
}
}
class AnimatedOpacityDemo extends StatefulWidget {
const AnimatedOpacityDemo({super.key});
@override
State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}
class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
double _opacity = 1.0; // 初始完全不透明
void _toggleOpacity() {
setState(() {
_opacity = _opacity == 1.0 ? 0.2 : 1.0;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedOpacity 案例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _opacity,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
child: const Text(
'透明度动画',
style: TextStyle(fontSize: 30),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _toggleOpacity,
child: const Text('切换透明度'),
)
],
),
),
);
}
}
注意事项
- opacity 值必须在 0~1 之间,超出会报错;
- 透明状态下组件仍占据布局空间(区别于 Visibility 组件);
- 可结合
AnimatedSwitcher实现「透明+切换」复合动画。
3. 组件切换:AnimatedSwitcher
功能
监听子组件替换时,为「旧组件退出」和「新组件进入」添加过渡动画,解决组件切换生硬问题。
核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| duration | Duration | 动画时长(必传) |
| reverseDuration | Duration | 反向动画时长(可选) |
| transitionBuilder | Widget Function(Widget, Animation) | 自定义切换动画(如缩放、平移) |
| child | Widget | 要切换的子组件(必须有唯一 key) |
案例代码
import 'package:flutter/material.dart';
class AnimatedSwitcherDemo extends StatefulWidget {
const AnimatedSwitcherDemo({super.key});
@override
State<AnimatedSwitcherDemo> createState() => _AnimatedSwitcherDemoState();
}
class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
bool _isFirstChild = true;
void _toggleChild() {
setState(() {
_isFirstChild = !_isFirstChild;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedSwitcher 案例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 核心:AnimatedSwitcher 包裹切换的子组件
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
reverseDuration: const Duration(milliseconds: 300),
// 自定义切换动画:缩放+渐隐
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
// 子组件必须设置唯一 key
child: _isFirstChild
? Text(
'第一个组件',
key: const ValueKey(1),
style: const TextStyle(fontSize: 30),
)
: Container(
key: const ValueKey(2),
width: 150,
height: 150,
color: Colors.green,
child: const Center(
child: Text('第二个组件', style: TextStyle(fontSize: 20, color: Colors.white)),
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _toggleChild,
child: const Text('切换组件'),
)
],
),
),
);
}
}
注意事项
- 切换的子组件必须设置唯一 key(否则 Flutter 认为是同一个组件,不会触发动画);
- transitionBuilder 可组合多个动画(如 Scale + Fade + Slide);
- 若子组件是相同类型,需通过 key 区分(如 ValueKey 传不同值)。
4. 共享元素:Hero 动画
功能
实现「跨页面」的元素共享过渡(如列表项图片跳转到详情页图片),是页面跳转的核心动画之一。
核心规则
- 两个页面的共享元素必须设置相同的
heroTag(唯一标识); - Hero 组件需包裹要共享的元素(如 Image、Container);
- 动画由 Flutter 自动计算,无需手动控制。
案例代码
页面1(列表页)
import 'package:flutter/material.dart';
class HeroFirstPage extends StatelessWidget {
const HeroFirstPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero 动画 - 页面1')),
body: Center(
child: GestureDetector(
// 点击跳转到详情页
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const HeroSecondPage()),
),
// 共享元素:Hero 包裹图片
child: Hero(
tag: 'image_tag', // 唯一标识,两个页面必须相同
child: Image.network(
'https://picsum.photos/200/200',
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
),
),
);
}
}
页面2(详情页)
import 'package:flutter/material.dart';
class HeroSecondPage extends StatelessWidget {
const HeroSecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero 动画 - 页面2')),
body: Center(
// 共享元素:相同 tag 的 Hero 包裹大图
child: Hero(
tag: 'image_tag', // 与页面1一致
child: Image.network(
'https://picsum.photos/200/200',
width: 300,
height: 300,
fit: BoxFit.cover,
),
),
),
);
}
}
注意事项
- heroTag 必须全局唯一(多个 Hero 动画需用不同 tag);
- 共享元素建议是「静态组件」(如 Image、Container),避免包裹动态变化的组件;
- 页面跳转需用 MaterialPageRoute(CupertinoPageRoute 也支持,但动画效果不同)。
5. 动画曲线 Curves
功能
控制动画的「速度变化规律」,让动画更贴近真实物理效果(如加速、减速、回弹)。
常用曲线
| 曲线名称 | 效果 | 适用场景 |
|---|---|---|
| Curves.linear | 匀速 | 机械感动画(如进度条) |
| Curves.easeIn | 慢→快 | 进入动画(如弹窗弹出) |
| Curves.easeOut | 快→慢 | 退出动画(如弹窗消失) |
| Curves.easeInOut | 慢→快→慢 | 通用过渡(如容器尺寸变化) |
| Curves.bounceOut | 回弹 | 弹性效果(如按钮点击反馈) |
| Curves.elasticOut | 弹性 | 夸张的弹性效果(如卡片弹出) |
案例:不同曲线对比
AnimatedContainer(
width: _width,
height: _height,
color: _color,
duration: const Duration(milliseconds: 800),
curve: Curves.bounceOut, // 替换为不同曲线测试效果
);
注意事项
- 曲线仅影响「速度变化」,不改变动画时长;
- 避免过度使用弹性曲线(如 bounceOut),易导致视觉疲劳;
- 自定义曲线需继承
Curve类(新手建议先用内置曲线)。
四、综合应用案例:动画版商品卡片+详情页
功能说明
整合本章节所有技术,实现:
- 商品卡片点击时,AnimatedContainer 触发尺寸/颜色动画;
- 卡片内价格标签用 AnimatedOpacity 控制显隐;
- 卡片切换状态时用 AnimatedSwitcher 切换图标;
- 点击卡片跳转到详情页,用 Hero 实现图片共享动画;
- 所有动画使用 Curves 优化速度曲线。
完整代码
import 'package:flutter/material.dart';
// 主入口
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动画综合案例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ProductCardPage(),
);
}
}
// 商品卡片页面
class ProductCardPage extends StatefulWidget {
const ProductCardPage({super.key});
@override
State<ProductCardPage> createState() => _ProductCardPageState();
}
class _ProductCardPageState extends State<ProductCardPage> {
bool _isExpanded = false; // 卡片是否展开
bool _showPrice = true; // 是否显示价格
bool _isFavorite = false; // 是否收藏
// 切换卡片展开状态
void _toggleExpand() {
setState(() {
_isExpanded = !_isExpanded;
_showPrice = !_showPrice; // 同步切换价格显隐
_isFavorite = !_isFavorite; // 同步切换收藏状态
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('商品列表')),
body: Center(
child: GestureDetector(
onTap: () {
// 跳转到详情页
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProductDetailPage()),
);
},
child: AnimatedContainer(
width: _isExpanded ? 300 : 250,
height: _isExpanded ? 400 : 300,
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _isExpanded ? Colors.white : Colors.blue[50],
borderRadius: BorderRadius.circular(_isExpanded ? 20 : 10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
blurRadius: _isExpanded ? 10 : 5,
offset: const Offset(0, 3),
)
],
),
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut, // 缓入缓出曲线
child: Column(
children: [
// Hero 共享图片
Hero(
tag: 'product_image',
child: Image.network(
'https://picsum.photos/300/200',
width: double.infinity,
height: _isExpanded ? 200 : 150,
fit: BoxFit.cover,
),
),
const SizedBox(height: 10),
// 价格透明度动画
AnimatedOpacity(
opacity: _showPrice ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
child: const Text(
'¥99.00',
style: TextStyle(fontSize: 20, color: Colors.red),
),
),
const SizedBox(height: 10),
// 收藏图标切换动画
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) {
return RotationTransition(
turns: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: Icon(
_isFavorite ? Icons.favorite : Icons.favorite_border,
key: ValueKey(_isFavorite),
color: _isFavorite ? Colors.red : Colors.grey,
size: 30,
),
),
const Spacer(),
// 展开/收起按钮
ElevatedButton(
onPressed: _toggleExpand,
child: Text(_isExpanded ? '收起卡片' : '展开卡片'),
),
const SizedBox(height: 20),
],
),
),
),
),
);
}
}
// 商品详情页
class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('商品详情')),
body: Column(
children: [
// Hero 共享图片(与列表页同tag)
Hero(
tag: 'product_image',
child: Image.network(
'https://picsum.photos/300/200',
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
),
const Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'商品名称',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'商品详情描述:这是一个带所有基础过渡动画的商品卡片案例,包含AnimatedContainer、AnimatedOpacity、AnimatedSwitcher、Hero动画。',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
SizedBox(height: 20),
Text(
'¥99.00',
style: TextStyle(fontSize: 22, color: Colors.red),
),
],
),
),
],
),
);
}
}
效果说明
- 点击「展开/收起卡片」:AnimatedContainer 触发尺寸、背景、阴影动画;AnimatedOpacity 控制价格显隐;AnimatedSwitcher 切换收藏图标(带旋转+渐隐动画);
- 点击卡片任意位置:跳转到详情页,Hero 动画实现图片从小到大的平滑过渡;
- 所有动画均使用 Curves.easeInOut/Curves.easeOut 优化,效果更自然。