heroes.dart 18.6 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6

Hixie's avatar
Hixie committed
7
import 'basic.dart';
8
import 'binding.dart';
Hixie's avatar
Hixie committed
9
import 'framework.dart';
10 11 12
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
Hixie's avatar
Hixie committed
13 14
import 'transitions.dart';

Hans Muller's avatar
Hans Muller committed
15 16 17 18 19 20 21 22 23 24 25 26 27
/// Signature for a function that takes two [Rect] instances and returns a
/// [RectTween] that transitions between them.
///
/// This is typically used with a [HeroController] to provide an animation for
/// [Hero] positions that looks nicer than a linear movement. For example, see
/// [MaterialRectArcTween].
typedef RectTween CreateRectTween(Rect begin, Rect end);

typedef void _OnFlightEnded(_HeroFlight flight);

enum _HeroFlightType {
  push, // Fly the "to" hero and animate with the "to" route.
  pop, // Fly the "to" hero and animate with the "from" route.
Hixie's avatar
Hixie committed
28 29
}

Hans Muller's avatar
Hans Muller committed
30
// The bounding box for context in global coordinates.
31
Rect _globalBoundingBoxFor(BuildContext context) {
Hans Muller's avatar
Hans Muller committed
32 33
  final RenderBox box = context.findRenderObject();
  assert(box != null && box.hasSize);
34
  return MatrixUtils.transformRect(box.getTransformTo(null), Offset.zero & box.size);
Hixie's avatar
Hixie committed
35 36
}

Ian Hickson's avatar
Ian Hickson committed
37 38
/// A widget that marks its child as being a candidate for hero animations.
///
Hans Muller's avatar
Hans Muller committed
39 40 41 42 43 44 45 46
/// When a [PageRoute] is pushed or popped with the [Navigator], the entire
/// screen's content is replaced. An old route disappears and a new route
/// appears. If there's a common visual feature on both routes then it can
/// be helpful for orienting the user for the feature to physically move from
/// one page to the other during the routes' transition. Such an animation
/// is called a *hero animation*. The hero widgets "fly" in the Navigator's
/// overlay during the transition and while they're in-flight they're
/// not shown in their original locations in the old and new routes.
Ian Hickson's avatar
Ian Hickson committed
47
///
Hans Muller's avatar
Hans Muller committed
48 49 50 51
/// To label a widget as such a feature, wrap it in a [Hero] widget. When
/// navigation happens, the [Hero] widgets on each route are identified
/// by the [HeroController]. For each pair of [Hero] widgets that have the
/// same tag, a hero animation is triggered.
Ian Hickson's avatar
Ian Hickson committed
52
///
Hans Muller's avatar
Hans Muller committed
53 54
/// If a [Hero] is already in flight when navigation occurs, its
/// flight animation will be redirected to its new destination.
Ian Hickson's avatar
Ian Hickson committed
55
///
Hans Muller's avatar
Hans Muller committed
56
/// Routes must not contain more than one [Hero] for each [tag].
Ian Hickson's avatar
Ian Hickson committed
57 58 59
///
/// ## Discussion
///
Hans Muller's avatar
Hans Muller committed
60
/// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for
Ian Hickson's avatar
Ian Hickson committed
61
/// all this to work. The top left and bottom right coordinates of each animated
Hans Muller's avatar
Hans Muller committed
62
/// Hero will be converted to global coordinates and then from there converted
Ian Hickson's avatar
Ian Hickson committed
63 64 65 66 67 68 69 70 71 72 73 74 75
/// to that [Stack]'s coordinate space, and the entire Hero subtree will, for
/// the duration of the animation, be lifted out of its original place, and
/// positioned on that stack. If the [Hero] isn't axis aligned, this is going to
/// fail in a rather ugly fashion. Don't rotate your heroes!
///
/// To make the animations look good, it's critical that the widget tree for the
/// hero in both locations be essentially identical. The widget of the *target*
/// is used to do the transition: when going from route A to route B, route B's
/// hero's widget is placed over route A's hero's widget, and route A's hero is
/// hidden. Then the widget is animated to route B's hero's position, and then
/// the widget is inserted into route B. When going back from B to A, route A's
/// hero's widget is placed over where route B's hero's widget was, and then the
/// animation goes the other way.
76
class Hero extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
77 78
  /// Create a hero.
  ///
