(译)Hero 动画

原文链接

你会学到什么

  • hero指的是在路由之间飞行的widget。
  • 使用Flutter的Herowidget创建一个hero动画。
  • hero从一个路由飞到另一个路由。
  • 动画将hero的形状从圆形变为矩形,同时将其从一个路由飞到另一个路由。
  • Flutter中的Herowidget 实现了一种动画风格,通常称为共享元素过渡或共享元素动画。

你可能已经多次看过Hero动画了。例如,路由显示表示待售物品的缩略图列表。选择条目会将其飞到一个新路由,其中包含更多详细信息和“购买”按钮。将图像从一个路由飞到另一个路由在Flutter中称为hero动画,尽管相同的运动有时被称为共享元素过渡。

你可能想要观看介绍Hero widget的这一分钟视频:

本指南演示了如何构建标准Hero动画,以及在飞行过程中将图像从圆形变换为方形的Hero动画。

示例:本指南提供以下链接中每种hero动画样式的示例。

Flutter新手?本页假定您知道如何使用Flutter的widget创建布局。有关更多信息,请参阅在Flutter中构建布局

术语:路由描述Flutter应用程序中的页面或屏幕。

你可以使用Hero widget在Flutter中创建此动画。当hero以动画形式从源路由到目标路由时,目标路由会淡入视图。通常,hero是UI的一小部分,如图像,两条路由都有共同之处。从用户的角度来看,hero在路由之间“飞翔”。本指南介绍如何创建以下hero动画:

标准hero动画

标准hero动画将hero从一个路由飞到一个新路由,通常降落在不同的位置并且具有不同的大小。

以下视频(以低速录制)展示了一个典型示例。在路由中心点击脚蹼将它们飞到新的蓝色路由的左上角,尺寸较小。在蓝色路由中点击脚蹼(或使用设备的回到前一个路由的手势)将脚蹼飞回原始路由。

径向hero动画

在径向hero动画中,当hero在路由之间飞行时,其形状呈现为从圆形变为矩形。

以下视频(以低速录制)显示了径向hero动画的示例。开始时,路由底部会出现一排三个圆形图像。点击任何圆形图像会将图像飞到一个以正方形形状显示的新路由上。点击方形图像会使hero回到原始路由,显示为圆形。

在进入到特定于标准或径向hero动画的部分之前,阅读hero动画的基本结构以学习如何构建hero动画代码,并在表象背后了解Flutter如何执行hero动画的。

hero动画的基本结构

重点是什么?

  • 在不同的路由中使用两个widget小部件,但使用匹配的标签来实现动画。
  • Navigator管理着包含应用程序路由的堆栈。
  • 从导航器的堆栈中push路由或pop路由会触发动画。
  • Flutter框架计算一个矩形补间,用于定义hero从源路由到目的地路由的边界。在飞行过程中,hero被移动到应用程序的覆盖层,以便它出现在两个路由的顶部。

术语:如果补间或补间的概念对你来说是新的,请参阅Flutter中的动画教程

使用两个Hero widget实现hero动画:一个描述源路由中的widget,另一个描述目标路由中的小widget。从用户的角度来看,hero似乎是共享的,只有程序员才需要了解这个实现细节。

关于对话框的注意事项:hero从一个PageRoute路由飞到另一个。对话框(例如,用showDialog()显示 ),使用的是PopupRoutes,它们不是PageRoute。至少目前,你无法在Dialog上实现hero动画。有关进一步的开发(以及可能的解决方法),请查看此问题

hero动画代码具有以下结构:

  1. 定义一个起始hero小部件,称为源herohero指定其图形表示(通常是图像)和识别标记,并且在源路由定义的当前显示的widget树中。
  2. 定义一个结束hero小部件,称为目标hero。此hero还指定其图形表示,以及与源hero相同的标记。两个hero widget都必须使用相同的标记创建,通常是表示基础数据的对象。为了获得最佳效果,hero应该拥有几乎相同的widget树。
  3. 创建包含目标hero的路由。目标路由定义动画结尾处存在的widget树。
  4. 通过在导航器堆栈上按下目标路由来触发动画。导航器的push和pop操作会触发每对herohero动画,并在源和目标路由中使用匹配的标记。
  5. Flutter计算从起点到终点设置Hero边界的动画的补间(插值大小和位置),并在叠加层中执行动画。

