Flutter

Flutter Hero Animation Unmasked - Part 2/2

Welcome back to "Hero Animation Unmasked", our little adventure to recode the flutter Shared Element Transition called "Hero Animation".

Previously in Hero Animation Unmasked

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?

Display Hero on Overlay

Let's make them Fly!

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

1. ? Ready for Take-off?

Ready for Take-off

Compute Hero from/to locations on screen

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.

++pre>++code> /// lib/packages/unmasked_hero/unmasked_hero_controller.dart

/// Locate Hero from within a given context
/// returns a [Rect] that will hold the hero's position & size in the context's frame of reference
Rect _locateHero({
required UnmaskedHeroState hero,
required BuildContext context,
}) {
final heroRenderBox = (hero.context.findRenderObject() as RenderBox);
final RenderBox ancestorRenderBox = context.findRenderObject() as RenderBox;
assert(heroRenderBox.hasSize && heroRenderBox.size.isFinite);

return MatrixUtilstransformRect(
heroRenderBox.getTransformTo(ancestorRenderBox),
Offset.zero & heroRenderBox.size,
);
}
++/pre>

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:

++pre>++code>for (UnmaskedHeroState hero in destinationHeroes.values) {
final UnmaskedHeroState? sourceHero = sourceHeroes[hero.widget.tag];
final UnmaskedHeroState destinationHero = hero;
if (sourceHero == null) {
print(
'No source Hero could be found for destination Hero with tag: ${hero.widget.tag}');
continue;
}

final Rect fromPosition =
_locateHero(hero: sourceHero, context: fromContext);
final Rect toPosition =
_locateHero(hero: destinationHero, context: toContext);
print(fromPosition);
print(toPosition);
_displayFlyingHero(hero);
}++/pre>

Compute locations

Display flying Hero at source 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:

++pre>++code>/// lib/packages/unmasked_hero/unmasked_hero_controller.dart

/// Display Hero on Overlay at the given position
void _displayFlyingHeroAtPosition({
required UnmaskedHeroState hero,
required Rect position,
}) {
...
}

@override
void didPush(Route<dynamic> toRoute, Route<dynamic>? fromRoute) {
...

final Rect fromPosition =_locateHero(hero:: sourceHero, context: fromContext);
final Rect toPosition = _locateHero(hero: destinationHero, context: toContext);
_displayFlyingHeroAtPosition(hero: hero, position: fromPosition);
}++/pre>

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:

++pre>++code>/// lib/packages/unmasked_hero/unmasked_hero_controller.dart

overlayEntry = OverlayEntry(
builder: (BuildContext context) => Positioned(
child: hero.widget.child,
top: position.top,
left: position.left,
width: position.width,
height: position.height,
),
);++/pre>

Unmasked_Hero_Initial_Position-1

The overlayed Hero now appears at the same position as the source Hero widget.

2. ? Animate hero between from/to positions

Compute locations

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:

++pre>++code>/// lib/packages/unmasked_hero/flying_unmasked_hero.dart

import 'dart:async';
import 'package:flutter/widgets.dart';

class FlyingUnmaskedHero extends StatefulWidget {
final Rect fromPosition;
final Rect toPosition;
final Widget child;

FlyingUnmaskedHero({
required this.fromPosition,
required this.toPosition,
required this.child,
});

@override
FlyingUnmaskedHeroState createState() => FlyingUnmaskedHeroState();
}

class FlyingUnmaskedHeroState extends State<FlyingUnmaskedHero> {
bool flying = false;

@override
void initState() {
Timer(Duration(milliseconds: 0), () {
setState(() {
flying = true;
});
});
super.initState();
}

@override
Widget build(BuildContext context) {
final Rect fromPosition = widget.fromPosition;
final Rect toPosition = widget.toPosition;
return AnimatedPositioned(
child: widget.child,
duration: Duration(milliseconds: 200),
top: flying ? toPosition.top : fromPosition.top,
left: flying ? toPosition.left : fromPosition.left,
height: flying ? toPosition.height : fromPosition.height,
width: flying ? toPosition.width : fromPosition.width,
);
}
}
++/pre>

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:

++pre>++code>/// lib/packages/unmasked_hero/unmasked_hero_controller.dart

void _startFlying({
required UnmaskedHeroState hero,
required Rect fromPosition,
required Rect toPosition,
}) {
...
}

...

_startFlying(hero: hero, fromPosition: fromPosition, toPosition: toPosition);++/pre>

Finally, we can replace the widget built by the ++code>OverlayEntry++/code> to use our ++code>FlyingUnmaskedHero++/code>:

++pre>++code>/// lib/packages/unmasked_hero/unmasked_hero_controller.dart

overlayEntry = OverlayEntry(
builder: (BuildContext context) => FlyingUnmaskedHero(
fromPosition: fromPosition,
toPosition: toPosition,
child: hero.widget.child,
),
);++/pre>

? is it a bird ? ? is it a plane ? ? ?

Flying_Unmasked_Hero_Raw-1

Our hero animates nicely between the initial and final positions ?.

3. ? Clean up

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


Compute locations

Remove the widget from the overla

Développeur Mobile ?

Rejoins nos équipes