简介
Hero 动画(也称为共享元素转场动画)可以在页面切换时为共享元素提供流畅的动画效果。它通常用于在列表页面和详情页面之间创建视觉连续性。
基本用法
简单示例
// 列表页面
Hero(
tag: 'imageHero',
child: Image.network(
'https://picsum.photos/250?image=1',
),
)
// 详情页面
Hero(
tag: 'imageHero',
child: Image.network(
'https://picsum.photos/250?image=1',
),
)
页面导航
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(),
),
);
完整示例
// 列表页面
class GridPage extends StatelessWidget {
final List<String> images = List.generate(
10,
(index) => 'https://picsum.photos/500?image=${index + 1}',
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Hero Animation')),
body: GridView.builder(
padding: EdgeInsets.all(8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
imageUrl: images[index],
tag: 'image_$index',
),
),
);
},
child: Hero(
tag: 'image_$index',
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(images[index]),
fit: BoxFit.cover,
),
),
),
),
);
},
),
);
}
}
// 详情页面
class DetailPage extends StatelessWidget {
final String imageUrl;
final String tag;
DetailPage({
required this.imageUrl,
required this.tag,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
color: Colors.black,
child: Center(
child: Hero(
tag: tag,
child: Image.network(imageUrl),
),
),
),
),
);
}
}
高级用法
自定义Hero动画
Hero(
tag: 'customHero',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return RotationTransition(
turns: animation,
child: Material(
color: Colors.transparent,
child: toHeroContext.widget,
),
);
},
child: YourWidget(),
)
占位Hero
Hero(
tag: 'placeholderHero',
placeholderBuilder: (
BuildContext context,
Size heroSize,
Widget child,
) {
return Container(
width: heroSize.width,
height: heroSize.height,
color: Colors.grey,
);
},
child: YourWidget(),
)
组合动画
class CombinedHeroAnimation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Hero(
tag: 'combinedHero',
child: Material(
color: Colors.transparent,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 500),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: child,
);
},
child: YourWidget(),
),
),
);
}
}
最佳实践
- 使用唯一的标签
// 推荐
Hero(tag: 'image_${item.id}', child: ...)
// 不推荐
Hero(tag: 'image', child: ...)
- 保持Hero内容一致
// 推荐
Hero(
tag: 'avatar',
child: CircleAvatar(
backgroundImage: NetworkImage(url),
),
)
// 不推荐在不同页面使用不同的widget
- 处理Hero动画中断(注:WillPopScope 在新版 Flutter 中已被弃用,请用 PopScope)
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
Navigator.pop(context);
}
},
child: YourHeroWidget(),
)
注意事项
- Hero标签必须在整个应用中唯一
- 避免在Hero动画期间修改widget树
- 注意内存管理,特别是大图片
- 合理设置动画时长
- 处理好Hero动画的交互
性能优化
- 使用适当的图片尺寸
Image.network(
imageUrl,
cacheWidth: 300,
cacheHeight: 300,
)
- 优化Hero内容
Hero(
tag: tag,
child: OptimizedImage(), // 使用优化过的图片组件
)
- 避免复杂的Hero内容
// 推荐
Hero(
tag: tag,
child: SimpleWidget(),
)
// 避免
Hero(
tag: tag,
child: ComplexWidget(), // 包含大量子widget的复杂组件
)
实战:Hero 的选型取舍
我在几个电商类小项目(美妆电商项目、家居电商项目 这类 GetX 栈)里,首页轮播图都是直接用的 carousel_slider——自动播放、指示器、滑动手势都是现成的,没必要为轮播手写动画。真正值得考虑 Hero 的场景是商品列表点进详情页:商品图从卡片位置”飞”到详情页大图,这个共享元素转场用默认的路由 push 动画做不出来,Hero 是正解。
但我落地时会先过三道取舍:第一,tag 必须全局唯一,我的习惯是用”商品 id 拼 tag”,两个同名 tag 同时在树上会直接抛异常;第二,Hero 两端的 widget 结构差异太大时,飞行中途会闪一下,不如两端保持简单一致;第三,快速滑动列表时频繁触发 Hero,转场会有卡顿感。所以我的结论很固定:轮播用现成组件,跨页共享元素才上 Hero,别为了炫技到处包 Hero。
总结
Hero 动画是 Flutter 中一个强大的页面转场动画特性,通过合理使用可以显著提升应用的用户体验。理解并掌握 Hero 动画的使用对于开发高质量的 Flutter 应用很有帮助。
