Flutter Hero Animations

Flutter Hero Animations



Information drawn from

What you’ll learn

You’ve probably seen hero animations many times. For example, a screen displays a list of thumbnails representing items for sale. Selecting an item flies it to a new screen, containing more details and a “Buy” button. Flying an image from one screen to another is called a hero animation in Flutter, though the same motion is sometimes referred to as a shared element transition.

You might want to watch this one-minute video introducing the Hero widget

This guide demonstrates how to build standard hero animations, and hero animations that transform the image from a circular shape to a square shape during flight.

Examples: This guide provides examples of each hero animation style at the following links:

Terminology: A Route describes a page or screen in a Flutter app.

You can create this animation in Flutter with Hero widgets. As the hero animates from the source to the destination route, the destination route (minus the hero) fades into view. Typically, heroes are small parts of the UI, like images, that both routes have in common. From the user’s perspective the hero “flies” between the routes. This guide shows how to create the following hero animations:

Standard hero animations

A standard hero animation flies the hero from one route to a new route, usually landing at a different location and with a different size.

The following video (recorded at slow speed) shows a typical example. Tapping the flippers in the center of the route flies them to the upper left corner of a new, blue route, at a smaller size. Tapping the flippers in the blue route (or using the device’s back-to-previous-route gesture) flies the flippers back to the original route.

Radial hero animations

In radial hero animation, as the hero flies between routes its shape appears to change from circular to rectangular.

The following video (recorded at slow speed), shows an example of a radial hero animation. At the start, a row of three circular images appears at the bottom of the route. Tapping any of the circular images flies that image to a new route that displays it with a square shape. Tapping the square image flies the hero back to the original route, displayed with a circular shape.

Before moving to the sections specific to standard or radial hero animations, read basic structure of a hero animation to learn how to structure hero animation code, and behind the scenes to understand how Flutter performs a hero animation.

Basic structure of a hero animation

What’s the point?

Terminology: If the concept of tweens or tweening is new to you, see the Animations in Flutter tutorial.

Hero animations are implemented using two Hero widgets: one describing the widget in the source route, and another describing the widget in the destination route. From the user’s point of view, the hero appears to be shared, and only the programmer needs to understand this implementation detail.

Note about dialogs: Heroes fly from one PageRoute to another. Dialogs (displayed with showDialog(), for example), use PopupRoutes, which are not PageRoutes. At least for now, you can’t animate a hero to a Dialog. For further developments (and a possible workaround), watch this issue.

Hero animation code has the following structure:

Flutter calculates the tween that animates the Hero’s bounds from the starting point to the endpoint (interpolating size and position), and performs the animation in an overlay.

The next section describes Flutter’s process in greater detail.

Behind the scenes

The following describes how Flutter performs the transition from one route to another.

hero-transition-0.png Before the transition the source hero appears in the source route

Before transition, the source hero waits in the source route’s widget tree. The destination route does not yet exist, and the overlay is empty.

hero-transition-1.png The transition begins

Pushing a route to the Navigator triggers the animation. At t=0.0, Flutter does the following:

hero-transition-2.png The hero flies in the overlay to its final position and size

As the hero flies, its rectangular bounds are animated using Tween<Rect> , specified in Hero’s createRectTween property. By default, Flutter uses an instance of MaterialRectArcTween, which animates the rectangle’s opposing corners along a curved path. (See Radial hero animations for an example that uses a different Tween animation.)

hero-transition-3.png When the transition is complete, the hero is moved from the overlay to the destination route

When the flight completes:

Popping the route performs the same process, animating the hero back to its size and location in the source route.

Essential classes

The examples in this guide use the following classes to implement hero animations:

Hero The widget that flies from the source to the destination route. Define one Hero for the source route and another for the destination route, and assign each the same tag. Flutter animates pairs of heroes with matching tags.

Inkwell Specifies what happens when tapping the hero. The InkWell’s onTap() method builds the new route and pushes it to the Navigator’s stack.