Hans Muller's avatar
Hans Muller committed
79
  /// The [tag] and [child] parameters must not be null.
80
  const Hero({
Hixie's avatar
Hixie committed
81
    Key key,
Ian Hickson's avatar
Ian Hickson committed
82 83
    @required this.tag,
    @required this.child,
84 85 86
  }) : assert(tag != null),
       assert(child != null),
       super(key: key);
Hixie's avatar
Hixie committed
87

Ian Hickson's avatar
Ian Hickson committed
88
  /// The identifier for this particular hero. If the tag of this hero matches
Hans Muller's avatar
Hans Muller committed
89 90
  /// the tag of a hero on a [PageRoute] that we're navigating to or from, then
  /// a hero animation will be triggered.
Hixie's avatar
Hixie committed
91
  final Object tag;
92

Hans Muller's avatar
Hans Muller committed
93 94
  /// The widget subtree that will "fly" from one route to another during a
  /// [Naviator] push or pop transition.
Ian Hickson's avatar
Ian Hickson committed
95
  ///
Hans Muller's avatar
Hans Muller committed
96 97 98 99
  /// The appearance of this subtree should be similar to the appearance of
  /// the subtrees of any other heroes in the application with the same [tag].
  /// Changes in scale and aspect ratio work well in hero animations, changes
  /// in layout or composition do not.
Ian Hickson's avatar
Ian Hickson committed
100 101
  final Widget child;

Hans Muller's avatar
Hans Muller committed
102 103 104 105
  // Returns a map of all of the heroes in context, indexed by hero tag.
  static Map<Object, _HeroState> _allHeroesFor(BuildContext context) {
    assert(context != null);
    final Map<Object, _HeroState> result = <Object, _HeroState>{};
Hixie's avatar
Hixie committed
106 107
    void visitor(Element element) {
      if (element.widget is Hero) {
Hans Muller's avatar
Hans Muller committed
108 109 110
        final StatefulElement hero = element;
        final Hero heroWidget = element.widget;
        final Object tag = heroWidget.tag;
Hixie's avatar
Hixie committed
111
        assert(tag != null);
112
        assert(() {
113
          if (result.containsKey(tag)) {
Hans Muller's avatar
Hans Muller committed
114
            throw new FlutterError(
115
              'There are multiple heroes that share the same tag within a subtree.\n'
116
              'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
117 118
              'each Hero must have a unique non-null tag.\n'
              'In this case, multiple heroes had the tag "$tag".'
Hixie's avatar
Hixie committed
119
            );
120 121 122
          }
          return true;
        });
123
        final _HeroState heroState = hero.state;
124
        result[tag] = heroState;
Hixie's avatar
Hixie committed
125 126 127 128 129 130 131
      }
      element.visitChildren(visitor);
    }
    context.visitChildElements(visitor);
    return result;
  }

132
  @override
133
  _HeroState createState() => new _HeroState();
Hixie's avatar
Hixie committed
134 135
}

Hans Muller's avatar
Hans Muller committed
136
class _HeroState extends State<Hero> {
137
  final GlobalKey _key = new GlobalKey();
Hixie's avatar
Hixie committed
138
  Size _placeholderSize;
Hixie's avatar
Hixie committed
139

Hans Muller's avatar
Hans Muller committed
140
  void startFlight() {
Hixie's avatar
Hixie committed
141
    assert(mounted);
Hans Muller's avatar
Hans Muller committed
142 143
    final RenderBox box = context.findRenderObject();
    assert(box != null && box.hasSize);
Hixie's avatar
Hixie committed
144
    setState(() {
Hans Muller's avatar
Hans Muller committed
145
      _placeholderSize = box.size;
Hixie's avatar
Hixie committed
146
    });
Hixie's avatar
Hixie committed
147 148
  }

Hans Muller's avatar
Hans Muller committed
149 150 151 152 153 154
  void endFlight() {
    if (mounted) {
      setState(() {
        _placeholderSize = null;
      });
    }
155 156
  }

157
  @override
Hixie's avatar
Hixie committed
158
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
159
    if (_placeholderSize != null) {
Hans Muller's avatar
Hans Muller committed
160 161 162 163
      return new SizedBox(
        width: _placeholderSize.width,
        height: _placeholderSize.height
      );
Hixie's avatar
Hixie committed
164
    }
Hixie's avatar
Hixie committed
165 166
    return new KeyedSubtree(
      key: _key,
167
      child: widget.child,
Hixie's avatar
Hixie committed
168
    );
Hixie's avatar
Hixie committed
169 170 171
  }
}

