Hero 动画
:::次要内容 你将学习的内容
- hero 指的是在屏幕之间飞行的部件。
- 使用 Flutter 的 Hero 部件创建 hero 动画。
- 将 hero 从一个屏幕移动到另一个屏幕。
- 在将 hero 从一个屏幕移动到另一个屏幕的同时,使其形状从圆形转换为矩形。
- Flutter 中的 Hero 部件实现了一种通常被称为 共享元素过渡 或_共享元素动画_的动画风格。 :::
你可能多次见过 hero 动画。例如,一个屏幕显示一系列缩略图,代表待售商品。选择一个商品会将其移动到一个新屏幕,其中包含更多详细信息和“购买”按钮。在 Flutter 中,将图像从一个屏幕移动到另一个屏幕称为_hero 动画_,尽管同样的动作有时也称为 共享元素过渡 。
你可能想观看这个介绍 Hero 部件的一分钟视频:
Hero | Flutter widget of the week
本指南演示如何构建标准 hero 动画以及在飞行过程中将图像形状从圆形转换为方形的 hero 动画。
:::次要内容 示例 本指南在以下链接中提供了每种 hero 动画风格的示例。
:::次要内容 Flutter 新手? 此页面假设您知道如何使用 Flutter 的部件创建布局。有关更多信息,请参阅 在 Flutter 中构建布局。 :::
:::提示 术语 一个 Route 描述了 Flutter 应用中的一个页面或屏幕。 :::
你可以使用 Hero 部件在 Flutter 中创建此动画。 当 hero 从源路由动画到目标路由时,目标路由(减去 hero)会淡入视图。 通常,hero 是 UI 的小部分,例如图像,两个路由都有。从用户的角度来看,hero 在路由之间“飞行”。本指南展示了如何创建以下 hero 动画:
标准 hero 动画
标准 hero 动画 将 hero 从一个路由移动到一个新路由,通常会落在不同的位置并具有不同的尺寸。
以下视频(以慢速录制)显示了一个典型的示例。点击路由中心的鳍状物会将其移动到新的蓝色路由的左上角,尺寸更小。点击蓝色路由中的鳍状物(或使用设备的返回上一路由手势)会将鳍状物返回到原始路由。
Standard hero animation in Flutter
径向 hero 动画
在_径向 hero 动画_中,当 hero 在路由之间飞行时,它的形状似乎会从圆形变为矩形。
以下视频(以慢速录制)显示了一个径向 hero 动画的示例。开始时,路由底部出现一行三个圆形图像。点击任何圆形图像都会将该图像移动到一个新路由,该路由以方形显示它。点击正方形图像会将 hero 返回到原始路由,并以圆形显示。
Radial hero animation in Flutter
在转到特定于 标准 或 径向 hero 动画的部分之前,请阅读 hero 动画的基本结构 以了解如何构建 hero 动画代码,并阅读 幕后 以了解 Flutter 如何执行 hero 动画。
hero 动画的基本结构
#:::次要内容 关键点
- 使用不同路由中但具有匹配标签的两个 hero 部件来实现动画。
- Navigator 管理包含应用路由的堆栈。
- 在 Navigator 的堆栈上推送路由或弹出路由会触发动画。
- Flutter 框架计算一个矩形补间,
RectTween
它定义了 hero 从源路由飞到目标路由时的边界。 在飞行过程中,hero 会移动到应用程序叠加层,以便它显示在两个路由的顶部。 :::
:::提示 术语 如果你不了解补间或补间动画的概念,请查看Flutter 中的动画教程。 :::
Hero 动画是使用两个 Hero
部件实现的:一个描述源路由中的部件,另一个描述目标路由中的部件。从用户的角度来看,hero 似乎是共享的,只有程序员需要理解这个实现细节。Hero 动画代码具有以下结构:
- 定义一个起始 Hero 部件,称为_源 hero_。hero 指定其图形表示(通常是图像)和一个标识标签,并且位于源路由定义的当前显示部件树中。
- 定义一个结束 Hero 部件,称为_目标 hero_。此 hero 也指定其图形表示,以及与源 hero 相同的标签。至关重要的是,两个 hero 部件都使用相同的标签创建,通常是一个表示底层数据的对象。为了获得最佳效果,hero 应该具有几乎相同的部件树。
- 创建一个包含目标 hero 的路由。目标路由定义动画结束时存在的部件树。
- 通过在 Navigator 的堆栈上推送目标路由来触发动画。Navigator 的 push 和 pop 操作会为源路由和目标路由中具有匹配标签的每对 hero 触发 hero 动画。
Flutter 计算补间,该补间将 Hero 的边界从起点动画到终点(内插大小和位置),并在叠加层中执行动画。
下一部分将更详细地描述 Flutter 的过程。
幕后
#以下是 Flutter 如何在一个路由到另一个路由之间进行过渡的描述。
过渡前,源 hero 在源路由的部件树中等待。目标路由尚不存在,叠加层为空。
将路由推送到 Navigator
会触发动画。在 t=0.0
时,Flutter 执行以下操作:
使用 Material 运动规范中描述的曲线运动,计算目标 hero 的路径(屏幕外)。Flutter 现在知道 hero 最终会在哪里。
将目标 hero 放入叠加层,位置和大小与_源_ hero 相同。将 hero 添加到叠加层会更改其 Z 顺序,使其显示在所有路由的顶部。
将源 hero 移到屏幕外。
当 hero 飞行时,其矩形边界使用 TweencreateRectTween
属性中指定。默认情况下,Flutter 使用 MaterialRectArcTween
的实例,它沿着曲线路径为矩形的相对角点设置动画。(有关使用不同补间动画的示例,请参见 径向 hero 动画。)
飞行完成后:
Flutter 将 hero 部件从叠加层移动到目标路由。叠加层现在为空。
目标 hero 出现在目标路由中的最终位置。
源 hero 被恢复到其路由。
弹出路由会执行相同的过程,将 hero 动画回其在源路由中的大小和位置。
重要的类
#本指南中的示例使用以下类来实现 hero 动画:
Hero
- 从源路由飞到目标路由的部件。为源路由和目标路由定义一个 Hero,并为每个 Hero 分配相同的标签。Flutter 会为具有匹配标签的 hero 对设置动画。
InkWell
- 指定点击 hero 时发生的情况。
InkWell
的onTap()
方法构建新路由并将其推送到Navigator
的堆栈。 Navigator
Navigator
管理路由的堆栈。在 Navigator 的堆栈上推送路由或弹出路由会触发动画。Route
- 指定屏幕或页面。除最基本的应用外,大多数应用都有多个路由。
标准 hero 动画
#:::次要内容 关键点
- 使用
MaterialPageRoute
、CupertinoPageRoute
指定路由,或使用PageRouteBuilder
构建自定义路由。本节中的示例使用 MaterialPageRoute。 - 通过将目标的图像包装在
SizedBox
中来更改过渡结束时图像的大小。 - 通过将目标图像放置在布局部件中来更改图像的位置。这些示例使用
Container
。 :::
:::次要内容 标准 hero 动画代码 以下每个示例都演示了将图像从一个路由移动到另一个路由。本指南描述了第一个示例。
- hero_animation
- 将 hero 代码封装在一个自定义
PhotoHero
部件中。动画 hero 沿着曲线路径移动,如 Material 运动规范中所述。 - basic_hero_animation
- 直接使用 hero 部件。本指南未对此更基本的示例(为了参考而提供)进行描述。 :::
发生了什么?
#使用 Flutter 的 hero 部件很容易实现将图像从一个路由移动到另一个路由。当使用 MaterialPageRoute
指定新路由时,图像会沿着曲线路径移动,如 Material Design 运动规范 中所述。
创建新的 Flutter 示例 并使用 hero_animation 中的文件对其进行更新。
要运行示例:
- 点击主页路由的照片,将图像移动到显示同一照片的新路由,位置和比例不同。
- 通过点击图像或使用设备的返回上一路由手势返回到上一路由。
- 你可以使用
timeDilation
属性进一步减慢过渡速度。
PhotoHero 类
#自定义 PhotoHero 类维护 hero 及其大小、图像和点击时的行为。PhotoHero 构建以下部件树:
代码如下:
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.width,
});
final String photo;
final VoidCallback? onTap;
final double width;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}
关键信息:
- 当提供
HeroAnimation
作为应用程序的 home 属性时,MaterialApp
会隐式推送起始路由。 InkWell
包裹图像,使得向源 hero 和目标 hero 添加点击手势变得非常简单。- 使用透明颜色定义 Material 部件,使得图像在飞向目标时能够“弹出”背景。
SizedBox
指定动画开始和结束时 hero 的大小。- 将 Image 的
fit
属性设置为BoxFit.contain
,确保图像在过渡期间尽可能大,而不会改变其纵横比。
HeroAnimation 类
#HeroAnimation
类创建源 PhotoHero 和目标 PhotoHero,并设置过渡。
代码如下:
class HeroAnimation extends StatelessWidget {
const HeroAnimation({super.key});
Widget build(BuildContext context) {
//timeDilation = 5.0; // 1.0 表示正常动画速度。
return Scaffold(
appBar: AppBar(
title: const Text('基本 Hero 动画'),
),
body: Center(
//child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 300.0,
//onTap: () {
//Navigator.of(context).push(MaterialPageRoute<void>(
//builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text('鳍状物页面'),
),
body: Container(
// 将背景设置为蓝色以强调这是一个新路由。
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
//child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
//onTap: () {
//Navigator.of(context).pop();
//},
//),
),
);
//}
//));
//},
//),
),
);
}
}
关键信息:
- 当用户点击包含源 hero 的
InkWell
时,代码使用MaterialPageRoute
创建目标路由。将目标路由推送到Navigator
的堆栈会触发动画。 Container
将PhotoHero
定位在目标路由的左上角,位于AppBar
下方。- 目标
PhotoHero
的onTap()
方法弹出Navigator
的堆栈,触发将Hero
移动回原始路由的动画。 - 使用
timeDilation
属性在调试时减慢过渡速度。
径向 hero 动画
#:::次要内容 关键点
- 径向变换 将圆形形状动画成方形形状。
- 径向 hero 动画在将 hero 从源路由移动到目标路由的同时执行径向变换。
- MaterialRectCenterArcTween 定义补间动画。
- 使用
PageRouteBuilder
构建目标路由。 :::
在 hero 从圆形变换为矩形形状的同时,将其从一个路由移动到另一个路由,这是一种很酷的效果,你可以使用 Hero 部件来实现。为此,代码会为两个剪辑形状的交集设置动画:圆形和方形。在整个动画过程中,圆形剪辑(和图像)从 minRadius
缩放至 maxRadius
,而方形剪辑保持恒定大小。同时,图像从其在源路由中的位置移动到其在目标路由中的位置。有关此过渡的可视化示例,请参阅 Material 运动规范中的 径向变换。
此动画看起来可能很复杂(确实如此),但是你可以 根据自己的需要自定义提供的示例 。繁重的工作已经为你完成了。
:::次要内容 径向 hero 动画代码 以下每个示例都演示了径向 hero 动画。本指南描述了第一个示例。
- radial_hero_animation
- 如 Material 运动规范中所述的径向 hero 动画。
- basic_radial_hero_animation
- 最简单的径向 hero 动画示例。目标路由没有 Scaffold、Card、Column 或 Text。本指南未对此更基本的示例(为了参考而提供)进行描述。
- radial_hero_animation_animate
_rectclip - 通过也为矩形剪辑的大小设置动画来扩展 radial_hero_animation。本指南未对此更高级的示例(为了参考而提供)进行描述。 :::
:::提示 专业提示 径向 hero 动画涉及将圆形与方形相交。即使使用 timeDilation
减慢动画速度,也很难看到这一点,因此你可能需要在开发过程中启用 debugPaintSizeEnabled
标志。 :::
发生了什么?
#下图显示了动画开始 (t = 0.0
) 和结束 (t = 1.0
) 时的剪辑图像。
蓝色渐变(代表图像)指示剪辑形状相交的位置。在过渡开始时,交集的结果是一个圆形剪辑 (ClipOval
)。在变换过程中,ClipOval
从 minRadius
缩放至 maxRadius
,而 ClipRect 保持恒定大小。在过渡结束时,圆形和矩形剪辑的交集会产生一个与 hero 部件大小相同的矩形。换句话说,在过渡结束时,图像不再被剪辑。
创建新的 Flutter 示例 并使用 radial_hero_animation GitHub 目录中的文件对其进行更新。
要运行示例:
- 点击三个圆形缩略图之一,将图像动画到一个较大的正方形,该正方形位于遮挡原始路由的新路由的中间。
- 通过点击图像或使用设备的返回上一路由手势返回到上一路由。
- 你可以使用
timeDilation
属性进一步减慢过渡速度。
Photo 类
#Photo
类构建保存图像的部件树:
class Photo extends StatelessWidget {
const Photo({super.key, required this.photo, this.color, this.onTap});
final String photo;
final Color? color;
final VoidCallback onTap;
Widget build(BuildContext context) {
return //Material(
// 图像具有透明度的地方会出现稍微不透明的颜色。
//color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
child: //InkWell(
onTap: //onTap,
child: //Image.asset(
photo,
fit: BoxFit.contain,
//),
//),
//);
}
}
关键信息:
InkWell
捕获点击手势。调用函数将onTap()
函数传递给Photo
的构造函数。- 在飞行过程中,
InkWell
会在其第一个 Material 祖先上绘制其飞溅效果。 - Material 部件具有稍微不透明的颜色,因此图像的透明部分会以颜色呈现。这确保了圆形到方形的过渡很容易看到,即使对于具有透明度的图像也是如此。
Photo
类在其部件树中不包含Hero
。为了使动画工作,hero 需要包装RadialExpansion
部件。
RadialExpansion 类
#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
//),
//),
//),
//);
}
}
关键信息:
hero 包装
RadialExpansion
部件。当 hero 飞行时,其大小会发生变化,并且因为它限制了其子部件的大小,所以
RadialExpansion
部件的大小也会随之改变。RadialExpansion
动画是由两个重叠的剪辑创建的。示例使用
MaterialRectCenterArcTween
定义补间插值。hero 动画的默认飞行路径使用 hero 的角点对补间进行插值。这种方法会影响径向变换期间 hero 的纵横比,因此新的飞行路径使用MaterialRectCenterArcTween
使用每个 hero 的中心点对补间进行插值。代码如下:
dartstatic RectTween _createRectTween(Rect? begin, Rect? end) { return MaterialRectCenterArcTween(begin: begin, end: end); }
hero 的飞行路径仍然遵循弧线,但图像的纵横比保持不变。
[RectTween
]: https://api.flutter.dev/flutter/animation/RectTween-class.html 矩形补间
[Route]: /cookbook/navigation/navigation-basics 路由
[Route
]: https://api.flutter.dev/flutter/widgets/Route-class.html 路由
[Standard hero animation code]: #standard-hero-animation-code 标准 hero 动画代码
[Tween
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。