Flutter Hero 动画完全指南
·
Flutter Hero 动画完全指南
引言
Hero 动画是 Flutter 中实现页面间元素过渡的强大工具,它允许元素在页面跳转时平滑地从一个页面"飞"到另一个页面。本文将深入探讨 Hero 动画的各种用法和高级技巧。
基础概念回顾
什么是 Hero 动画
Hero 动画是一种共享元素过渡效果,它让同一个 Widget 在两个页面之间平滑过渡。
基本语法
// 源页面
Hero(
tag: 'hero-tag',
child: Image.network('image.jpg'),
)
// 目标页面
Hero(
tag: 'hero-tag',
child: Image.network('image.jpg'),
)
高级技巧一:基本 Hero 动画
创建简单的 Hero 动画
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: Hero(
tag: 'avatar',
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
child: CircleAvatar(
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
radius: 50,
),
),
),
),
);
}
}
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('详情页')),
body: Center(
child: Hero(
tag: 'avatar',
child: CircleAvatar(
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
radius: 100,
),
),
),
);
}
}
自定义过渡效果
Hero(
tag: 'custom-hero',
createRectTween: (begin, end) {
return CustomRectTween(begin: begin, end: end);
},
child: MyWidget(),
)
高级技巧二:Hero 动画样式
添加圆角
Hero(
tag: 'rounded-image',
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network('image.jpg'),
),
)
添加阴影
Hero(
tag: 'shadow-hero',
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(2, 2),
),
],
),
child: Image.network('image.jpg'),
),
)
高级技巧三:多个 Hero 动画
并行过渡多个元素
// 源页面
Row(
children: [
Hero(
tag: 'image-hero',
child: Image.network('image.jpg'),
),
Hero(
tag: 'text-hero',
child: Text('标题'),
),
],
)
// 目标页面
Column(
children: [
Hero(
tag: 'image-hero',
child: Image.network('image.jpg'),
),
Hero(
tag: 'text-hero',
child: Text('标题'),
),
],
)
高级技巧四:Hero 动画控制器
使用 HeroController
MaterialApp(
home: MyHomePage(),
heroController: HeroController(
createRectTween: (begin, end) {
return MaterialRectArcTween(begin: begin, end: end);
},
),
)
自定义过渡曲线
Hero(
tag: 'curve-hero',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return ScaleTransition(
scale: animation.drive(
CurveTween(curve: Curves.easeInOut),
),
child: Image.network('image.jpg'),
);
},
child: Image.network('image.jpg'),
)
实战案例:图片画廊
class GalleryPage extends StatelessWidget {
final List<String> images = [
'https://example.com/img1.jpg',
'https://example.com/img2.jpg',
'https://example.com/img3.jpg',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('图片画廊')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailImagePage(
imageUrl: images[index],
index: index,
),
),
);
},
child: Hero(
tag: 'image-$index',
child: Image.network(
images[index],
fit: BoxFit.cover,
),
),
);
},
),
);
}
}
class DetailImagePage extends StatelessWidget {
final String imageUrl;
final int index;
DetailImagePage({required this.imageUrl, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Hero(
tag: 'image-$index',
child: Image.network(imageUrl),
),
),
);
}
}
实战案例:卡片详情页
class CardListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('卡片列表')),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Hero(
tag: 'avatar-$index',
child: CircleAvatar(
child: Text('${index + 1}'),
),
),
title: Hero(
tag: 'title-$index',
child: Text('卡片 ${index + 1}'),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CardDetailPage(index: index),
),
);
},
),
);
},
),
);
}
}
class CardDetailPage extends StatelessWidget {
final int index;
CardDetailPage({required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Hero(tag: 'title-$index', child: Text('卡片 ${index + 1}'))),
body: Center(
child: Hero(
tag: 'avatar-$index',
child: CircleAvatar(
radius: 80,
child: Text('${index + 1}'),
),
),
),
);
}
}
实战案例:Hero 动画与页面路由
class CustomHeroRoute extends PageRouteBuilder {
final Widget page;
CustomHeroRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
}
// 使用
Navigator.push(
context,
CustomHeroRoute(page: DetailPage()),
);
常见问题与解决方案
Q1:Hero 动画不生效?
A:确保两个页面的 Hero tag 相同:
// 正确
Hero(tag: 'same-tag', child: ...)
// 错误
Hero(tag: 'tag1', child: ...) // 源页面
Hero(tag: 'tag2', child: ...) // 目标页面
Q2:如何自定义过渡路径?
A:使用 createRectTween:
Hero(
tag: 'custom-path',
createRectTween: (begin, end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
},
child: ...,
)
Q3:如何在 Hero 动画期间显示遮罩?
A:使用 flightShuttleBuilder:
Hero(
tag: 'mask-hero',
flightShuttleBuilder: (context, animation, flightDirection, from, to) {
return Material(
color: Colors.black54,
child: Image.network('image.jpg'),
);
},
child: Image.network('image.jpg'),
)
最佳实践
1. 使用唯一的 tag
// 推荐
Hero(tag: 'user-avatar-${user.id}', child: ...)
// 不推荐
Hero(tag: 'avatar', child: ...) // 多个元素使用相同 tag
2. 保持子 Widget 类型一致
// 推荐
Hero(tag: 'image', child: Image.network('url')) // 两边都是 Image
// 不推荐
Hero(tag: 'image', child: Image.network('url')) // 源页面
Hero(tag: 'image', child: Container()) // 目标页面
3. 避免复杂的 Hero 子 Widget
// 推荐
Hero(
tag: 'simple-hero',
child: Image.network('url'), // 简单 Widget
)
// 不推荐
Hero(
tag: 'complex-hero',
child: Container(
child: Column(
children: [...], // 复杂嵌套
),
),
)
总结
Flutter 的 Hero 动画是创建流畅页面过渡的强大工具。通过本文的学习,你应该能够:
- 创建基本的 Hero 动画
- 实现多个元素的并行过渡
- 自定义过渡效果和路径
- 处理常见问题
- 遵循最佳实践
掌握这些技巧,能够帮助你创建更加吸引人的用户体验。
更多推荐
所有评论(0)