下一节将更详细地介绍Flutter的处理过程。

表象背后

以下描述了Flutter如何执行从一个路由到另一个路由的过渡。

在转换之前,源hero在源路由的widget树中等待。目标路由尚不存在,并且叠加层为空。

将路由push到导航器会触发动画。在t = 0.0时,Flutter执行以下操作:

在画面外,使用Material运动规范中描述的曲面运动计算目标hero的路径。Flutter现在知道hero最终的位置。

将目标hero放置在叠加层中,与源hero的位置和大小相同。向叠加层添加hero会更改其Z轴上顺序,以使其显示在所有路由的顶部。

将源hero移动到屏幕外。

hero飞行时,它的矩形边界使用在Hero的createRectTween属性中指定的Tween <Rect>进行动画处理。默认情况下,Flutter使用MaterialRectArcTween的实例,该实例沿着弯曲路径设置矩形的对角线。(有关使用不同Tween动画的示例,请参阅径向hero动画。)

飞行完成时:

Flutter将hero widget从叠加层移动到目标路由。叠加层现在为空。

目标hero出现在目的地路由的最终位置。

hero将恢复到其路由。

pop路由执行相同的过程,将hero动画回原点和源路由中的位置。

必要的类

本指南中的示例使用以下类来实现hero动画:

hero

从源到目标路由的widget。为源路由定义一个Hero,为目标路由定义另一个Hero,并为每个分配相同的标记。flutter使用匹配的标签做出hero对的动画。

Inkwell

指定点击hero时会发生什么。InkWell的onTap()方法构建新路由并将其push到Navigator的堆栈。

Navigator

Navigator管理一堆路由。从导航器的堆栈中push路由或pop路由会触发动画。

Route

指定屏幕或页面。除最基本的应用程序之外,大多数应用程序都有个路由。

标准Hero动画

重点是什么?

  • 使用MaterialPageRouteCupertinoPageRoute指定路由,或使用PageRouteBuilder构建自定义路由。本节中的示例使用MaterialPageRoute
  • 通过将目标图像包装在SizedBox中,在过渡结束时更改图像的大小。
  • 通过将目标图像放置在widget中来更改图像的位置。这些示例使用Container。

标准hero动画代码

以下每个示例都演示了将图像从一个路由飞到另一个路由。本指南介绍了第一个示例。

hero_animation

hero代码封装在自定义PhotoHero widget 中。沿着曲线路径动画hero的动作,如Material运动规范中所述。

basic_hero_animation

直接使用herowidget。本指南中未介绍此基本示例,供你参考。

这是怎么回事?

使用Flutter的hero widget可以轻松实现将图像从一个路由传输到另一个路由。使用MaterialPageRoute指定新路由时,图像沿着弯曲路径飞行,如“Material设计”运动规范所述。

创建一个新的Flutter示例并使用Gi​​tHub目录中的文件进行更新。

运行示例:

  • 点击主路由的照片,将图像拖动到新的路由,在不同的位置,以不同比例显示相同的照片。
  • 通过点击图像或使用设备的返回上一个路由手势返回上一个路由。
  • 你可以使用timeDilation属性进一步减慢过渡。

PhotoHero类

自定义PhotoHero类在点击时维护hero及其大小,图像和行为。PhotoHero构建了以下widget树:

这里是代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PhotoHero extends StatelessWidget {
const PhotoHero({ Key key, this.photo, this.onTap, this.width }) : super(key: key);

final String photo;
final VoidCallback onTap;
final double width;

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作为app的home属性提供时,MaterialApp会隐式push起始路由。
  • InkWell包装图像,使得向源和目标hero添加点击手势变得微不足道。
  • 使用透明颜色定义“Material”widget可使图像在飞往目标时“弹出”背景。
  • SizedBox指定动画开始和结束时hero的大小。
  • 将Image的fit属性设置为BoxFit.contain,可确保在过渡期间图像尽可能大,而不会更改其宽高比。

HeroAnimation类

HeroAnimation类创建源和目标 PhotoHeroes,并设置过渡。

这是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class HeroAnimation extends StatelessWidget {
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: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
// The blue background emphasizes that it's a new route.
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pop();
},
),
),
);
}
));
},
),
),
);
}
}

