第 10 章 动画(完整可运行示例)
三个层次:隐式动画(改参数自动过渡)→ 显式动画(控制器完全掌控)→ Hero(页面间共享元素)。一个文件跑完三种。
创建步骤
flutter create anim_demo && cd anim_demo;- 替换
lib/main.dart,无需额外依赖; - 点方块切换大小、点「播放淡入」看控制器动画、点头像跳详情看 Hero 飞行。
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: AnimDemoPage()));
class AnimDemoPage extends StatefulWidget {
const AnimDemoPage({super.key});
@override
State<AnimDemoPage> createState() => _AnimDemoPageState();
}
// SingleTickerProviderStateMixin 提供 vsync(把动画帧对齐屏幕刷新)
class _AnimDemoPageState extends State<AnimDemoPage>
with SingleTickerProviderStateMixin {
bool _big = false;
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
late final Animation<double> _opacity =
CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
@override
void dispose() {
_controller.dispose(); // 必须释放,否则页面退出后仍在跑
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('动画示例')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
const Text('① 隐式动画:AnimatedContainer', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 10),
Center(
child: GestureDetector(
onTap: () => setState(() => _big = !_big),
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutBack, // 带一点回弹
width: _big ? 180 : 90,
height: _big ? 180 : 90,
decoration: BoxDecoration(
color: _big ? Colors.orange : Colors.teal,
borderRadius: BorderRadius.circular(_big ? 40 : 12),
),
alignment: Alignment.center,
child: const Text('点我', style: TextStyle(color: Colors.white)),
),
),
),
const SizedBox(height: 34),
const Text('② 显式动画:AnimationController + FadeTransition',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 10),
Center(
child: FadeTransition(
opacity: _opacity,
child: const FlutterLogo(size: 90),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: () => _controller.forward(from: 0),
child: const Text('播放淡入'),
),
const SizedBox(width: 10),
OutlinedButton(
onPressed: () => _controller.reverse(),
child: const Text('淡出'),
),
],
),
const SizedBox(height: 34),
const Text('③ Hero:页面间共享元素动画',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 10),
Center(
child: GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DetailPage()),
),
child: const Hero(
tag: 'logo', // 两页 tag 必须一致
child: FlutterLogo(size: 64),
),
),
),
const SizedBox(height: 8),
const Center(
child: Text('点 Logo 进入详情,观察它「飞」过去',
style: TextStyle(fontSize: 12, color: Colors.grey)),
),
],
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情')),
body: const Center(
child: Hero(
tag: 'logo',
child: FlutterLogo(size: 180), // 尺寸不同 → 自动补间放大
),
),
);
}
}
关键点
- 能用
AnimatedXxx(隐式)就别手写控制器;只有需要暂停、反向、联动多个属性时才用AnimationController。 AnimationController必须dispose,且通常配合SingleTickerProviderStateMixin。CurvedAnimation决定节奏:easeInOut自然,easeOutBack带弹性。- Hero 的两个
tag必须唯一且一致;同一页出现相同 tag 会直接报错。 - 动画时长参考:页面切换 250~350ms,强调反馈 150~200ms,超过 400ms 会显得拖沓。
动态效果 · 隐式动画补间
Animated