Understanding and Animating Stuff using HeroWidget

Hetashree Bharadwaj
Mindful Engineering
8 min readJul 9, 2021
Photo by Esteban Lopez on Unsplash

Who is the Hero of Widget story?

If simply put, Hero Widget makes a widget of one screen to animate to another widget in next screen, afcourse it happens when you push to that screen.

Hero is a Widget which takes another widget as child, it can be an Image, Button or any other widget, When we move from one screen to another and both the screen has such matching Hero Widgets then It will tween between those two widgets, while animating size, position and other properties. when we navigate back to the earlier page, the animation will be played in reverse and the icon or image is animated back to the existing place.

Well, we will discuss the basics of hero animation and also discuss some level of customization in it.

Let’s see the basics first. 🙌🏻

Each Hero tag must be unique and match with both origination & the landing page.

// main pagechild: Hero(tag: ‘imageHero’,child: Image.network(‘https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',),// detail pagechild: Hero(tag: ‘imageHero’,child: Image.network(‘https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',),

The Hero animation is powerful built-in animation to hint user about the context of the action, It automatically animates a widget from one page to another by matching size and position. When you will navigate back to the previous page, Hero animates back to the original widget’s position. Each Hero tag needs to be unique and has to match in both original and landing pages.

Customizing Hero Animations

The hero widget allows us to customize aspects of the animation. Let’s explore a few possibilities.

Placeholders:
It allows us to proide widget which is shown after hero widget flies off the place, it used to be in the starting screen and in the destination screen before the widget arrives, there is an empty space at the destination. We can add a placeholder to this location. We can use the placeholderBuilder to construct the placeholder and return the widget we would like to have as the placeholder. Below we have used the CircularProgressIndicator() in the placeholder.

Hero(  tag: ‘imageHero’,  child: Image.network(  ‘https://encrypted-tbn0.gstatic.com     /images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',  ),// Using the placeholderBuilder, we can add a placeholder to this location.  placeholderBuilder: (context, heroSize, child) {    return Center(    child: Container(      height: 30,      width: 30,      child: CircularProgressIndicator(),    ),  );},)

When the Hero is flying

Hero Widget allows us to change the widget when the widget actually animates from one page to another page using the flightShuttleBuilder. The flightShuttleBuilder has 5 params and gives us the animation as well as the direction of the animation. For now, the unit Icon stays in both directions. We can have different configurations for each direction by using the direction parameter (for push & pop).

Hero(  tag: ‘imageHero’,  child: Image.network(    ‘https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',  ),flightShuttleBuilder: (context, animation, direction, fromContext, toContext) {  if (direction == HeroFlightDirection.push) {  return Image.network(  ‘https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',);} else {  return Icon(Icons.ac_unit);}},

Make it work with a back swipe gesture

By default, the hero animation will work with the back pressed but in iOS, it wasn't working with the back swipe. To solve that issue, simply set the transitionOnUserGesturesto true on both Hero widgets. This is by default false.

Hero(
tag: 'imageHero',
child: Center(
child: Image.network(
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',
),
),
transitionOnUserGestures: true,
),

Hero Animation with PageRouteBuilder Transition

The Hero widget is a great out-of-box animation to convey the navigation action of the widget animating into place from one Page to another page.

For example, we have a list of some items containing images, name & brief description, the user selects any entry and we can animate to the details page with image transition by moving & growing to the full size. The image thumbnail is the superhero and when we tap on it, it flies into the action by moving from the list page to the detail page and lands perfectly on the current location at the top of the detail page showing the full image. When the detail page is dismissed, the hero widget flies back to the original page, position & its size.

Hero Widget has the animation feature built-in; there is no need to write the custom code to handle the size & its animation between pages. The Hero widget is marked for hero animation. When we are navigating one page to another page (push & pop the pages) at that time entire screen’s content is replaced. This means during the animation transition the Hero widget is not shown in the original position in both old & the new route but it moves and resizes from one page to another page.

The Hero widget in flutter implements a style of animation commonly known as shared element transitions or shared element animation.

  1. Standard Hero Animation: It flies the one route to another new route & usually at a different place with different sizes.
  2. Radial Hero Animation: It changes between routes and shapes change from one to another. (Like circle to square).

I have made a demo using radial hero animation, this is how it looks. You can find the source code in Github link at the end of this article.

The PageRouteBuilder class is used to create custom route transitions. PageRouteBuilder provides an Animation object. This Animation can be used with Tween and Curve objects to customize the transition animation.

We need to define the PageRouteBuilder function to create the route’s content and define the transitionBuilder function to add transition animation.

Navigator.push(context, PageRouteBuilder(
transitionDuration: Duration(milliseconds: 1000),
pageBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return ListDetailsAnimation(
index: index,
image: images[index],
desc: 'The ${titles[index]} is awesome place. if you feel that you need to gop for the vacation.',
name: titles[index],
);
},
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return Align(
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
),
);

Note: For now you can’t animate a hero widget to a Dialog.

If we want to set the hero animation with alert dialog then we could try with a full-screen dialog. Because hero transition only enables the transition between two instances of pageRoute. So if we want to make use of the existing hero system, we should probably use a pageRoute.

Instead of using an AlertDialog, we could try a full-screen dialog.

Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
// title: Text('Dialog'),
),
body: Hero(
tag: "preview",
child: Container(
child: Center(
child: Image.network(
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSITaeKtjuK5B17HIwGhml6d13nSLOdojN9uw&usqp=CAU',
),
),
),
),
);
}
));

