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
  /// The widget subtree that will "fly" from one route to another during a
94
  /// [Navigator] 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,
183
  }) : assert(fromHero.widget.tag == toHero.widget.tag);
Hixie's avatar
Hixie committed
184

Hans Muller's avatar
Hans Muller committed
185 186 187 188 189 190 191 192
  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;
193

194
  Object get tag => fromHero.widget.tag;
195

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

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

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

Hans Muller's avatar
Hans Muller committed
215
  final _OnFlightEnded onFlightEnded;
Hixie's avatar
Hixie committed
216

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

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

Hans Muller's avatar
Hans Muller committed
230 231 232 233 234
  // The OverlayEntry WidgetBuilder callback for the hero's overlay.
  Widget _buildOverlay(BuildContext context) {
    assert(manifest != null);
    return new AnimatedBuilder(
      animation: _proxyAnimation,
235
      child: manifest.toHero.widget,
Hans Muller's avatar
Hans Muller committed
236 237
      builder: (BuildContext context, Widget child) {
        final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject();
238 239 240 241
        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
242 243 244 245 246 247 248 249
            _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();
250
          final Offset heroOriginEnd = toHeroBox.localToGlobal(Offset.zero, ancestor: routeBox);
Hans Muller's avatar
Hans Muller committed
251 252 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
          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
279

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

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

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

Hans Muller's avatar
Hans Muller committed
294 295
  // The simple case: we're either starting a push or a pop animation.
  void start(_HeroFlightManifest initialManifest) {
296
    assert(!_aborted);
Hans Muller's avatar
Hans Muller committed
297 298 299 300 301 302 303 304 305
    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
306

Hans Muller's avatar
Hans Muller committed
307
    manifest = initialManifest;
308

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

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

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

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

Hans Muller's avatar
Hans Muller committed
326 327
  // 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.
328
  void divert(_HeroFlightManifest newManifest) {
Hans Muller's avatar
Hans Muller committed
329
    assert(manifest.tag == newManifest.tag);
Hixie's avatar
Hixie committed
330

Hans Muller's avatar
Hans Muller committed
331 332 333 334 335 336 337
    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);
338

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

341
      heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
Hans Muller's avatar
Hans Muller committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355
    } 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();
356
        heroRect = _doCreateRectTween(heroRect.end, _globalBoundingBoxFor(newManifest.toHero.context));
Hans Muller's avatar
Hans Muller committed
357
      } else {
358
        heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
Hixie's avatar
Hixie committed
359
      }
Hans Muller's avatar
Hans Muller committed
360 361 362 363 364 365 366
    } 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);

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

      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
378
    }
Hans Muller's avatar
Hans Muller committed
379

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

384 385 386 387
  void abort() {
    _aborted = true;
  }

388
  @override
Hans Muller's avatar
Hans Muller committed
389 390 391 392 393 394
  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
395
}
396

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

408 409 410
  /// 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
411
  final CreateRectTween createRectTween;
412

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

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

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

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

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

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

Hans Muller's avatar
Hans Muller committed
444 445 446 447 448 449 450 451
  // 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;

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

456 457 458 459
      // 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
460

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

Hans Muller's avatar
Hans Muller committed
467
  // Find the matching pairs of heros in from and to and either start or a new
468
  // hero flight, or divert an existing one.
Hans Muller's avatar
Hans Muller committed
469
  void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {
470 471
    // 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
472
    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
473
      to.offstage = false; // in case we set this in _maybeStartHeroTransition
474 475
      return;
    }
476

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

    // 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);

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

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