Welcome back to "Hero Animation Unmasked", our little adventure to recode the flutter Shared Element Transition called "Hero Animation".
In the previous part: Hero Animation Unmasked - Part 1, we have ?
1. understood the general mechanism of the Hero Animation
2. created our own ++code>UnmaskedHero++/code> widget and its ++code>UnmaskedHeroController
++/code>3. hooked ourselves to the ++code>didPush++/code> Navigation method to react to navigation
4. browsed the Elements Tree to look for interesting Widget instances
5. displayed our ++code>UnmaskedHero++/code> into the screen overlay
Now that we've managed to find our Hero-wrapped widgets and display them onto the screen?
In order to so, we'll implement the following steps:
1. Display our ++code>UnmaskedHeroes++/code> at their initial position on screen
2. Animate them from their initial to their final positions
3. Hide the source & destination widgets during the flight
Eventually, we'll buy our ++code>UnmaskedHero++/code> a return ticket by making sure they can fly back when we navigate back.
Commit: 7f37e14b1d45335f9044fba6187d83ec3ccb0350
In order to make our Hero fly, we first need to compute the locations on the screen from and to which they should be flying.
To do so, in the ++code>UnmaskedHeroController++/code> class, we create a ++code>_locateHero++/code> method that, given the ++code>UnmaskedHeroState++/code> and a ++code>BuildContext++/code>, will return a ++code>Rect++/code> object holding the actual onscreen position of the associated element.
Here, we access the ++code>RenderObject++/code> of the hero by calling ++code>hero.context.findRenderObject()++/code> and the ++code>RenderObject++/code> of the global context (which we refer to as its ancestor): ++code>context.findRenderObject()++/code>.
Then we compute the transformation matrix that describes the geometric transformation between 2 ++code>RenderObject++/code> using the ++code>getTransformTo++/code> method.
Finally, we apply this transformation using ++code>MatrixUtils.transformRect++/code> to return our hero's location in the frame of reference of the given context.
In the ++code>didPush++/code> method, let's now call the ++code>_locateHero++/code> method for both source and destination to compute the from and to position:
Commit: ef3c500cd5271cbd30f785d541f2e78108a42040
Now that our Hero's initial and final positions are computed. We need to make them initially appear at the "from" position. For that, let's rename the ++code>displayFlyingHero++/code> method with a more explanatory name: ++code>_displayFlyingHeroAtPosition++/code> and pass it an additional ++code>Rect++/code> parameter that will hold the position at which we want to display the Hero:
Next, we replace the ++code>Container++/code> returned by the ++code>builder++/code> of the ++code>OverlayEntry++/code> with a ++code>Positioned++/code> widget and pass it the given position:
The overlayed Hero now appears at the same position as the source Hero widget.
Commit: daa20dd0837a87659885411c8de0c591643d7bd8
Eventually, here comes the actual "Flying" part we've been waiting for 😅!
To do so, we'll create a widget responsible for the animation and display it on the overlay:
First, in the ++code>unmasked_hero++/code> folder, create a ++code>FlyingUnmaskedHero++/code> widget:
This widget is a simple ++code>StatefulWidget++/code> responsible for handling the animation between the initial and final position of our hero that we pass as parameters.
For the sake of simplicity, we use an ++code>AnimatedPositioned++/code> widget to handle the animation between the 2 positions. The actual Hero widget uses the lower-level API ++code>RectTween++/code>. This induces a couple of notable things here:
1. We hardcode the duration of the animation to ++code>200++/code>. In real life, we would want to ensure the animation duration matches the animation of the navigation between the 2 pages.
2. We use a ++code>Timer++/code> of ++code>0 milliseconds++/code> in the ++code>initState++/code> method to ensure the widget is built once with the ++code>flying++/code> state set to false, which initialize the position to ++code>toPosition++/code> before being animated to toward the ++code>fromPosition++/code>.
Next, we rename the ++code>_displayFlyingHeroAtPosition++/code> to ++code>_startFlying++/code> and pass it both the from and to positions:
Finally, we can replace the widget built by the ++code>OverlayEntry++/code> to use our ++code>FlyingUnmaskedHero++/code>:
… is it a bird 🦅 ? is it a plane 🛩 ? 🤩
Our hero animates nicely between the initial and final positions 💪.
We're almost there. Now we just need to make sure that the original widgets and the one flying onto the overlay are not displayed simultaneously to produce the illusion that they are the same widget.
In order to produce this illusion, there are 2 things left to do:
1. Remove the widget from the overlay when the animation ends
2. Hide our ++code>UnmaskedHero++/code> widget's child during the animation
Commit: dc3e4b6b222d6c0ad004b958464ae5e14466b714
Add a ++code>onFlyingEnded++/code> callback property to the ++code>FlyingUnmaskedHero++/code> widget to be able to listen to this event:
and pass it a function that removes the ++code>overlayEntry++/code>:
Commit: 7836d20d8fa1ddba160aab782aaf689c92899b3e
We first modify our ++code>UnmaskedHeroState++/code> to add the capibility to hide or show its child:
We now modify the ++code>startFlying++/code> method to pass both ++code>sourceHero++/code> and ++code>destinationHero++/code>.
And finally, we add the logic:
1. Hide the widgets before the animation starts
2. Show the widgets once it ended
Well… this starts to look very much like a proper Shared Element Transition doesn’t it?
Alright, I heard you… it does not quite because the animation is not performed backward when we navigate back to the source page 😏.
Let’s take this last step together:
Commit: add9e0e1fcbf282f1ce817d8eb86f34a0c64311c
In the ++code>UnmaskedHeroController++/code>, proceed to an “extract method” type of refactoring to move the entire content of the didPush method inside a ++code>_flyFromTo++/code> method:
and eventually use it inside the overriden ++code>didPop++/code> method of the controller:
And there you go…
Our ++code>UnmaskedHerosmoothly++/code> flies back and forth from one page to the other, just as the original Hero widget.
Our great adventure of recoding the Flutter Hero animation comes to an end. Let’s take a look back at our journey:
In the 1st part of this article: Flutter Hero Animation Unmasked - Part 1/2, we have:
1. understood the general mechanism of the Hero Animation
2. created our own UnmaskedHero widget and its UnmaskedHeroController
3. hooked ourselves to the didPush Navigation method to react to navigation
4. browsed the Elements Tree to look for interesting Widget instances
5. displayed our UnmaskedHero into the screen overlay
in this 2nd part, we have:
6. computed the initial and final position on screen of our widget
7. animated them onto the overlay from initial to final position
8. produced the illusion of the widget “moving” by hiding the original ones during the animation and removing the overlayed one afterward
9. added support for the backward animation when navigating back from the destination page
I really hope that you enjoyed this journey to Unmasked the Hero Animation as much as I did and that you learned some things.
The Hero widget has no secrets for you now 😉.
If you want to dive even deeper in your understanding of this animation, check out the source code of the actual Hero widget.
If you have any questions, if you find any mistakes, inaccuracies, or if you just want to have a chat about Flutter, I’d be very happy to. You can find me on Twitter @Guitoof or on the Flutter Community Slack where I also go by the name Guitoof.