172
// Everything known about a hero flight that's to be started or diverted.
Hans Muller's avatar
Hans Muller committed
173 174 175 176 177 178 179 180 181 182
class _HeroFlightManifest {
  _HeroFlightManifest({
    @required this.type,
    @required this.overlay,
    @required this.navigatorRect,
    @required this.fromRoute,
    @required this.toRoute,
    @required this.fromHero,
    @required this.toHero,
    @required this.createRectTween,
Hixie's avatar
Hixie committed
183
  }) {
184
    assert(fromHero.widget.tag == toHero.widget.tag);
Hixie's avatar
Hixie committed
185 186
  }

Hans Muller's avatar
Hans Muller committed
187 188 189 190 191 192 193 194
  final _HeroFlightType type;
  final OverlayState overlay;
  final Rect navigatorRect;
  final PageRoute<dynamic> fromRoute;
  final PageRoute<dynamic> toRoute;
  final _HeroState fromHero;
  final _HeroState toHero;
  final CreateRectTween createRectTween;
195

196
  Object get tag => fromHero.widget.tag;
197

Hans Muller's avatar
Hans Muller committed
198 199 200 201
  Animation<double> get animation {
    return new CurvedAnimation(
      parent: (type == _HeroFlightType.push) ? toRoute.animation : fromRoute.animation,
      curve: Curves.fastOutSlowIn,
Hixie's avatar
Hixie committed
202 203 204
    );
  }

Hans Muller's avatar
Hans Muller committed
205 206 207
  @override
  String toString() {
    return '_HeroFlightManifest($type hero: $tag from: ${fromRoute.settings} to: ${toRoute.settings})';
208
  }
Hans Muller's avatar
Hans Muller committed
209
}
210

