Skip to main content

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 动画代码具有以下结构:

  1. 定义一个起始 Hero 部件,称为_源 hero_。hero 指定其图形表示(通常是图像)和一个标识标签,并且位于源路由定义的当前显示部件树中。
  2. 定义一个结束 Hero 部件,称为_目标 hero_。此 hero 也指定其图形表示,以及与源 hero 相同的标签。至关重要的是,两个 hero 部件都使用相同的标签创建,通常是一个表示底层数据的对象。为了获得最佳效果,hero 应该具有几乎相同的部件树。
  3. 创建一个包含目标 hero 的路由。目标路由定义动画结束时存在的部件树。
  4. 通过在 Navigator 的堆栈上推送目标路由来触发动画。Navigator 的 push 和 pop 操作会为源路由和目标路由中具有匹配标签的每对 hero 触发 hero 动画。

Flutter 计算补间,该补间将 Hero 的边界从起点动画到终点(内插大小和位置),并在叠加层中执行动画。

下一部分将更详细地描述 Flutter 的过程。

幕后

#

以下是 Flutter 如何在一个路由到另一个路由之间进行过渡的描述。

过渡前,源 hero 出现在源路由中

过渡前,源 hero 在源路由的部件树中等待。目标路由尚不存在,叠加层为空。


过渡开始

将路由推送到 Navigator 会触发动画。在 t=0.0 时,Flutter 执行以下操作:

  • 使用 Material 运动规范中描述的曲线运动,计算目标 hero 的路径(屏幕外)。Flutter 现在知道 hero 最终会在哪里。

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

  • 将源 hero 移到屏幕外。


hero 在叠加层中飞到其最终位置和大小

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


过渡完成后,hero 将从叠加层移动到目标路由

飞行完成后:

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

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

  • 源 hero 被恢复到其路由。


弹出路由会执行相同的过程,将 hero 动画回其在源路由中的大小和位置。

重要的类

#

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

Hero
从源路由飞到目标路由的部件。为源路由和目标路由定义一个 Hero,并为每个 Hero 分配相同的标签。Flutter 会为具有匹配标签的 hero 对设置动画。
InkWell
指定点击 hero 时发生的情况。InkWellonTap() 方法构建新路由并将其推送到 Navigator 的堆栈。
Navigator
Navigator 管理路由的堆栈。在 Navigator 的堆栈上推送路由或弹出路由会触发动画。
Route
指定屏幕或页面。除最基本的应用外,大多数应用都有多个路由。

标准 hero 动画

#

:::次要内容 关键点

  • 使用 MaterialPageRouteCupertinoPageRoute 指定路由,或使用 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 构建以下部件树:

PhotoHero 类部件树

代码如下:

dart
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,并设置过渡。

代码如下:

dart
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 的堆栈会触发动画。
  • ContainerPhotoHero 定位在目标路由的左上角,位于 AppBar 下方。
  • 目标 PhotoHeroonTap() 方法弹出 Navigator 的堆栈,触发将 Hero 移动回原始路由的动画。
  • 使用 timeDilation 属性在调试时减慢过渡速度。

径向 hero 动画

#

:::次要内容 关键点

  • 径向变换 将圆形形状动画成方形形状。
  • 径向 hero 动画在将 hero 从源路由移动到目标路由的同时执行径向变换。
  • MaterialRectCenter­Arc­Tween 定义补间动画。
  • 使用 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)。在变换过程中,ClipOvalminRadius 缩放至 maxRadius,而 ClipRect 保持恒定大小。在过渡结束时,圆形和矩形剪辑的交集会产生一个与 hero 部件大小相同的矩形。换句话说,在过渡结束时,图像不再被剪辑。

创建新的 Flutter 示例 并使用 radial_hero_animation GitHub 目录中的文件对其进行更新。

要运行示例:

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

Photo 类

#

Photo 类构建保存图像的部件树:

dart
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 部件是演示的核心,它构建在过渡期间剪辑图像的部件树。剪辑形状是圆形剪辑(在飞行过程中会增长)与矩形剪辑(在整个过程中保持恒定大小)的交集的结果。

为此,它构建以下部件树:

RadialExpansion 部件树

代码如下:

dart
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 的中心点对补间进行插值。

    代码如下:

    dart
    static 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]: https://api.flutter.dev/flutter/animation/Tween-class.html 矩形补间