- 原 句 : Flutter Heroes and Villains — Bringing balance to the Flutterverse.
- Originally written by Norbert
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: DateBro
This is a story about how Heroes and Villains work.
A Hero is often accompanied by multiple villains.
Villain allows you to add the page transformation above with just a few lines of code.
The installation package is here. You can use Villains in the README of the project. This article focused more on explaining Heroes and Villains and the thought process behind all of this.
One of the most amazing things about Flutter is that it provides a nice and clean API for everything. I like the way you use Hero. Two simple lines of code, and it works. All you need to do is drop the Hero into these two places, assign it according to the label, and forget about the rest.
Before you can understand Villain, you must understand Hero.
Take a quick look at Hero.
Let’s take a quick look at how Hero is implemented.
An overview of
Hero animation involves three main steps.
1. Find and match Heroes
The first step is to determine which Heros exist and which have the same tag.
2. Locate the Hero
Then, capture the locations of both Heros and prepare for the journey.
3. Start the journey
The journey always takes place on the new screen, not in the actual component. The component on the start page is replaced with an empty placeholder component (SizedBox) during the journey. Instead, use overlays (overlays display components on top of everything).
The entire Hero animation takes place on the page being opened. Components are completely independent and do not share any state between pages.
NavigationObserver
NavigationObserver allows you to observe events that push in and eject routes.
/// a [Navigator] observer that manages the transition to [Hero]. /// /// should use an instance of [HeroController] in [navigator.observers]. /// This is done automatically by [MaterialApp]. class HeroController extends NavigatorObserverCopy the code
HeroController
Hero uses this class to start the journey. In addition to being able to add NavigationObservers themselves, The MaterialApp adds HeroController by default. Let’s take a look here.
Hero components
/// Create a Hero /// /// [tag] and [child] must be non-null. const Hero({ Key key, @required this.tag, this.createRectTween, @required this.child, }) : assert(tag ! = null), assert(child ! = null), super(key: key);Copy the code
Constructor for Hero
The Hero component doesn’t actually do much. It has child and Tag. In addition, the createRectTween parameter determines the route taken by the Hero to its destination. The default implementation is the MaterialRectArcTween. As the name suggests, it moves the Hero in an arc to its final position.
Hero’s state is also responsible for capturing the size and replacing itself with a placeholder.
_allHeroesFor
Elements (concrete components) are placed in the tree. With visitors, you can go down the tree and gather information.
// Returns a map of all Heros in the context, indexed by Hero. static Map<Object, _HeroState> _allHeroesFor(BuildContext context) { assert(context ! = null); final Map<Object, _HeroState> result = <Object, _HeroState>{}; void visitor(Element element) {if(element.widget is Hero) { final StatefulElement hero = element; final Hero heroWidget = element.widget; final Object tag = heroWidget.tag; assert(tag ! = null); assert(() {if (result.containsKey(tag)) {
throw new FlutterError(
'There are multiple heroes that share the same tag within a subtree.\n'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
'each Hero must have a unique non-null tag.\n'
'In this case, multiple heroes had the following tag: $tag\n'
'Here is the subtree for one of the offending heroes:\n'
'${element.toStringDeep(prefixLineOne: "# ")}'
);
}
return true; } ()); final _HeroState heroState = hero.state; result[tag] = heroState; } element.visitChildren(visitor); } context.visitChildElements(visitor);return result;
}
Copy the code
heroes.dart
An inline function called visitor is declared inside the method. Context. VisitChildElements (visitor) method and element visitChildren (vistor) until you visit the context of the all elements to the calling function. On each visit, it checks to see if the child is Hero and, if so, saves it to the map.
The beginning of a journey
// Find a matching Hero pair in from and to and start a new Hero journey, // or transfer an existing Hero journey. void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {// If the navigator or one of the routing subtrees is removed before the end-of-frame callback is called, the conversion will not actually begin.if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
to.offstage = false; // in case we set this in _maybeStartHeroTransition
return; } final Rect navigatorRect = _globalBoundingBoxFor(navigator.context); // At this point, toHeroes is probably the first build and layout. final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext); final Map<Object, _HeroState> toHeroes = Hero._allHeroesFor(to.subtreeContext); // If the 'to' route is off-screen, // then we secretly restore its animation value to the state before it was "moved" off-screen. to.offstage =false;
for (Object tag in fromHeroes.keys) {
if(toHeroes[tag] ! = null) { final _HeroFlightManifest manifest = new _HeroFlightManifest(type: flightType,
overlay: navigator.overlay,
navigatorRect: navigatorRect,
fromRoute: from,
toRoute: to,
fromHero: fromHeroes[tag],
toHero: toHeroes[tag],
createRectTween: createRectTween,
);
if(_flights[tag] ! = null) _flights[tag].divert(manifest);else_flights[tag] = new _HeroFlight(_handleFlightEnded).. start(manifest); }else if(_flights[tag] ! = null) { _flights[tag].abort(); }}}Copy the code
heroes.dart
This is called in response to a routing pushin/eject event. In lines 14 and 15, you see the _allHeroesFor call, which finds all heros on both pages. Starting at line 21, build the _HeroFlightManifest and start the journey. From here, there’s a bunch of animation code setup and edge case handling. I suggest you take a look at the whole class, it’s interesting and there’s a lot to learn from it. You can also take a look at this.
How does Villains work
Villains is easier than Hero.
Hero and 3 villains used (AppBar, Text, FAB).
They use the same mechanism to find all villains in a given context, and they also use NavigationObserver to automatically react to page transitions. But instead of animations from screen to screen, animations only on their respective screens.
SequenceAnimation and custom TickerProvider
When dealing with animation, usually use SingleTickerProviderStateMixin or TickerProviderStateMixin. In this case, the animation will not start in the StatefulWidget, so we need another way to access the TickerProvider.
class TransitionTickerProvider implements TickerProvider {
final bool enabled;
TransitionTickerProvider(this.enabled);
@override
Ticker createTicker(TickerCallback onTick) {
return new Ticker(onTick, debugLabel: 'created by $this').. muted = ! this.enabled; }}Copy the code
Customizing a Ticker is very simple. All this is done to implement the TickerProvider interface and return a new Ticker.
static Future playAllVillains(BuildContext context, {bool entrance = true}) { List<_VillainState> villains = VillainController._allVillainssFor(context) .. removeWhere((villain) {if (entrance) {
return! villain.widget.animateEntrance; }else {
return! villain.widget.animateExit; }}); AnimationController controller = new AnimationController(vsync: TransitionTickerProvider(TickerMode.of(context))); SequenceAnimationBuilder builder = new SequenceAnimationBuilder();for (_VillainState villain inVillains) {build.addanimatable (Anim: Tween<double>(begin: 0.0, end: 1.0), from: villain.widget.villainAnimation.from, to: villain.widget.villainAnimation.to, tag: villain.hashCode, ); } SequenceAnimation sequenceAnimation = builder.animate(controller);for (_VillainState villain invillains) { villain.startAnimation(sequenceAnimation[villain.hashCode]); } // Start animationreturn controller.forward().then((_) {
controller.dispose();
});
}
Copy the code
First of all, all should not show Villain (those who set animateExit/animateEntrance to false) will be filtered out. Then create an AnimationController with a custom TickerProvider. Using the SequenceAnimation library, each Villain is assigned an animation that runs from 0.0 to 1.0 (from and to durations) in their respective time. Finally, the animation begins. When they are all done, the controller is discarded.
Villains’ build() method
@override
Widget build(BuildContext context) {
Widget animatedWidget = widget.villainAnimation
.animatedWidgetBuilder(widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation), widget.child);
if(widget.secondaryVillainAnimation ! = null) { animatedWidget = widget.secondaryVillainAnimation.animatedWidgetBuilder( widget.secondaryVillainAnimation.animatable.chain(CurveTween(curve: widget.secondaryVillainAnimation.curve)).animate(_animation), animatedWidget); }return animatedWidget;
}
Copy the code
This may seem scary, but bear with me for a moment. Let’s look at lines 3 and 4. Widget. VillainAnimation. AnimatedWidgetBuilder is a custom typedef:
typedef Widget AnimatedWidgetBuilder(Animation animation, Widget child);
Copy the code
Its job is to return a component drawn from an animation (most of the time the returned component is an AnimatedWidget).
It gets Villain’s child and this animation:
widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation)
Copy the code
The chain approach first evaluates the CurveTween. It then uses this value to evaluate the animatable that called it. This simply adds the desired curve to the animation.
Here’s a rough overview of how Villain works, so be sure to check it out tooThe source codeAnd don’t hesitate to ask your questions.
Mutable static variables are bad. Let me explain
Late at night, I sat at my desk and wrote down tests. After a few hours, each individual test had passed and seemed bug-free. Just before going to bed, I put all the tests together to make sure it really works. And then this happened:
Each test can only be passed individually.
I’m confused. Every test was successful. Sure enough, when I ran both tests myself, they worked fine. But when all the tests were run together, the last two failed. WTF.
The obvious first reaction is: “My code must be right, it must be doing something about the way the tests are executed! Maybe the tests are played in parallel and therefore interfere with each other? Maybe it’s because I used the same keys?”
Brian Egan pointed out to me that removing one particular test fixed the bug and moved it to the top so that all the other tests failed as well. If that’s not “sharing data” then I don’t know what is.
I couldn’t help laughing when I found out what the problem was. This is why using static variables is bad in some cases.
Basically, predefined animations are static. I’m too lazy to write a method for each animation to get all the parameters required by the VillainAnimation. So I made the VillainAnimation mutable (bad idea). This way I don’t have to explicitly write all the necessary parameters in the method. It looks like this when used:
Villain(
villainAnimation: VillainAnimation.fromBottom(0.4)
..to = Duration(milliseconds: 150),
child: Text("HI"),Copy the code
A test that breaks everything should start testing the Villain transformation after the page transformation is complete. It sets the start of the animation to 1 second. Because it sets it on a static reference, subsequent tests use it as the default. The test failed because the animation could not run between 1 second and 750 milliseconds.
The fix is simple (make everything immutable and pass parameters in the method) but I still find this little bug very interesting.
conclusion
Thank Villain for restoring the balance between good and bad.
Comments and discussion about # FlutterVillains were welcome. If you’re using Villain to make cool animations together, I’d love to see it.
My Twitter: @ norbertkozsir
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.