关键信息:

  • 当用户点击包含源hero的InkWell时,代码使用MaterialPageRoute创建目标路由。将目标路由push到导航器的堆栈会触发动画。
  • Container将PhotoHero定位在AppBar下方的目的路由的左上角。
  • 目标PhotoHero的onTap()方法 pop导航器的堆栈,触发将Hero飞回原始路由的动画。
  • 使用timeDilation属性可以在调试时减慢转换速度。

径向hero动画

重点是什么?

  • 径向过渡将圆形动画变为方形。
  • 径向hero动画在将hero从源路由飞到目标路由时执行径向过渡。
  • MaterialRectCenterArcTween 定义 补间动画。
  • 使用PageRouteBuilder构建目标路由。

将路由从一个路由飞向另一个路由,因为它从圆形过渡为矩形,这是一种光滑的效果,你可以使用herowidget来实现。为此,代码动画两个剪辑形状的交集:圆形和方形。在整个动画中,圆形剪辑(和图像)从minRadius缩放到maxRadius,而方形剪辑保持不变的大小。同时,图像从其在源路由中的位置飞到其在目的路由中的位置。有关此过渡的可视示例,请参阅材质运动规范中的径向过渡

此动画可能看起来很复杂(并且确实如此),但你可以根据需要自定义提供的示例。繁重的工作已经为你完成了。

径向hero动画代码

以下每个示例都演示了径向hero动画。本指南介绍了第一个示例。

radial_hero_animation

材质运动规范中描述的径向hero动画。

basic_radial_hero_animation

径向hero动画的最简单示例。目的路由没有Scaffold, Card, ColumnText。本指南中未介绍此基本示例,供你参考。

radial_hero_animation_animate_rectclip

通过动画化矩形剪辑的大小来扩展radial_hero_animaton。本指南中未介绍此高级示例,供你参考。

专业提示:径向hero动画涉及将圆形与正方形相交。即使使用timeDilation减慢动画速度,也很难看到这一点,因此你可以考虑在开发过程中启用Flutter的可视化调试模式。

这是怎么回事?

下图显示了动画开头(t = 0.0)和结束(t = 1.0)的剪裁图像。

蓝色渐变(表示图像)表示剪辑形状相交的位置。在转换开始时,交集的结果是一个圆形剪辑(ClipOval)。在转换期间,ClipOval从minRadius缩放到maxRadius,而ClipRect保持恒定大小。在过渡结束时,圆形和矩形剪辑的交叉产生一个与herowidget大小相同的矩形。换句话说,在转换结束时,图像不再被剪裁。

创建一个新的Flutter示例并使用Gi​​tHub目录中的文件进行更新。

运行示例:

  • 点击三个圆形缩略图中的一个,将图像设置为一个较大的正方形,该正方形位于新路由的中间,遮挡了原始路由。
  • 通过点击图像或使用设备的返回上一个路由手势返回上一个路由。
  • 你可以使用timeDilation属性进一步减慢过渡。

Photo类

Photo类构建保存图像的widget树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Photo extends StatelessWidget {
Photo({ Key key, this.photo, this.color, this.onTap }) : super(key: key);

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”widget具有略微不透明的颜色,因此图像的透明部分将使用颜色进行渲染。这确保了即使对于具有透明度的图像也容易看到圆到方的过渡。
  • Photo类在其widget树中不包含Hero。为了使动画起作用,hero包装了RadialExpansion widget。

RadialExpansion类

RadialExpansion widget 是演示的核心,它构建了在过渡期间剪切图像的widget树。剪裁的形状来自圆形剪辑(在飞行期间生长)与矩形剪辑(整个过程中保持恒定大小)的交叉。

为此,它构建了以下widget树:

这里是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class RadialExpansion extends StatelessWidget {
RadialExpansion({
Key key,
this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2),
super(key: key);

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 widget。
  • hero飞行时,它的大小会发生变化,并且由于它限制了孩子的大小,因此RadialExpansion widget会更改大小以匹配。
  • RadialExpansion动画由两个重叠的剪辑创建。
  • 该示例使用MaterialRectCenterArcTween定义补间插值。hero动画的默认飞行路径使用hero的角插入补间。此方法会影响径向变换期间hero的纵横比,因此新的飞行路由使用MaterialRectCenterArcTween使用每个hero的中心点插补补间。

这是代码:

1
2
3
static RectTween _createRectTween(Rect begin, Rect end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}

hero的飞行路径仍遵循弧线,但图像的纵横比保持不变。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号