heroes.dart 20.2 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
/// 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].
21
typedef Tween<Rect> CreateRectTween(Rect begin, Rect end);
Hans Muller's avatar
Hans Muller committed
22 23 24 25 26 27

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
    @required this.tag,
83
    this.createRectTween,
Ian Hickson's avatar
Ian Hickson committed
84
    @required this.child,
85 86 87
  }) : assert(tag != null),
       assert(child != null),
       super(key: key);
Hixie's avatar
Hixie committed
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

94 95 96 97
  /// Defines how the destination hero's bounds change as it flies from the starting
  /// route to the destination route.
  ///
  /// A hero flight begins with the destination hero's [child] aligned with the
98
  /// starting hero's child. The [Tween<Rect>] returned by this callback is used
99 100 101 102 103
  /// to compute the hero's bounds as the flight animation's value goes from 0.0
  /// to 1.0.
  ///
  /// If this property is null, the default, then the value of
  /// [HeroController.createRectTween] is used. The [HeroController] created by
104
  /// [MaterialApp] creates a [MaterialRectAreTween].
105 106
  final CreateRectTween createRectTween;

Hans Muller's avatar
Hans Muller committed
107
  /// The widget subtree that will "fly" from one route to another during a
108
  /// [Navigator] push or pop transition.
Ian Hickson's avatar
Ian Hickson committed
109
  ///
Hans Muller's avatar
Hans Muller committed
110 111 112 113
  /// 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.
114 115
  ///
  /// {@macro flutter.widgets.child}
Ian Hickson's avatar
Ian Hickson committed
116 117
  final Widget child;

Hans Muller's avatar
Hans Muller committed
118 119 120 121
  // 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
122 123
    void visitor(Element element) {
      if (element.widget is Hero) {
Hans Muller's avatar
Hans Muller committed
124 125 126
        final StatefulElement hero = element;
        final Hero heroWidget = element.widget;
        final Object tag = heroWidget.tag;
Hixie's avatar
Hixie committed
127
        assert(tag != null);
128
        assert(() {
129
          if (result.containsKey(tag)) {
Hans Muller's avatar
Hans Muller committed
130
            throw new FlutterError(
131
              'There are multiple heroes that share the same tag within a subtree.\n'
132
              'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
133
              'each Hero must have a unique non-null tag.\n'
134 135 136
              '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: "# ")}'
Hixie's avatar
Hixie committed
137
            );
138 139
          }
          return true;
140
        }());
141
        final _HeroState heroState = hero.state;
142
        result[tag] = heroState;
Hixie's avatar
Hixie committed
143 144 145 146 147 148 149
      }
      element.visitChildren(visitor);
    }
    context.visitChildElements(visitor);
    return result;
  }

150
  @override
151
  _HeroState createState() => new _HeroState();
152 153 154 155 156 157

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(new DiagnosticsProperty<Object>('tag', tag));
  }
Hixie's avatar
Hixie committed
158 159
}

Hans Muller's avatar
Hans Muller committed
160
class _HeroState extends State<Hero> {
161
  final GlobalKey _key = new GlobalKey();
Hixie's avatar
Hixie committed
162
  Size _placeholderSize;
Hixie's avatar
Hixie committed
163

Hans Muller's avatar
Hans Muller committed
164
  void startFlight() {
Hixie's avatar
Hixie committed
165
    assert(mounted);
Hans Muller's avatar
Hans Muller committed
166 167
    final RenderBox box = context.findRenderObject();
    assert(box != null && box.hasSize);
Hixie's avatar
Hixie committed
168
    setState(() {
Hans Muller's avatar
Hans Muller committed
169
      _placeholderSize = box.size;
Hixie's avatar
Hixie committed
170
    });
Hixie's avatar
Hixie committed
171 172
  }

Hans Muller's avatar
Hans Muller committed
173 174 175 176 177 178
  void endFlight() {
    if (mounted) {
      setState(() {
        _placeholderSize = null;
      });
    }
179 180
  }

181
  @override
Hixie's avatar
Hixie committed
182
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
183
    if (_placeholderSize != null) {
Hans Muller's avatar
Hans Muller committed
184 185 186 187
      return new SizedBox(
        width: _placeholderSize.width,
        height: _placeholderSize.height
      );
Hixie's avatar
Hixie committed
188
    }
Hixie's avatar
Hixie committed
189 190
    return new KeyedSubtree(
      key: _key,
191
      child: widget.child,
Hixie's avatar
Hixie committed
192
    );
Hixie's avatar
Hixie committed
193 194 195
  }
}