Hans Muller's avatar
Hans Muller committed
211 212 213 214
// Builds the in-flight hero widget.
class _HeroFlight {
  _HeroFlight(this.onFlightEnded) {
    _proxyAnimation = new ProxyAnimation()..addStatusListener(_handleAnimationUpdate);
Hixie's avatar
Hixie committed
215
  }
216

Hans Muller's avatar
Hans Muller committed
217
  final _OnFlightEnded onFlightEnded;
Hixie's avatar
Hixie committed
218

Hans Muller's avatar
Hans Muller committed
219 220 221 222 223
  RectTween heroRect;
  Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
  ProxyAnimation _proxyAnimation;
  _HeroFlightManifest manifest;
  OverlayEntry overlayEntry;
224
  bool _aborted = false;
Hixie's avatar
Hixie committed
225

Hans Muller's avatar
Hans Muller committed
226 227 228 229 230
  RectTween _doCreateRectTween(Rect begin, Rect end) {
    if (manifest.createRectTween != null)
      return manifest.createRectTween(begin, end);
    return new RectTween(begin: begin, end: end);
  }
231

Hans Muller's avatar
Hans Muller committed
232 233 234 235 236
  // The OverlayEntry WidgetBuilder callback for the hero's overlay.
  Widget _buildOverlay(BuildContext context) {
    assert(manifest != null);
    return new AnimatedBuilder(
      animation: _proxyAnimation,
237
      child: manifest.toHero.widget,
Hans Muller's avatar
Hans Muller committed
238 239
      builder: (BuildContext context, Widget child) {
        final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject();
240 241 242 243
        if (_aborted || toHeroBox == null || !toHeroBox.attached) {
          // The toHero no longer exists or it's no longer the flight's destination.
          // Continue flying while fading out.
          if (_heroOpacity.isCompleted) {
Hans Muller's avatar
Hans Muller committed
244 245 246 247 248 249 250 251
            _heroOpacity = new Tween<double>(begin: 1.0, end: 0.0)
              .chain(new CurveTween(curve: new Interval(_proxyAnimation.value, 1.0)))
              .animate(_proxyAnimation);
          }
        } else if (toHeroBox.hasSize) {
          // The toHero has been laid out. If it's no longer where the hero animation is
          // supposed to end up (heroRect.end) then recreate the heroRect tween.
          final RenderBox routeBox = manifest.toRoute.subtreeContext?.findRenderObject();
252
          final Offset heroOriginEnd = toHeroBox.localToGlobal(Offset.zero, ancestor: routeBox);
Hans Muller's avatar
Hans Muller committed
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
          if (heroOriginEnd != heroRect.end.topLeft) {
            final Rect heroRectEnd = heroOriginEnd & heroRect.end.size;
            heroRect = _doCreateRectTween(heroRect.begin, heroRectEnd);
          }
        }

        final Rect rect = heroRect.evaluate(_proxyAnimation);
        final Size size = manifest.navigatorRect.size;
        final RelativeRect offsets = new RelativeRect.fromSize(rect, size);

        return new Positioned(
          top: offsets.top,
          right: offsets.right,
          bottom: offsets.bottom,
          left: offsets.left,
          child:  new IgnorePointer(
            child: new RepaintBoundary(
              child: new Opacity(
                key: manifest.toHero._key,
                opacity: _heroOpacity.value,
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }
Hixie's avatar
Hixie committed
281

Hans Muller's avatar
Hans Muller committed
282 283 284
  void _handleAnimationUpdate(AnimationStatus status) {
    if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
      _proxyAnimation.parent = null;
Hixie's avatar
Hixie committed
285

Hans Muller's avatar
Hans Muller committed
286 287 288
      assert(overlayEntry != null);
      overlayEntry.remove();
      overlayEntry = null;
Hixie's avatar
Hixie committed
289

Hans Muller's avatar
Hans Muller committed
290 291 292 293
      manifest.fromHero.endFlight();
      manifest.toHero.endFlight();
      onFlightEnded(this);
    }
Hixie's avatar
Hixie committed
294 295
  }

Hans Muller's avatar
Hans Muller committed
296 297
  // The simple case: we're either starting a push or a pop animation.
  void start(_HeroFlightManifest initialManifest) {
298
    assert(!_aborted);
Hans Muller's avatar
Hans Muller committed
299 300 301 302 303 304 305 306 307
    assert(() {
      final Animation<double> initial = initialManifest.animation;
      switch (initialManifest.type) {
        case _HeroFlightType.pop:
          return initial.value == 1.0 && initial.status == AnimationStatus.reverse;
        case _HeroFlightType.push:
          return initial.value == 0.0 && initial.status == AnimationStatus.forward;
      }
    });
Hixie's avatar
Hixie committed
308

Hans Muller's avatar
Hans Muller committed
309
    manifest = initialManifest;
310

Hans Muller's avatar
Hans Muller committed
311 312 313 314
    if (manifest.type == _HeroFlightType.pop)
      _proxyAnimation.parent = new ReverseAnimation(manifest.animation);
    else
      _proxyAnimation.parent = manifest.animation;
Hixie's avatar
Hixie committed
315

Hans Muller's avatar
Hans Muller committed
316 317
    manifest.fromHero.startFlight();
    manifest.toHero.startFlight();
Hixie's avatar
Hixie committed
318

319 320 321
    heroRect = _doCreateRectTween(
      _globalBoundingBoxFor(manifest.fromHero.context),
      _globalBoundingBoxFor(manifest.toHero.context),
Hans Muller's avatar
Hans Muller committed
322
    );
Hixie's avatar
Hixie committed
323

Hans Muller's avatar
Hans Muller committed
324 325
    overlayEntry = new OverlayEntry(builder: _buildOverlay);
    manifest.overlay.insert(overlayEntry);
Hixie's avatar
Hixie committed
326 327
  }

Hans Muller's avatar
Hans Muller committed
328 329
  // While this flight's hero was in transition a push or a pop occurred for
  // routes with the same hero. Redirect the in-flight hero to the new toRoute.
330
  void divert(_HeroFlightManifest newManifest) {
Hans Muller's avatar
Hans Muller committed
331
    assert(manifest.tag == newManifest.tag);
Hixie's avatar
Hixie committed
332

Hans Muller's avatar
Hans Muller committed
333 334 335 336 337 338 339
    if (manifest.type == _HeroFlightType.push && newManifest.type == _HeroFlightType.pop) {
      // A push flight was interrupted by a pop.
      assert(newManifest.animation.status == AnimationStatus.reverse);
      assert(manifest.fromHero == newManifest.toHero);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.fromRoute == newManifest.toRoute);
      assert(manifest.toRoute == newManifest.fromRoute);
340

Hans Muller's avatar
Hans Muller committed
341
      _proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
Hixie's avatar
Hixie committed
342

343
      heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
Hans Muller's avatar
Hans Muller committed
344 345 346 347 348 349 350 351 352 353 354 355 356 357
    } else if (manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.push) {
      // A pop flight was interrupted by a push.
      assert(newManifest.animation.status == AnimationStatus.forward);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.toRoute == newManifest.fromRoute);

      _proxyAnimation.parent = new Tween<double>(
        begin: manifest.animation.value,
        end: 1.0,
      ).animate(newManifest.animation);

      if (manifest.fromHero != newManifest.toHero) {
        manifest.fromHero.endFlight();
        newManifest.toHero.startFlight();
358
        heroRect = _doCreateRectTween(heroRect.end, _globalBoundingBoxFor(newManifest.toHero.context));
Hans Muller's avatar
Hans Muller committed
359
      } else {
360
        heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
Hixie's avatar
Hixie committed
361
      }
Hans Muller's avatar
Hans Muller committed
362 363 364 365 366 367 368
    } else {
      // A push or a pop flight is heading to a new route, i.e.
      // manifest.type == _HeroFlightType.push && newManifest.type == _HeroFlightType.push ||
      // manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.pop
      assert(manifest.fromHero != newManifest.fromHero);
      assert(manifest.toHero != newManifest.toHero);

369
      heroRect = _doCreateRectTween(heroRect.evaluate(_proxyAnimation), _globalBoundingBoxFor(newManifest.toHero.context));
Hans Muller's avatar
Hans Muller committed
370 371 372 373 374 375 376 377 378 379

      if (newManifest.type == _HeroFlightType.pop)
        _proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
      else
        _proxyAnimation.parent = newManifest.animation;

      manifest.fromHero.endFlight();
      manifest.toHero.endFlight();
      newManifest.fromHero.startFlight();
      newManifest.toHero.startFlight();
Hixie's avatar
Hixie committed
380
    }
Hans Muller's avatar
Hans Muller committed
381

382
    _aborted = false;
Hans Muller's avatar
Hans Muller committed
383
    manifest = newManifest;
Hixie's avatar
Hixie committed
384 385
  }

386 387 388 389
  void abort() {
    _aborted = true;
  }

390
  @override
Hans Muller's avatar
Hans Muller committed
391 392 393 394 395 396
  String toString() {
    final RouteSettings from = manifest.fromRoute.settings;
    final RouteSettings to = manifest.toRoute.settings;
    final Object tag = manifest.tag;
    return 'HeroFlight(for: $tag, from: $from, to: $to ${_proxyAnimation.parent})';
  }
Hixie's avatar
Hixie committed
397
}
398

Ian Hickson's avatar
Ian Hickson committed
399 400 401 402
/// A [Navigator] observer that manages [Hero] transitions.
///
/// An instance of [HeroController] should be used as the [Navigator.observer].
/// This is done automatically by [MaterialApp].
403
class HeroController extends NavigatorObserver {
Ian Hickson's avatar
Ian Hickson committed
404 405
  /// Creates a hero controller with the given [RectTween] constructor if any.
  ///
406 407
  /// The [createRectTween] argument is optional. If null, the controller uses a
  /// linear [RectTween].
Hans Muller's avatar
Hans Muller committed
408
  HeroController({ this.createRectTween });
409

410 411 412
  /// Used to create [RectTween]s that interpolate the position of heros in flight.
  ///
  /// If null, the controller uses a linear [RectTween].
Hans Muller's avatar
Hans Muller committed
413
  final CreateRectTween createRectTween;
414

Hans Muller's avatar
Hans Muller committed
415 416
  // Disable Hero animations while a user gesture is controlling the navigation.
  bool _questsEnabled = true;
417

Hans Muller's avatar
Hans Muller committed
418 419
  // All of the heroes that are currently in the overlay and in motion.
  // Indexed by the hero tag.
420
  final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{};
421

422
  @override
Hans Muller's avatar
Hans Muller committed
423
  void didPush(Route<dynamic> to, Route<dynamic> from) {
424
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
425 426
    assert(to != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.push);
427 428
  }

429
  @override
Hans Muller's avatar
Hans Muller committed
430
  void didPop(Route<dynamic> from, Route<dynamic> to) {
431
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
432 433
    assert(from != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.pop);
434 435
  }

436 437 438 439 440 441 442 443 444 445
  @override
  void didStartUserGesture() {
    _questsEnabled = false;
  }

  @override
  void didStopUserGesture() {
    _questsEnabled = true;
  }

Hans Muller's avatar
Hans Muller committed
446 447 448 449 450 451 452 453
  // If we're transitioning between different page routes, start a hero transition
  // after the toRoute has been laid out with its animation's value at 1.0.
  void _maybeStartHeroTransition(Route<dynamic> fromRoute, Route<dynamic> toRoute, _HeroFlightType flightType) {
    if (_questsEnabled && toRoute != fromRoute && toRoute is PageRoute<dynamic> && fromRoute is PageRoute<dynamic>) {
      final PageRoute<dynamic> from = fromRoute;
      final PageRoute<dynamic> to = toRoute;
      final Animation<double> animation = (flightType == _HeroFlightType.push) ? to.animation : from.animation;

454
      // A user gesture may have already completed the pop.
Hans Muller's avatar
Hans Muller committed
455 456 457
      if (flightType == _HeroFlightType.pop && animation.status == AnimationStatus.dismissed)
        return;

458 459 460 461
      // Putting a route offstage changes its animation value to 1.0. Once this
      // frame completes, we'll know where the heroes in the `to` route are
      // going to end up, and the `to` route will go back onstage.
      to.offstage = to.animation.value == 0.0;
Hans Muller's avatar
Hans Muller committed
462

463
      WidgetsBinding.instance.addPostFrameCallback((Duration value) {
Hans Muller's avatar
Hans Muller committed
464 465
        _startHeroTransition(from, to, flightType);
      });
466 467 468
    }
  }

Hans Muller's avatar
Hans Muller committed
469
  // Find the matching pairs of heros in from and to and either start or a new
470
  // hero flight, or divert an existing one.
Hans Muller's avatar
Hans Muller committed
471
  void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {
472 473
    // If the navigator or one of the routes subtrees was removed before this
    // end-of-frame callback was called, then don't actually start a transition.
Hans Muller's avatar
Hans Muller committed
474
    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
475
      to.offstage = false; // in case we set this in _maybeStartHeroTransition
476 477
      return;
    }
478

479
    final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);
Hans Muller's avatar
Hans Muller committed
480 481 482 483 484

    // At this point the toHeroes may have been built and laid out for the first time.
    final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext);
    final Map<Object, _HeroState> toHeroes = Hero._allHeroesFor(to.subtreeContext);

485
    // If the `to` route was offstage, then we're implicitly restoring its
Hans Muller's avatar
Hans Muller committed
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
    // animation value back to what it was before it was "moved" offstage.
    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)
502
          _flights[tag].divert(manifest);
Hans Muller's avatar
Hans Muller committed
503 504
        else
          _flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);
505 506
      } else if (_flights[tag] != null) {
        _flights[tag].abort();
Hans Muller's avatar
Hans Muller committed
507
      }
508
    }
509 510
  }

Hans Muller's avatar
Hans Muller committed
511 512
  void _handleFlightEnded(_HeroFlight flight) {
    _flights.remove(flight.manifest.tag);
513 514
  }
}