Flutter - Creating Hero Transition Examples

An application usually allows the users to navigate between pages or routes. In Flutter, we can define a list of PageRoutes and use Navigator to navigate between the routes. When moving to another route, the Widget of the old route is replaced by the new route's Widget. If there is a common visual on both routes, we can apply a transition as if the common visual flies from the position on the old route to the position of the one on the new route. That kind of animation is called hero transition.

Creating hero transition in Flutter is very easy as Flutter already provides a Widget called Hero. What you need to do is wrap the widgets considered as common visual on both routes as the child of Hero widgets. This tutorial has some usage examples of Hero widget, from the basic example to how to customize the effects of the transition.

Using Hero

Here's the constructor of Flutter's Hero widget.

  const Hero({
    Key key,
    @required this.tag,
    this.createRectTween,
    this.flightShuttleBuilder,
    this.placeholderBuilder,
    this.transitionOnUserGestures = false,
    @required this.child,
  })

There are two parameters marked as required. The first one is tag which is used as identifier. When moving to another page, Flutter will create the transition animation if each page contains a Hero with the same tag value. You are also required to pass child which is the Widget where the hero animation will be applied on.

Creating a Hero is very simple. You only need to call the constructor by passing at least tag and child arguments.

  Hero(
    tag: "HeroOne",
    child: Icon(
      Icons.image,
      size: 50.0,
    ),
  )

However, in order for the hero animation to be shown, the app must have another route that contains a Hero with the same tag. If you don't already know how to navigate between pages in Flutter, you can read a tutorial about navigation between screens in Flutter.

In the example below, we are going to create a simple application consisting of two pages. In order for the hero animation to work, both pages must have a hero with the same tag. The position and of both Hero widgets must be different so that we can see the animation. To make the animation more obvious, we need to use a custom PageRoute with the transition duration set to 2 seconds.

  import 'package:flutter/material.dart';
  
  void main() => runApp(MyApp());
  
  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        initialRoute: '/first',
        routes: {
          '/first': (context) => FirstScreen(),
          '/second': (context) => SecondScreen(),
        },
      );
    }
  }
  
  class FirstScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('First Screen'),
        ),
        body: Padding(
          padding: EdgeInsets.all(15),
          child: Column(
            children: [
              Hero(
                tag: "HeroOne",
                child: Icon(
                  Icons.image,
                  size: 50.0,
                ),
  
              ),
              ElevatedButton(
                child: Text('Go to second screen'),
                onPressed: () {
                  Navigator.push(context, CustomPageRoute(SecondScreen()));
                },
              ),
            ],
          ),
        ),
      );
    }
  }
  
  class SecondScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text("Second Screen"),
        ),
        body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Hero(
                  tag: "HeroOne",
                  child: Icon(
                    Icons.image,
                    size: 150.0,
                  ),
                ),
                ElevatedButton(
                  child: Text('Back to first screen!'),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                ),
              ],
            )
        ),
      );
    }
  }
  
  class CustomPageRoute<T> extends PageRoute<T> {
  
    final Widget child;
  
    CustomPageRoute(this.child);
  
    @override
    Color get barrierColor => Colors.black;
  
    @override
    String get barrierLabel => '';
  
    @override
    bool get maintainState => true;
  
    @override
    Duration get transitionDuration => Duration(seconds: 2);
  
    @override
    Widget buildPage(
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation
    ) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    }
  }

Output:

Flutter - Hero - Basic

 

Using HeroFlightShuttleBuilder

If you want to show another widget during the transition, pass a function as flightShuttleBuilder. The function must have 5 arguments (as shown in the following example) and return a Widget.

For example, we modify the Hero on the second page to use a HeroFlightShuttleBuilder function that returns another Icon.

  Hero(
    tag: "HeroOne",
    flightShuttleBuilder: (
        BuildContext flightContext,
        Animation<double> animation,
        HeroFlightDirection flightDirection,
        BuildContext fromHeroContext,
        BuildContext toHeroContext,
    ) {
      return Icon(
        Icons.view_comfy,
        size: 150.0,
      );
    },
    child: Icon(
      Icons.image,
      size: 150.0,
    ),
  )

Output:

Flutter - Hero - HeroFlightShuttleBuilder

 

As you can see from the output, the widget returned by flightShuttleBuilder will be displayed during transition. The position of the in-flight widget is always the same as the position of the widget during the transition. In other words, the new widget is animated to the position on the new route. Although we only define flightShuttleBuilder on the second page's Hero, it turns out that the custom widget is displayed during the transition from the first page to the second page, and also from the second page to the first page. To display a different widget while a transition from second page to first page occurs, pass another HeroFlightShuttleBuilder function for the Hero on the first page.

  Hero(
    tag: "HeroOne",
    flightShuttleBuilder: (
        BuildContext flightContext,
        Animation<double> animation,
        HeroFlightDirection flightDirection,
        BuildContext fromHeroContext,
        BuildContext toHeroContext,
    ) {
      return RefreshProgressIndicator();
    },
    child: Icon(
      Icons.image,
      size: 50.0,
    ),
  )

