你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个页面称为 hero 动画,相同的动作有时也被称为 共享元素过渡。 引自-->. docs.flutter.cn/ui/animatio….
说白一点 就是, 同一个元素在不同页面之间的过渡动画.
举两个案例:
商品的详细页面
.用户的个人资料页
我们分析一下, 为什么在这种场景, 用hero 比较合适.
元素从一个页面平滑地过渡到另一个页面
,这种视觉上的连贯性
能够让用户更直观地理解两个页面之间的关联
,减少认知负担,从而提升用户体验
。商品图片
还是用户头像
,都是页面中比较关键的元素
。使用 hero 动画可以在页面转换时将用户的注意力集中在这些关键元素
上,强调其重要性,引导用户进一步了解相关信息。应用界面的一致性和整体性
。我们主要拿从文章列表 跳转到文章详情页面, 过渡文章的封面图, 过渡内容 大小 和 位置
.
效果:
仔细观察 我们就能看到 图片从外边到另外一个页面时,发生大小的变化 以及位置的偏移. 我们要实现起来也是非常的容易, 在这里我不讲 原理,只讲解如何使用的. 对原理实现感兴趣的大家可以去阅读这篇文章(docs.flutter.cn/ui/animatio…).
实现 1. 定义源 Hero 控件
Hero
widget。Hero
指定一个唯一的 tag
,用于识别这个共享元素。Hero(
tag: 'hero-tag',
child: FlutterLogo(size: 100), // 源页面的图形表示
)
2. 定义目标 Hero 控件
Hero
widget。Hero
使用与源 Hero 相同的 tag
。Hero(
tag: 'hero-tag',
child: FlutterLogo(size: 200), // 目标页面的图形表示
)
3. 创建目标路由
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Page')),
body: Center(
child: Hero(
tag: 'hero-tag',
child: FlutterLogo(size: 200), // 目标 Hero
),
),
);
}
}
4. 触发动画
Navigator.push
方法将目标路由推送到导航堆栈。Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
5. 动画过程
相似的 Widget 树:为了获得最佳效果,源 Hero 和目标 Hero 应该有相似的 Widget 树结构。 唯一的 Tag:确保
tag
在整个应用中是唯一的,以避免冲突。
径向 Hero 动画是一种特殊类型的 Hero 动画,它通过从一个点向外扩展或收缩来创建视觉效果,通常用于在页面之间共享元素。与常规的 Hero 动画相比,径向 Hero 动画更注重从中心点向外的过渡效果。
我们就以 官方的案例 进行演示讲解. docs.flutter.dev/ui/animatio…
RadialExpansion 类
class RadialExpansion extends StatelessWidget {
const RadialExpansion({
super.key,
required this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
final double maxRadius;
final clipRectSize;
final Widget child;
@override
Widget build(BuildContext context) {
return ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(
child: child, // Photo
),
),
),
);
}
}
构建底部四个 图表widget
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(4, (i) => _buildItem(context, i)),
)
构建每一个item
_buildItem(BuildContext context, int index) {
const radius = 30;
return CupertinoButton(
child: SizedBox(
width: radius * 2,
height: radius * 2,
child: Hero(
tag: 'hero_tag_$index',
createRectTween: (begin, end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
},
child: RadialExpansionWidget(
maxRadius: 120,
child: Container(
color: Colors.amber,
child: LayoutBuilder(builder: (context, constraints) {
return FlutterLogo(size: constraints.maxWidth);
})))),
),
onPressed: () {
Get.to(TargetPage(index: index));
});
}
构建路由页的class
class TargetPage extends StatelessWidget {
final int index;
const TargetPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
const maxRadius = 120.0;
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
color: Theme.of(context).canvasColor,
height: double.infinity,
width: double.infinity,
alignment: Alignment.center,
child: Card(
elevation: 8.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: maxRadius * 2,
height: maxRadius * 2,
child: Hero(
tag: 'hero_tag_$index',
createRectTween: (begin, end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
},
child: RadialExpansionWidget(
maxRadius: maxRadius,
child: Container(
color: Colors.red,
child: LayoutBuilder(
builder: (context, constraints) {
return FlutterLogo(size: constraints.maxWidth);
},
),
),
),
),
),
Text(
'第$index个Item',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16.0),
],
),
),
),
);
}
}