Flutter Hero 动画完全指南
·
Flutter Hero 动画完全指南
引言
Hero 动画是 Flutter 中实现页面间过渡的强大工具。本文将深入探讨 Hero 动画的各种用法和高级技巧。
基础概念回顾
Hero 动画原理
- Hero Widget: 标记要共享的 widget
- Flight Route: 定义过渡路径
- HeroController: 控制动画过程
基本语法
// 源页面
Hero(
tag: 'hero-tag',
child: Image.network('image-url'),
)
// 目标页面
Hero(
tag: 'hero-tag',
child: Image.network('image-url'),
)
高级技巧一:基础 Hero 动画
简单图片过渡
// FirstScreen
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: Hero(
tag: 'imageHero',
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SecondScreen()),
);
},
child: Image.network(
'https://picsum.photos/250?image=9',
width: 100,
height: 100,
),
),
),
),
);
}
}
// SecondScreen
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Screen')),
body: Center(
child: Hero(
tag: 'imageHero',
child: Image.network('https://picsum.photos/250?image=9'),
),
),
);
}
}
自定义过渡效果
Hero(
tag: 'customHero',
createRectTween: (begin, end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
},
child: const Icon(Icons.star, size: 50),
)
高级技巧二:Hero 动画类型
矩形过渡
Hero(
tag: 'rectHero',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
圆形过渡
Hero(
tag: 'circleHero',
child: const CircleAvatar(
backgroundImage: NetworkImage('https://picsum.photos/250?image=9'),
radius: 50,
),
)
任意形状过渡
Hero(
tag: 'shapeHero',
child: ClipPath(
clipper: StarClipper(),
child: Container(
width: 100,
height: 100,
color: Colors.orange,
),
),
)
高级技巧三:Hero 动画参数
飞行速度
Hero(
tag: 'slowHero',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return ScaleTransition(
scale: animation.drive(Tween<double>(begin: 1.0, end: 1.5)),
child: const Icon(Icons.flight, size: 50),
);
},
child: const Icon(Icons.flight, size: 50),
)
自定义过渡路径
Hero(
tag: 'arcHero',
createRectTween: (begin, end) {
return MaterialRectArcTween(begin: begin, end: end);
},
child: const Icon(Icons.arrow_upward, size: 50),
)
高级技巧四:多个 Hero 动画
同步动画
// FirstScreen
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'hero1',
child: Container(width: 50, height: 50, color: Colors.red),
),
const SizedBox(width: 20),
Hero(
tag: 'hero2',
child: Container(width: 50, height: 50, color: Colors.blue),
),
],
)
// SecondScreen
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'hero1',
child: Container(width: 100, height: 100, color: Colors.red),
),
const SizedBox(height: 20),
Hero(
tag: 'hero2',
child: Container(width: 100, height: 100, color: Colors.blue),
),
],
)
实战案例:图片详情页
class GalleryScreen extends StatelessWidget {
const GalleryScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Gallery')),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 9,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailScreen(index: index),
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
},
child: Hero(
tag: 'image-$index',
child: Image.network(
'https://picsum.photos/200/200?image=$index',
fit: BoxFit.cover,
),
),
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
final int index;
const DetailScreen({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Hero(
tag: 'image-$index',
child: Image.network(
'https://picsum.photos/600/600?image=$index',
fit: BoxFit.contain,
),
),
),
);
}
}
实战案例:卡片展开动画
class CardScreen extends StatelessWidget {
const CardScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cards')),
body: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return CardItem(index: index);
},
),
);
}
}
class CardItem extends StatelessWidget {
final int index;
const CardItem({super.key, required this.index});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => CardDetail(index: index)),
);
},
child: Hero(
tag: 'card-$index',
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 60,
height: 60,
color: Colors.blue,
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Card Title'),
Text('Card description'),
],
),
],
),
),
),
),
);
}
}
class CardDetail extends StatelessWidget {
final int index;
const CardDetail({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Card Detail')),
body: Hero(
tag: 'card-$index',
child: Card(
elevation: 8,
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Container(
width: 200,
height: 200,
color: Colors.blue,
),
const SizedBox(height: 24),
const Text('Card Title', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
const Text(
'This is the detailed description of the card. It contains more information about the content.',
style: TextStyle(fontSize: 16),
),
],
),
),
),
),
);
}
}
实战案例:共享元素导航
class ListScreen extends StatelessWidget {
const ListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Items')),
body: ListView(
children: const [
ListItem(title: 'Item 1', subtitle: 'Description 1'),
ListItem(title: 'Item 2', subtitle: 'Description 2'),
ListItem(title: 'Item 3', subtitle: 'Description 3'),
],
),
);
}
}
class ListItem extends StatelessWidget {
final String title;
final String subtitle;
const ListItem({super.key, required this.title, required this.subtitle});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Hero(
tag: 'icon-$title',
child: const Icon(Icons.star),
),
title: Hero(
tag: 'title-$title',
child: Material(
type: MaterialType.transparency,
child: Text(title),
),
),
subtitle: Text(subtitle),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailScreen(title: title)),
);
},
);
}
}
class DetailScreen extends StatelessWidget {
final String title;
const DetailScreen({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Hero(
tag: 'icon-$title',
child: const Icon(Icons.star),
),
title: Hero(
tag: 'title-$title',
child: Text(title),
),
),
body: const Center(child: Text('Detail content')),
);
}
}
常见问题与解决方案
Q1:Hero 动画不生效?
A:确保两个页面使用相同的 tag:
Hero(
tag: 'same-tag',
child: ...,
)
Q2:Hero 重叠问题?
A:使用不同的 tag:
Hero(
tag: 'unique-tag-${item.id}',
child: ...,
)
Q3:动画卡顿?
A:避免在 Hero 中使用复杂 widget:
Hero(
tag: 'simple-hero',
child: Container(width: 50, height: 50, color: Colors.blue),
)
最佳实践
1. 使用唯一的 tag
Hero(
tag: 'user-avatar-${user.id}',
child: CircleAvatar(...),
)
2. 保持 widget 类型一致
// 源页面
Hero(tag: 'hero', child: Image.network(...))
// 目标页面
Hero(tag: 'hero', child: Image.network(...))
3. 使用 Material 包装文本
Hero(
tag: 'text-hero',
child: Material(
type: MaterialType.transparency,
child: Text('Hello'),
),
)
总结
Hero 动画是创建流畅页面过渡的强大工具。通过本文的学习,你应该能够:
- 创建基础 Hero 动画
- 使用不同类型的过渡
- 自定义动画参数
- 实现多个 Hero 动画
- 创建图片详情页过渡
- 实现卡片展开效果
掌握这些技巧,能够帮助你创建更加吸引人的用户体验。
更多推荐
所有评论(0)