Output:

Flutter - Hero - HeroFlightShuttleBuilder

 

Using HeroPlaceholderBuilder

It's also possible to have another widget displayed on the actual position of the Hero's child during a transition. To do so, you need to pass a function as placeholderBuilder. The passed function is responsible to return the placeholder widget.

  Hero(
    tag: "HeroOne",
    placeholderBuilder: (
        BuildContext context,
        Size heroSize,
        Widget child,
    ) {
      return SizedBox(
        height: 150.0,
        width: 150.0,
        child: CircularProgressIndicator(),
      );
    },
    child: Icon(
      Icons.image,
      size: 150.0,
    ),
  )

Output:

Flutter - Hero - HeroPlaceholderBuilder

 

If the placeholderBuilder is only defined on the second page's Hero, the widget returned by HeroPlaceholderBuilder function is only displayed on the second page Hero's child. That means it's also necessary to pass another placeholderBuilder for the Hero on the first page if you want to display a placeholder for it.

  Hero(
    tag: "HeroOne",
    placeholderBuilder: (
        BuildContext context,
        Size heroSize,
        Widget child,
    ) {
      return SizedBox(
        height: 50.0,
        width: 50.0,
        child: RefreshProgressIndicator(),
      );
    },
    child: Icon(
      Icons.image,
      size: 50.0,
    ),
  )

Output:

Flutter - Hero - HeroPlaceholderBuilder

 

Using CreateRectTween

How to control the animation of a hero from the starting route to the destination route? Flutter also provides another named parameter createRectTween where you can pass a function that returns Tween<Rect>. There are some built-in classes that can be used to change the animation, such as MaterialRectArcTween, MaterialRectCenterArcTween.

  Hero(
    tag: "HeroOne",
    createRectTween: (begin, end) {
      return MaterialRectArcTween(begin: begin, end: end);
    },
    child: Icon(
      Icons.image,
      size: 150.0,
    ),
  )

Output:

Flutter - Hero - MaterialRectArcTween

 

Another way is by creating a class that extends RectTween (RectTween extends Tween<Rect>) and defining your own animation by overriding lerp method.

  class CustomRectTween extends RectTween {
  
    final Rect begin;
    final Rect end;
  
    CustomRectTween({this.begin, this.end}) : super(begin: begin, end: end);
  
    @override
    Rect lerp(double t) {
      double x = Curves.easeOutCirc.transform(t);
  
      return Rect.fromLTRB  (
        lerpDouble(begin.left, end.left, t),
        lerpDouble(begin.top, end.top, t),
        lerpDouble(begin.right, end.right, t) * (1 + x),
        lerpDouble(begin.bottom, end.bottom, t) * (1 + x),
      );
    }
  
    double lerpDouble(num begin, num end, double t) {
      return begin + (end - begin) * t;
    }
  }

Then, create an instance of the class and use it as the return value of CreateRectTween function.

  Hero(
    tag: "HeroOne",
    createRectTween: (begin, end) {
      return CustomRectTween(begin: begin, end: end);
    },
    child: Icon(
      Icons.image,
      size: 150.0,
    ),
  )

Output:

Flutter - Hero - Custom RectTween

 

Handling Gesture Transition

By default, if a PageRoute transition is triggered by a gesture such as back swipe on iOS, hero transition will not run. To change that behavior, pass transitionOnUserGestures with true as the value.

  Hero(
    tag: "HeroOne",
    child: Icon(
      Icons.image,
      size: 50.0,
    ),
    transitionOnUserGestures: true,
  )

 

Hero Parameters

Below are the named parameters you can pass to the constructor.

  • Key key: The widget's key.
  • Object tag: The identifier of the hero.
  • createRectTween: Defines how the destination hero's bounds change.
  • HeroFlightShuttleBuilder flightShuttleBuilder: Can be used to supply a Widget during transition.
  • HeroPlaceholderBuilder placeholderBuilder: Placeholder widget left in place as the child once the flight takes off.
  • bool transitionOnUserGestures: Whether to perform the hero transition if a user gesture, such as a back swipe on iOS, triggers PageRoute transition. Defaults to false.
  • Widget child: The widget subtree that will fly during a Navigator transition.

*: required

 

That's all about how to create a hero transition in Flutter. Overall, you can create a hero transition by having two Hero widgets with the same tag on each route, with the common visual is set as the child of each Hero widget. You can also customize the animation by creating HeroFlightShuttleBuilder, HeroPlaceholderBuilder, and CreateRectTween.