An application usually allows the users to navigate between pages or routes. In Flutter, we can define a list of PageRoute
s 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:
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:
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:
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:
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:
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:
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:
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 aWidget
during transition.HeroPlaceholderBuilder placeholderBuilder
: Placeholder widget left in place as thechild
once the flight takes off.bool transitionOnUserGestures
: Whether to perform the hero transition if a user gesture, such as a back swipe on iOS, triggersPageRoute
transition. Defaults tofalse
.Widget child
: The widget subtree that will fly during aNavigator
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
.