Hero 动画
介绍
你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个页面称为 hero 动画,相同的动作有时也被称为 共享元素过渡。 引自-->. docs.flutter.cn/ui/animatio….
说白一点 就是, 同一个元素在不同页面之间的过渡动画.
场景
举两个案例:
- 从商品的简介, 点击商品之后 跳转到
商品的详细页面
. - 从外边的文章列表, 点击文章之后, 跳转到
- 从外部用户的头像,点击头像之后, 跳转到
用户的个人资料页
我们分析一下, 为什么在这种场景, 用hero 比较合适.
-
增强用户体验
- 在页面切换过程中,通过 hero 动画,用户可以清晰地看到
元素从一个页面平滑地过渡到另一个页面
,这种视觉上的连贯性
能够让用户更直观地理解两个页面之间的关联
,减少认知负担,从而提升用户体验
。
- 在页面切换过程中,通过 hero 动画,用户可以清晰地看到
-
突出关键元素
- 无论是
商品图片
还是用户头像
,都是页面中比较关键的元素
。使用 hero 动画可以在页面转换时将用户的注意力集中在这些关键元素
上,强调其重要性,引导用户进一步了解相关信息。
- 无论是
-
保持界面的一致性
- 这种动画效果使得不同页面之间在元素过渡上保持一种统一的风格,不会让用户在页面跳转时感到突兀,有助于维护整个
应用界面的一致性和整体性
。
- 这种动画效果使得不同页面之间在元素过渡上保持一种统一的风格,不会让用户在页面跳转时感到突兀,有助于维护整个
场景模拟实现
我们主要拿从文章列表 跳转到文章详情页面, 过渡文章的封面图, 过渡内容 大小 和 位置
.
效果:
仔细观察 我们就能看到 图片从外边到另外一个页面时,发生大小的变化 以及位置的偏移. 我们要实现起来也是非常的容易, 在这里我不讲 原理,只讲解如何使用的. 对原理实现感兴趣的大家可以去阅读这篇文章(docs.flutter.cn/ui/animatio…).
实现 1. 定义源 Hero 控件
- 源 Hero:在第一个页面中创建一个
Hero
widget。 - 标签:为
Hero
指定一个唯一的tag
,用于识别这个共享元素。 - 图形表示:通常是一个图像或图标,放置在当前显示的控件树中。
Hero(
tag: 'hero-tag',
child: FlutterLogo(size: 100), // 源页面的图形表示
)
2. 定义目标 Hero 控件
- 目标 Hero:在第二个页面中创建另一个
Hero
widget。 - 相同的标签:确保这个
Hero
使用与源 Hero 相同的tag
。 - 图形表示:通常是一个更大的图形,表示动画结束时的状态。
Hero(
tag: 'hero-tag',
child: FlutterLogo(size: 200), // 目标页面的图形表示
)
3. 创建目标路由
- 目标路由:定义一个新的页面(目标路由),其中包含目标 Hero。
- 页面内容:在这个页面中,展示目标 Hero 的内容。
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
方法将目标路由推送到导航堆栈。 - 动画触发:当目标路由被推送时,Flutter 会自动处理源 Hero 和目标 Hero 之间的动画。
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
5. 动画过程
- 动画计算:Flutter 会计算从源 Hero 到目标 Hero 的动画路径,包括位置和大小的变化。
- 动画执行:在动画过程中,两个页面的 Hero 在叠加层中进行平滑过渡,给用户一种元素在页面之间移动的感觉。
相似的 Widget 树:为了获得最佳效果,源 Hero 和目标 Hero 应该有相似的 Widget 树结构。 唯一的 Tag:确保
tag
在整个应用中是唯一的,以避免冲突。
径向hero 动画
径向 Hero 动画是一种特殊类型的 Hero 动画,它通过从一个点向外扩展或收缩来创建视觉效果,通常用于在页面之间共享元素。与常规的 Hero 动画相比,径向 Hero 动画更注重从中心点向外的过渡效果。
- 径向变换 将圆形动画 变为方形
- 径向英雄动画在将英雄从源路线飞行到目标路线时执行径向变换
- MaterialRectCenterArcTween定义补间动画
- 使用PageRouteBuilder构建目标路线。
我们就以 官方的案例 进行演示讲解. 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),
],
),
),
),
);
}
}