196
// Everything known about a hero flight that's to be started or diverted.
Hans Muller's avatar
Hans Muller committed
197 198 199 200 201 202 203 204 205 206
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,
207
  }) : assert(fromHero.widget.tag == toHero.widget.tag);
Hixie's avatar
Hixie committed
208

Hans Muller's avatar
Hans Muller committed
209 210 211 212 213 214 215 216
  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;
217

218
  Object get tag => fromHero.widget.tag;
219

Hans Muller's avatar
Hans Muller committed
220 221 222 223
  Animation<double> get animation {
    return new CurvedAnimation(
      parent: (type == _HeroFlightType.push) ? toRoute.animation : fromRoute.animation,
      curve: Curves.fastOutSlowIn,
Hixie's avatar
Hixie committed
224 225 226
    );
  }

Hans Muller's avatar
Hans Muller committed
227 228 229
  @override
  String toString() {
    return '_HeroFlightManifest($type hero: $tag from: ${fromRoute.settings} to: ${toRoute.settings})';
230
  }
Hans Muller's avatar
Hans Muller committed
231
}
232

Hans Muller's avatar
Hans Muller committed
233 234 235 236
// Builds the in-flight hero widget.
class _HeroFlight {
  _HeroFlight(this.onFlightEnded) {
    _proxyAnimation = new ProxyAnimation()..addStatusListener(_handleAnimationUpdate);
Hixie's avatar
Hixie committed
237
  }
238

Hans Muller's avatar
Hans Muller committed
239
  final _OnFlightEnded onFlightEnded;
Hixie's avatar
Hixie committed
240

241
  Tween<Rect> heroRect;
Hans Muller's avatar
Hans Muller committed
242 243 244 245
  Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
  ProxyAnimation _proxyAnimation;
  _HeroFlightManifest manifest;
  OverlayEntry overlayEntry;
246
  bool _aborted = false;
Hixie's avatar
Hixie committed
247

248
  Tween<Rect> _doCreateRectTween(Rect begin, Rect end) {
249 250 251
    final CreateRectTween createRectTween = manifest.toHero.widget.createRectTween ?? manifest.createRectTween;
    if (createRectTween != null)
      return createRectTween(begin, end);
Hans Muller's avatar
Hans Muller committed
252 253
    return new RectTween(begin: begin, end: end);
  }
254

Hans Muller's avatar
Hans Muller committed
255 256 257 258 259
  // The OverlayEntry WidgetBuilder callback for the hero's overlay.
  Widget _buildOverlay(BuildContext context) {
    assert(manifest != null);
    return new AnimatedBuilder(
      animation: _proxyAnimation,
260
      child: manifest.toHero.widget,
Hans Muller's avatar
Hans Muller committed
261 262
      builder: (BuildContext context, Widget child) {
        final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject();
263 264 265 266
        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
267 268 269 270 271 272
            _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
273 274 275 276 277
          // supposed to end up then recreate the heroRect tween.
          final RenderBox finalRouteBox = manifest.toRoute.subtreeContext?.findRenderObject();
          final Offset toHeroOrigin = toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
          if (toHeroOrigin != heroRect.end.topLeft) {
            final Rect heroRectEnd = toHeroOrigin & heroRect.end.size;
Hans Muller's avatar
Hans Muller committed
278 279 280 281 282 283 284 285 286 287 288 289 290
            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,
291
          child: new IgnorePointer(
Hans Muller's avatar
Hans Muller committed
292 293 294 295 296 297 298 299 300 301 302 303
            child: new RepaintBoundary(
              child: new Opacity(
                key: manifest.toHero._key,
                opacity: _heroOpacity.value,
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }
Hixie's avatar
Hixie committed
304

Hans Muller's avatar
Hans Muller committed
305 306 307
  void _handleAnimationUpdate(AnimationStatus status) {
    if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
      _proxyAnimation.parent = null;
Hixie's avatar
Hixie committed
308

Hans Muller's avatar
Hans Muller committed
309 310 311
      assert(overlayEntry != null);
      overlayEntry.remove();
      overlayEntry = null;
Hixie's avatar
Hixie committed
312

Hans Muller's avatar
Hans Muller committed
313 314 315 316
      manifest.fromHero.endFlight();
      manifest.toHero.endFlight();
      onFlightEnded(this);
    }
Hixie's avatar
Hixie committed
317 318
  }

Hans Muller's avatar
Hans Muller committed
319 320
  // The simple case: we're either starting a push or a pop animation.
  void start(_HeroFlightManifest initialManifest) {
321
    assert(!_aborted);
Hans Muller's avatar
Hans Muller committed
322 323 324 325 326 327 328 329
    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;
      }
330
    }());
Hixie's avatar
Hixie committed
331

Hans Muller's avatar
Hans Muller committed
332
    manifest = initialManifest;
333

Hans Muller's avatar
Hans Muller committed
334 335 336 337
    if (manifest.type == _HeroFlightType.pop)
      _proxyAnimation.parent = new ReverseAnimation(manifest.animation);
    else
      _proxyAnimation.parent = manifest.animation;
Hixie's avatar
Hixie committed
338

Hans Muller's avatar
Hans Muller committed
339 340
    manifest.fromHero.startFlight();
    manifest.toHero.startFlight();
Hixie's avatar
Hixie committed
341

342 343 344
    heroRect = _doCreateRectTween(
      _globalBoundingBoxFor(manifest.fromHero.context),
      _globalBoundingBoxFor(manifest.toHero.context),
Hans Muller's avatar
Hans Muller committed
345
    );
Hixie's avatar
Hixie committed
346

Hans Muller's avatar
Hans Muller committed
347 348
    overlayEntry = new OverlayEntry(builder: _buildOverlay);
    manifest.overlay.insert(overlayEntry);
Hixie's avatar
Hixie committed
349 350
  }

Hans Muller's avatar
Hans Muller committed
351 352
  // 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.
353
  void divert(_HeroFlightManifest newManifest) {
Hans Muller's avatar
Hans Muller committed
354
    assert(manifest.tag == newManifest.tag);
Hixie's avatar
Hixie committed
355

Hans Muller's avatar
Hans Muller committed
356 357 358 359 360 361 362
    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);
363

364 365 366 367 368
      // The same heroRect tween is used in reverse, rather than creating
      // a new heroRect with _doCreateRectTween(heroRect.end, heroRect.begin).
      // That's because tweens like MaterialRectArcTween may create a different
      // path for swapped begin and end parameters. We want the pop flight
      // path to be the same (in reverse) as the push flight path.
Hans Muller's avatar
Hans Muller committed
369
      _proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
370
      heroRect = new ReverseTween<Rect>(heroRect);
Hans Muller's avatar
Hans Muller committed
371 372 373 374 375 376 377 378 379 380 381 382 383 384
    } 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();
385
        heroRect = _doCreateRectTween(heroRect.end, _globalBoundingBoxFor(newManifest.toHero.context));
Hans Muller's avatar
Hans Muller committed
386
      } else {
387
        // TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
388
        heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
Hixie's avatar
Hixie committed
389
      }
Hans Muller's avatar
Hans Muller committed
390 391 392 393 394 395 396
    } 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);

