英雄动画
你可能多次见过 Hero 动画。例如,一个屏幕显示一个缩略图列表,表示待售商品。选择一个项目会将其飞到一个新屏幕,其中包含更多详细信息和一个“购买”按钮。在 Flutter 中,将图像从一个屏幕飞到另一个屏幕称为Hero 动画,尽管相同的动作有时也称为共享元素过渡。
你可能想观看这段介绍 Hero widget 的一分钟视频
Hero | Flutter widget of the week
本指南演示了如何构建标准 Hero 动画以及在飞行过程中将图像从圆形转换为方形的 Hero 动画。
一个 路由 描述 Flutter 应用中的一个页面或屏幕。
您可以使用 Hero widget 在 Flutter 中创建此动画。当 Hero 从源路由动画到目标路由时,目标路由(减去 Hero)会淡入。通常,Hero 是 UI 的小部分,例如图像,这两个路由都有。从用户的角度来看,Hero 在路由之间“飞行”。本指南展示了如何创建以下 Hero 动画
标准 Hero 动画
标准 Hero 动画将 Hero 从一个路由飞到一个新路由,通常会落在不同的位置,并且大小也不同。
以下视频(以慢速录制)显示了一个典型的示例。点击路由中心的鳍状物会将其飞到一个新的蓝色路由的左上角,并且尺寸更小。点击蓝色路由中的鳍状物(或使用设备的返回上一个路由手势)会将鳍状物飞回原始路由。
径向 Hero 动画
在径向 Hero 动画中,当 Hero 在路由之间飞行时,其形状似乎从圆形变为矩形。
以下视频(以慢速录制)显示了一个径向 Hero 动画的示例。在开始时,路由底部出现一行三个圆形图像。点击任意一个圆形图像会将该图像飞到一个新路由,在新路由中以方形显示。点击方形图像会将 Hero 飞回原始路由,并以圆形显示。
在转到特定于 标准 或 径向 Hero 动画的部分之前,请阅读 Hero 动画的基本结构 以了解如何构建 Hero 动画代码,以及 幕后 以了解 Flutter 如何执行 Hero 动画。
Hero 动画的基本结构
#- 在不同的路由中使用两个 Hero widget 但使用匹配的标签来实现动画。
- Navigator 管理一个包含应用路由的栈。
- 在 Navigator 的栈上推送或弹出路由会触发动画。
- Flutter 框架计算一个矩形补间,
RectTween
,它定义了 Hero 在从源路由飞到目标路由时的边界。在飞行过程中,Hero 会移动到应用程序叠加层,以便它显示在两个路由的顶部。
如果您不了解补间或补间动画的概念,请查看 Flutter 中的动画教程。
Hero 动画是使用两个 Hero
widget 实现的:一个描述源路由中的 widget,另一个描述目标路由中的 widget。从用户的角度来看,Hero 似乎是共享的,只有程序员需要了解此实现细节。Hero 动画代码具有以下结构
- 定义一个起始 Hero widget,称为源 Hero。Hero 指定其图形表示(通常是图像)和一个标识标签,并且位于源路由定义的当前显示的 widget 树中。
- 定义一个结束 Hero widget,称为目标 Hero。此 Hero 还指定其图形表示,以及与源 Hero 相同的标签。这两个 Hero widget 必须使用相同的标签创建,通常是表示基础数据的对象。为了获得最佳效果,Hero 应该具有几乎相同的 widget 树。
- 创建一个包含目标 Hero 的路由。目标路由定义动画结束时存在的 widget 树。
- 通过在 Navigator 的栈上推送目标路由来触发动画。Navigator 的 push 和 pop 操作会为源路由和目标路由中具有匹配标签的每对 Hero 触发 Hero 动画。
Flutter 计算补间,该补间将 Hero 的边界从起点动画到终点(插值大小和位置),并在叠加层中执行动画。
下一节将更详细地描述 Flutter 的过程。
幕后
#以下是 Flutter 如何从一个路由过渡到另一个路由的描述。
过渡前,源 Hero 在源路由的 widget 树中等待。目标路由尚不存在,叠加层为空。
将路由推送到Navigator
会触发动画。在t=0.0
时,Flutter 会执行以下操作
根据 Material 运动规范中描述的曲线运动,计算目标 Hero 的路径(屏幕外)。Flutter 现在知道 Hero 最终会到达哪里。
将目标 Hero 放置在叠加层中,与源 Hero 的位置和大小相同。将 Hero 添加到叠加层会更改其 Z 顺序,使其显示在所有路由的顶部。
将源 Hero 移到屏幕外。
当 Hero 飞行时,其矩形边界使用 Tween<Rect> 进行动画处理,该动画在 Hero 的 createRectTween
属性中指定。默认情况下,Flutter 使用 MaterialRectArcTween
的实例,该实例沿曲线路径对矩形的相对角进行动画处理。(有关使用不同 Tween 动画的示例,请参阅 径向 Hero 动画。)
飞行完成后
Flutter 将 Hero widget 从叠加层移动到目标路由。叠加层现在为空。
目标 Hero 出现在目标路由中的最终位置。
源 Hero 恢复到其路由。
弹出路由会执行相同的过程,将 Hero 动画回源路由中的大小和位置。
基本类
#本指南中的示例使用以下类来实现 Hero 动画
Hero
- 从源路由飞到目标路由的 widget。为源路由和目标路由定义一个 Hero,并为每个 Hero 分配相同的标签。Flutter 会对具有匹配标签的 Hero 对进行动画处理。
InkWell
- 指定点击 Hero 时会发生什么。
InkWell
的onTap()
方法构建新路由并将其推送到Navigator
的栈上。 Navigator
Navigator
管理一个路由栈。在 Navigator 的栈上推送或弹出路由会触发动画。Route
- 指定屏幕或页面。除了最基本的应用之外,大多数应用都有多个路由。
标准 Hero 动画
#- 使用
MaterialPageRoute
、CupertinoPageRoute
指定路由,或使用PageRouteBuilder
构建自定义路由。本节中的示例使用 MaterialPageRoute。 - 通过将目标的图像包装在
SizedBox
中来更改过渡结束时图像的大小。 - 通过将目标的图像放置在布局 widget 中来更改图像的位置。这些示例使用
Container
。
以下每个示例都演示了将图像从一个路由飞到另一个路由。本指南描述了第一个示例。
- hero_animation
- 将 Hero 代码封装在自定义
PhotoHero
widget 中。根据 Material 运动规范对 Hero 的运动沿曲线路径进行动画处理。 - basic_hero_animation
- 直接使用 Hero widget。此更基本的示例(为了您的参考提供)在本指南中未作描述。
发生了什么?
#使用 Flutter 的 Hero widget 可以轻松地实现将图像从一个路由飞到另一个路由。当使用MaterialPageRoute
指定新路由时,图像会沿曲线路径飞行,如 Material Design 运动规范 所述。
创建一个新的 Flutter 示例 并使用 hero_animation 中的文件对其进行更新。
要运行示例
- 点击主页路由的照片,将图像飞到一个新路由,在新路由中以不同的位置和比例显示相同的照片。
- 通过点击图像或使用设备的返回上一个路由手势返回上一个路由。
- 您可以使用
timeDilation
属性进一步降低过渡速度。
PhotoHero 类
#自定义 PhotoHero 类维护 Hero 及其大小、图像和点击时的行为。PhotoHero 构建以下 widget 树
代码如下
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
包裹着图像,使得为源和目标英雄添加点击手势变得非常简单。 - 使用透明颜色定义 Material 组件,使图像在飞向目标位置时能够“弹出”背景。
SizedBox
指定了动画开始和结束时英雄的大小。- 将 Image 的
fit
属性设置为BoxFit.contain
,确保图像在过渡期间尽可能大,而不会改变其纵横比。
HeroAnimation 类
#HeroAnimation
类创建源和目标 PhotoHeroes,并设置过渡。
代码如下
class HeroAnimation extends StatelessWidget {
const HeroAnimation({super.key});
Widget build(BuildContext context) {
timeDilation = 5.0; // 1.0 means normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
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('Flippers Page'),
),
body: Container(
// Set background to blue to emphasize that it's a new route.
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();
},
),
),
);
}
));
},
),
),
);
}
}
关键信息
- 当用户点击包含源英雄的
InkWell
时,代码使用MaterialPageRoute
创建目标路由。将目标路由推入Navigator
的堆栈会触发动画。 Container
将PhotoHero
定位在目标路由的左上角,位于AppBar
下方。- 目标
PhotoHero
的onTap()
方法弹出Navigator
的堆栈,触发将Hero
飞回原始路由的动画。 - 使用
timeDilation
属性在调试时减慢过渡速度。
径向 Hero 动画
#- 径向变换将圆形形状动画化为方形形状。
- 径向英雄动画在将英雄从源路由飞到目标路由的同时执行径向变换。
- MaterialRectCenterArcTween 定义了补间动画。
- 使用
PageRouteBuilder
构建目标路由。
让英雄从一个路由飞到另一个路由,同时从圆形变换为矩形,这是一种流畅的效果,您可以使用 Hero 组件实现。为了实现这一点,代码动画化了两个剪辑形状的交集:圆形和方形。在整个动画过程中,圆形剪辑(以及图像)从 minRadius
缩放至 maxRadius
,而方形剪辑保持恒定大小。同时,图像从其在源路由中的位置飞到其在目标路由中的位置。有关此过渡的视觉示例,请参阅材质运动规范中的径向变换。
此动画可能看起来很复杂(确实如此),但您可以根据自己的需求自定义提供的示例。繁重的工作已经为您完成。
以下每个示例都演示了一个径向英雄动画。本指南描述了第一个示例。
- radial_hero_animation
- 材质运动规范中描述的径向英雄动画。
- basic_radial_hero_animation
- 径向英雄动画的最简单示例。目标路由没有 Scaffold、Card、Column 或 Text。此基本示例仅供参考,本指南未对其进行描述。
- radial_hero_animation_animate
_rectclip - 通过同时动画化矩形剪辑的大小来扩展 radial_hero_animation。此更高级的示例仅供参考,本指南未对其进行描述。
径向英雄动画涉及圆形与方形的相交。即使使用 timeDilation
减慢动画速度,也很难看到这一点,因此您可能需要在开发期间启用debugPaintSizeEnabled
标志。
发生了什么?
#下图显示了动画开始(t = 0.0
)和结束(t = 1.0
)时的剪辑图像。
蓝色渐变(代表图像)指示剪辑形状相交的位置。在过渡开始时,交集的结果是圆形剪辑(ClipOval
)。在变换过程中,ClipOval
从 minRadius
缩放至 maxRadius
,而ClipRect 保持恒定大小。在过渡结束时,圆形和矩形剪辑的交集会产生一个与英雄组件大小相同的矩形。换句话说,在过渡结束时,图像不再被剪辑。
创建一个新的 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(
// Slightly opaque color appears where the image has transparency.
color: Theme.of(context).primaryColor.withOpacity(0.25),
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
);
}
}
关键信息
InkWell
捕获点击手势。调用函数将onTap()
函数传递给Photo
的构造函数。- 在飞行过程中,
InkWell
在其第一个 Material 父组件上绘制其水波纹。 - Material 组件具有略微不透明的颜色,因此图像的透明部分会以颜色渲染。这确保了圆形到方形的过渡易于查看,即使对于具有透明度的图像也是如此。
Photo
类在其组件树中不包含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
),
),
),
);
}
}
关键信息
英雄包裹着
RadialExpansion
组件。当英雄飞行时,其大小会发生变化,并且因为它限制了其子组件的大小,所以
RadialExpansion
组件的大小也会随之改变。RadialExpansion
动画由两个重叠的剪辑创建。示例使用
MaterialRectCenterArcTween
定义补间插值。英雄动画的默认飞行路径使用英雄的角来插值补间。这种方法会影响径向变换期间英雄的纵横比,因此新的飞行路径使用MaterialRectCenterArcTween
来使用每个英雄的中心点插值补间。代码如下
dartstatic RectTween _createRectTween(Rect? begin, Rect? end) { return MaterialRectCenterArcTween(begin: begin, end: end); }
英雄的飞行路径仍然遵循弧线,但图像的纵横比保持不变。
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面最后更新于 2024-07-06。 查看源代码 或 报告问题。