Custom Hero Animation

For example, we have a list of icons and we have a button to go to the next screen. if we click on the button it will jump to the next screen and We want the icon to start quickly and then slow down. The stars to the left side should faster slower than the right one.

Now let’s go through the properties of Hero Animation

_createRectTween — It defines how the destination hero’s bounds change as it flies from the starting route to the destination route.

As the hero widget 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.

static RectTween _createRectTween(Rect begin, Rect end, int index) {
switch (index) {
case 0:
// easeOutExpo
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.19, 1.0, 0.22, 1.0));
case 1:
// easeOutQuint
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.23, 1.0, 0.32, 1.0));
case 2:
// easeOutQuart
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.165, 0.84, 0.44, 1.0));
case 3:
// easeOutCubic
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.215, 0.61, 0.355, 1.0));
case 4:
// easeOutQuad
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.25, 0.46, 0.45, 0.94));
}
}

Now it’s time to implement CustomRectTween

class CustomRectTween extends RectTween {
Cubic _cubic;

CustomRectTween({Rect begin, Rect end, Cubic cubic})
: super(begin: begin, end: end) {
_cubic = cubic;
}

@override
Rect lerp(double t) {
double height = end.top - begin.top;
double width = end.left - begin.left;

double transformeY = _cubic.transform(t);

double animateX = begin.left + (t * width);
double animateY = begin.top + (transformeY * height);

return Rect.fromLTWH(animateX, animateY, 1, 1);
}
}

Now we need to return a Rect from lerp(double t), which specifies the position and bounds of the hero. The begin and end Rect parameters contain information about the widgets before and after the Hero animation. After that, we are calculating the difference in height and width before and after transition completion. but in this demo width will not change and we are moving vertically only. so value t will increase 0 to 1 vertically.

class CustomAnimation extends StatelessWidget {

final icons;
final double size;

const CustomAnimation({
this.icons,
this.size,
});

@override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return Hero(
tag: 'ratingStar$index',
createRectTween: (begin, end) {
return _createRectTween(begin, end, index);
},
child: Icon(
index < icons ? Icons.adjust : Icons.adjust,
color: Colors.blue,
size: size,
),
);
}),
),
);
}

static RectTween _createRectTween(Rect begin, Rect end, int index) {
switch (index) {
case 0:
// easeOutExpo
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.19, 1.0, 0.22, 1.0));
case 1:
// easeOutQuint
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.23, 1.0, 0.32, 1.0));
case 2:
// easeOutQuart
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.165, 0.84, 0.44, 1.0));
case 3:
// easeOutCubic
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.215, 0.61, 0.355, 1.0));
case 4:
// easeOutQuad
return CustomRectTween(
begin: begin, end: end, cubic: Cubic(0.25, 0.46, 0.45, 0.94));
}
}
}

class CustomRectTween extends RectTween {
Cubic _cubic;

CustomRectTween({Rect begin, Rect end, Cubic cubic})
: super(begin: begin, end: end) {
_cubic = cubic;
}

@override
Rect lerp(double t) {
double height = end.top - begin.top;
double width = end.left - begin.left;

double transformedY = _cubic.transform(t);

double animatedX = begin.left + (t * width);
double animatedY = begin.top + (transformedY * height);

return Rect.fromLTWH(animatedX, animatedY, 1, 1);
}
}

If we want to change the translation we need to change the ways of t values.

Thanks for Reading!! 🙂

I have created a demo of using Hero Widget. You can get started with it by downloading it using following link: Hero_animation_demo.

I really enjoyed a lot to build the “HERO” animation. It helps us to make animations with little code which can give us nice look and feel and the end user better understanding of relations between entities of the app.

--

--

Hetashree Bharadwaj
Mindful Engineering

Passionate App developer with expertise on Swift and Flutter, Novice writer as you might already have found out.