397
      heroRect = _doCreateRectTween(heroRect.evaluate(_proxyAnimation), _globalBoundingBoxFor(newManifest.toHero.context));
Hans Muller's avatar
Hans Muller committed
398 399 400 401 402 403 404 405 406 407

      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
408
    }
Hans Muller's avatar
Hans Muller committed
409

410
    _aborted = false;
Hans Muller's avatar
Hans Muller committed
411
    manifest = newManifest;
Hixie's avatar
Hixie committed
412 413
  }

414 415 416 417
  void abort() {
    _aborted = true;
  }

418
  @override
Hans Muller's avatar
Hans Muller committed
419 420 421 422 423 424
  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
425
}
426

Ian Hickson's avatar
Ian Hickson committed
427 428
/// A [Navigator] observer that manages [Hero] transitions.
///
429
/// An instance of [HeroController] should be used in [Navigator.observers].
Ian Hickson's avatar
Ian Hickson committed
430
/// This is done automatically by [MaterialApp].
431
class HeroController extends NavigatorObserver {
Ian Hickson's avatar
Ian Hickson committed
432 433
  /// Creates a hero controller with the given [RectTween] constructor if any.
  ///
434
  /// The [createRectTween] argument is optional. If null, the controller uses a
435
  /// linear [Tween<Rect>].
Hans Muller's avatar
Hans Muller committed
436
  HeroController({ this.createRectTween });
437

438 439 440
  /// 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
441
  final CreateRectTween createRectTween;
442

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

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

450
  @override
Hans Muller's avatar
Hans Muller committed
451
  void didPush(Route<dynamic> to, Route<dynamic> from) {
452
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
453 454
    assert(to != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.push);
455 456
  }

457
  @override
Hans Muller's avatar
Hans Muller committed
458
  void didPop(Route<dynamic> from, Route<dynamic> to) {
459
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
460 461
    assert(from != null);
    _maybeStartHeroTransition(from, to, _HeroFlightType.pop);
462 463
  }

464 465 466 467 468 469 470 471 472 473
  @override
  void didStartUserGesture() {
    _questsEnabled = false;
  }

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

Hans Muller's avatar
Hans Muller committed
474 475 476 477 478 479 480 481
  // 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;

482
      // A user gesture may have already completed the pop.
Hans Muller's avatar
Hans Muller committed
483 484 485
      if (flightType == _HeroFlightType.pop && animation.status == AnimationStatus.dismissed)
        return;

486 487 488 489
      // 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
490

491
      WidgetsBinding.instance.addPostFrameCallback((Duration value) {
Hans Muller's avatar
Hans Muller committed
492 493
        _startHeroTransition(from, to, flightType);
      });
494 495 496
    }
  }

Hans Muller's avatar
Hans Muller committed
497
  // Find the matching pairs of heros in from and to and either start or a new
498
  // hero flight, or divert an existing one.
Hans Muller's avatar
Hans Muller committed
499
  void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {
500 501
    // 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
502
    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
503
      to.offstage = false; // in case we set this in _maybeStartHeroTransition
504 505
      return;
    }
506

507
    final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);
Hans Muller's avatar
Hans Muller committed
508 509 510 511 512

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

513
    // If the `to` route was offstage, then we're implicitly restoring its
Hans Muller's avatar
Hans Muller committed
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    // 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)
530
          _flights[tag].divert(manifest);
Hans Muller's avatar
Hans Muller committed
531 532
        else
          _flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);
533 534
      } else if (_flights[tag] != null) {
        _flights[tag].abort();
Hans Muller's avatar
Hans Muller committed
535
      }
536
    }
537 538
  }

Hans Muller's avatar
Hans Muller committed
539 540
  void _handleFlightEnded(_HeroFlight flight) {
    _flights.remove(flight.manifest.tag);
541 542
  }
}