heroes.dart 18.8 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 31 32 33 34
// The bounding box for context in global coordinates.
Rect _globalRect(BuildContext context) {
  final RenderBox box = context.findRenderObject();
  assert(box != null && box.hasSize);
  return MatrixUtils.transformRect(box.getTransformTo(null), Point.origin & 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.
Hixie's avatar
Hixie committed
80 81
  Hero({
    Key key,
Ian Hickson's avatar
Ian Hickson committed
82 83
    @required this.tag,
    @required this.child,
Hixie's avatar
Hixie committed
84 85
  }) : super(key: key) {
    assert(tag != null);
Ian Hickson's avatar
Ian Hickson committed
86
    assert(child != null);
Hixie's avatar
Hixie committed
87 88
  }

Ian Hickson's avatar
Ian Hickson committed
89
  /// The identifier for this particular hero. If the tag of this hero matches
Hans Muller's avatar
Hans Muller committed
90 91
  /// 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
92
  final Object tag;
93

Hans Muller's avatar
Hans Muller committed
94 95
  /// 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
96
  ///
Hans Muller's avatar
Hans Muller committed
97 98 99 100
  /// 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
101 102
  final Widget child;

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

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

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

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

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

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

Hans Muller's avatar
Hans Muller committed
173 174 175 176 177 178 179 180 181 182 183
// Everything known about a hero flight that's to be started or restarted.
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
184
  }) {
Hans Muller's avatar
Hans Muller committed
185
    assert(fromHero.config.tag == toHero.config.tag);
Hixie's avatar
Hixie committed
186 187
  }

Hans Muller's avatar
Hans Muller committed
188 189 190 191 192 193 194 195
  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;
196

Hans Muller's avatar
Hans Muller committed
197
  Object get tag => fromHero.config.tag;
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Hans Muller's avatar
Hans Muller committed
320 321 322 323
    heroRect = new RectTween(
      begin: _globalRect(manifest.fromHero.context),
      end: _globalRect(manifest.toHero.context),
    );
Hixie's avatar
Hixie committed
324

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

Hans Muller's avatar
Hans Muller committed
329 330 331 332
  // 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.
  void restart(_HeroFlightManifest newManifest) {
    assert(manifest.tag == newManifest.tag);
Hixie's avatar
Hixie committed
333

Hans Muller's avatar
Hans Muller committed
334 335 336 337 338 339 340
    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);
341

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

Hans Muller's avatar
Hans Muller committed
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
      heroRect = new RectTween(
        begin: heroRect.end,
        end: heroRect.begin,
      );
    } 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();
        heroRect = new RectTween(
          begin: heroRect.end,
          end: _globalRect(newManifest.toHero.context),
        );
      } else {
        heroRect = new RectTween(
          begin: heroRect.end,
          end: heroRect.begin,
        );
Hixie's avatar
Hixie committed
371
      }
Hans Muller's avatar
Hans Muller committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
    } 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);

      heroRect = new RectTween(
        begin: heroRect.evaluate(_proxyAnimation),
        end: _globalRect(newManifest.toHero.context),
      );

      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
393
    }
Hans Muller's avatar
Hans Muller committed
394

395
    _aborted = false;
Hans Muller's avatar
Hans Muller committed
396
    manifest = newManifest;
Hixie's avatar
Hixie committed
397 398
  }

399 400 401 402
  void abort() {
    _aborted = true;
  }

403
  @override
Hans Muller's avatar
Hans Muller committed
404 405 406 407 408 409
  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
410
}
411

Ian Hickson's avatar
Ian Hickson committed
412 413 414 415
/// A [Navigator] observer that manages [Hero] transitions.
///
/// An instance of [HeroController] should be used as the [Navigator.observer].
/// This is done automatically by [MaterialApp].
416
class HeroController extends NavigatorObserver {
Ian Hickson's avatar
Ian Hickson committed
417 418
  /// Creates a hero controller with the given [RectTween] constructor if any.
  ///
419 420
  /// The [createRectTween] argument is optional. If null, the controller uses a
  /// linear [RectTween].
Hans Muller's avatar
Hans Muller committed
421
  HeroController({ this.createRectTween });
422

423 424 425
  /// 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
426
  final CreateRectTween createRectTween;
427

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

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

435
  @override
Hans Muller's avatar
Hans Muller committed
436
  void didPush(Route<dynamic> to, Route<dynamic> from) {
437
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
438 439
    assert(to != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.push);
440 441
  }

442
  @override
Hans Muller's avatar
Hans Muller committed
443
  void didPop(Route<dynamic> from, Route<dynamic> to) {
444
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
445 446
    assert(from != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.pop);
447 448
  }

449 450 451 452 453 454 455 456 457 458
  @override
  void didStartUserGesture() {
    _questsEnabled = false;
  }

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

Hans Muller's avatar
Hans Muller committed
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
  // 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;

      // A "user" gesture may have already completed the pop.
      if (flightType == _HeroFlightType.pop && animation.status == AnimationStatus.dismissed)
        return;

      // Putting a route offstage changes its animation value to 1.0.
      // Once this frame completes, we'll know where the heroes in the toRoute
      // are going to end up, and the toRoute will go back on stage.
      to.offstage = animation.value == 0.0 || animation.value == 1.0;

      WidgetsBinding.instance.addPostFrameCallback((Duration _) {
        _startHeroTransition(from, to, flightType);
      });
479 480 481
    }
  }

Hans Muller's avatar
Hans Muller committed
482 483 484 485 486 487 488 489
  // Find the matching pairs of heros in from and to and either start or a new
  // hero flight, or restart an existing one.
  void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {
    // 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.
    // TBD: need to generate tests for these cases
    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
      to.offstage = false; // TBD: only do this if to.subtreeContext != null?
490 491
      return;
    }
492

Hans Muller's avatar
Hans Muller committed
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    final Rect navigatorRect = _globalRect(navigator.context);

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

    // If the to route was offstage, then we're implicitly restoring its
    // 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)
            _flights[tag].restart(manifest);
        else
          _flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);
520 521
      } else if (_flights[tag] != null) {
        _flights[tag].abort();
Hans Muller's avatar
Hans Muller committed
522
      }
523
    }
524 525
  }

Hans Muller's avatar
Hans Muller committed
526 527
  void _handleFlightEnded(_HeroFlight flight) {
    _flights.remove(flight.manifest.tag);
528 529
  }
}