Navigator The Navigator manages a stack of routes. Pushing a route on or popping a route from the Navigator’s stack triggers the animation.

Route Specifies a screen or page. Most apps, beyond the most basic, have multiple routes.

Standard hero animations

What’s the point?

Standard hero animation code

Each of the following examples demonstrates flying an image from one route to another. This guide describes the first example.

What’s going on? Flying an image from one route to another is easy to implement using Flutter’s hero widget. When using MaterialPageRoute to specify the new route, the image flies along a curved path, as described by the Material Design motion spec.

Create a new Flutter example and update it using the files from the hero_animation.

To run the example:

standard-hero-animation.gif

####  PhotoHero class

The custom PhotoHero class maintains the hero, and its size, image, and behavior when tapped. The PhotoHero builds the following widget tree:

photohero-class.png PhotoHero class widget tree

Here’s the code:

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,
            ),
          ),
        ),
      ),
    );
  }
}

Key information:

HeroAnimation class

The HeroAnimation class creates the source and destination PhotoHeroes, and sets up the transition.

Here’s the code:

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();
                      },
                    ),
                  ),
                );
              }
            ));
          },
        ),
      ),
    );
  }
}

Key information:

Radial hero animations

What’s the point?

Flying a hero from one route to another as it transforms from a circular shape to a rectangular shape is a slick effect that you can implement using Hero widgets. To accomplish this, the code animates the intersection of two clip shapes: a circle and a square. Throughout the animation, the circle clip (and the image) scales from minRadius to maxRadius, while the square clip maintains constant size. At the same time, the image flies from its position in the source route to its position in the destination route. For visual examples of this transition, see Radial transformation in the Material motion spec.

This animation might seem complex (and it is), but you can customize the provided example to your needs. The heavy lifting is done for you.

Radial hero animation code

Each of the following examples demonstrates a radial hero animation. This guide describes the first example.

radial_hero_animation A radial hero animation as described in the Material motion spec. basic_radial_hero_animation The simplest example of a radial hero animation. The destination route has no Scaffold, Card, Column, or Text. This basic example, provided for your reference, isn’t described in this guide. radial_hero_animation_animate_rectclip Extends radial_hero_animaton by also animating the size of the rectangular clip. This more advanced example, provided for your reference, isn’t described in this guide.

Pro tip: The radial hero animation involves intersecting a round shape with a square shape. This can be hard to see, even when slowing the animation with timeDilation, so you might consider enabling the debugPaintSizeEnabled flag during development.

What’s going on? The following diagram shows the clipped image at the beginning (t = 0.0), and the end (t = 1.0) of the animation.

radial-hero-animation.png Radial transformation from beginning to end

The blue gradient (representing the image), indicates where the clip shapes intersect. At the beginning of the transition, the result of the intersection is a circular clip (ClipOval). During the transformation, the ClipOval scales from minRadius to maxRadius while the ClipRect maintains a constant size. At the end of the transition the intersection of the circular and rectangular clips yield a rectangle that’s the same size as the hero widget. In other words, at the end of the transition the image is no longer clipped.

Create a new Flutter example and update it using the files from the radial_hero_animation GitHub directory.

To run the example:

radial-hero-animation.gif

Photo class

The Photo class builds the widget tree that holds the image:

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,
          )
      ),
    );
  }
}

Key information:

####  RadialExpansion class

The RadialExpansion widget, the core of the demo, builds the widget tree that clips the image during the transition. The clipped shape results from the intersection of a circular clip (that grows during flight), with a rectangular clip (that remains a constant size throughout).

To do this, it builds the following widget tree:

radial-expansion-class.png RadialExpansion widget tree

Here’s the code:

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
          ),
        ),
      ),
    );
  }
}

Key information:

Here’s the code:

static RectTween _createRectTween(Rect begin, Rect end) {
}

The hero’s flight path still follows an arc, but the image’s aspect ratio remains constant.

------------------------------------------------------------------------

Last update on 01 Feb 2022

---