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

5 6
// @dart = 2.8

7
import 'package:flutter/foundation.dart';
8

Hixie's avatar
Hixie committed
9
import 'basic.dart';
10
import 'binding.dart';
Hixie's avatar
Hixie committed
11
import 'framework.dart';
12 13 14
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
15
import 'routes.dart';
16
import 'ticker_provider.dart' show TickerMode;
Hixie's avatar
Hixie committed
17 18
import 'transitions.dart';

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

27 28 29 30 31 32 33 34
/// Signature for a function that builds a [Hero] placeholder widget given a
/// child and a [Size].
///
/// The child can optionally be part of the returned widget tree. The returned
/// widget should typically be constrained to [heroSize], if it doesn't do so
/// implicitly.
///
/// See also:
35
///
36 37 38 39 40 41 42 43
///  * [TransitionBuilder], which is similar but only takes a [BuildContext]
///    and a child widget.
typedef HeroPlaceholderBuilder = Widget Function(
  BuildContext context,
  Size heroSize,
  Widget child,
);

44
/// A function that lets [Hero]es self supply a [Widget] that is shown during the
45 46
/// hero's flight from one route to another instead of default (which is to
/// show the destination route's instance of the Hero).
47
typedef HeroFlightShuttleBuilder = Widget Function(
48 49 50 51 52 53 54
  BuildContext flightContext,
  Animation<double> animation,
  HeroFlightDirection flightDirection,
  BuildContext fromHeroContext,
  BuildContext toHeroContext,
);

55
typedef _OnFlightEnded = void Function(_HeroFlight flight);
Hans Muller's avatar
Hans Muller committed
56

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
/// Direction of the hero's flight based on the navigation operation.
enum HeroFlightDirection {
  /// A flight triggered by a route push.
  ///
  /// The animation goes from 0 to 1.
  ///
  /// If no custom [HeroFlightShuttleBuilder] is supplied, the top route's
  /// [Hero] child is shown in flight.
  push,

  /// A flight triggered by a route pop.
  ///
  /// The animation goes from 1 to 0.
  ///
  /// If no custom [HeroFlightShuttleBuilder] is supplied, the bottom route's
  /// [Hero] child is shown in flight.
  pop,
Hixie's avatar
Hixie committed
74 75
}

76 77 78
// The bounding box for context in ancestorContext coordinate system, or in the global
// coordinate system when null.
Rect _boundingBoxFor(BuildContext context, [BuildContext ancestorContext]) {
79
  final RenderBox box = context.findRenderObject() as RenderBox;
Hans Muller's avatar
Hans Muller committed
80
  assert(box != null && box.hasSize);
81 82 83 84
  return MatrixUtils.transformRect(
      box.getTransformTo(ancestorContext?.findRenderObject()),
      Offset.zero & box.size,
  );
Hixie's avatar
Hixie committed
85 86
}

87 88
/// A widget that marks its child as being a candidate for
/// [hero animations](https://flutter.dev/docs/development/ui/animations/hero-animations).
Ian Hickson's avatar
Ian Hickson committed
89
///
Hans Muller's avatar
Hans Muller committed
90 91 92 93 94 95
/// 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
96 97
/// overlay during the transition and while they're in-flight they're, by
/// default, not shown in their original locations in the old and new routes.
Ian Hickson's avatar
Ian Hickson committed
98
///
Hans Muller's avatar
Hans Muller committed
99 100 101 102
/// 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
103
///
Hans Muller's avatar
Hans Muller committed
104
/// If a [Hero] is already in flight when navigation occurs, its
105 106
/// flight animation will be redirected to its new destination. The
/// widget shown in-flight during the transition is, by default, the
107
/// destination route's [Hero]'s child.
Ian Hickson's avatar
Ian Hickson committed
108
///
109 110 111
/// For a Hero animation to trigger, the Hero has to exist on the very first
/// frame of the new page's animation.
///
Hans Muller's avatar
Hans Muller committed
112
/// Routes must not contain more than one [Hero] for each [tag].
Ian Hickson's avatar
Ian Hickson committed
113
///
114 115
/// {@youtube 560 315 https://www.youtube.com/watch?v=Be9UH1kXFDw}
///
Ian Hickson's avatar
Ian Hickson committed
116 117
/// ## Discussion
///
Hans Muller's avatar
Hans Muller committed
118
/// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for
Ian Hickson's avatar
Ian Hickson committed
119
/// all this to work. The top left and bottom right coordinates of each animated
Hans Muller's avatar
Hans Muller committed
120
/// Hero will be converted to global coordinates and then from there converted
Ian Hickson's avatar
Ian Hickson committed
121 122 123 124 125 126 127
/// 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*
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
/// is, by default, 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. If a
/// [flightShuttleBuilder] is supplied, its output widget is shown during the
/// flight transition instead.
///
/// By default, both route A and route B's heroes are hidden while the
/// transitioning widget is animating in-flight above the 2 routes.
/// [placeholderBuilder] can be used to show a custom widget in their place
/// instead once the transition has taken flight.
///
/// During the transition, the transition 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, by default, placed over where route B's
/// hero's widget was, and then the animation goes the other way.
///
143 144
/// ### Nested Navigators
///
145
/// If either or both routes contain nested [Navigator]s, only [Hero]es
146 147
/// contained in the top-most routes (as defined by [Route.isCurrent]) *of those
/// nested [Navigator]s* are considered for animation. Just like in the
148
/// non-nested case the top-most routes containing these [Hero]es in the nested
149 150
/// [Navigator]s have to be [PageRoute]s.
///
151 152 153
/// ## Parts of a Hero Transition
///
/// ![Diagrams with parts of the Hero transition.](https://flutter.github.io/assets-for-api-docs/assets/interaction/heroes.png)
154
class Hero extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
155 156
  /// Create a hero.
  ///
Hans Muller's avatar
Hans Muller committed
157
  /// The [tag] and [child] parameters must not be null.
158
  /// The [child] parameter and all of the its descendants must not be [Hero]es.
159
  const Hero({
Hixie's avatar
Hixie committed
160
    Key key,
Ian Hickson's avatar
Ian Hickson committed
161
    @required this.tag,
162
    this.createRectTween,
163 164
    this.flightShuttleBuilder,
    this.placeholderBuilder,
xster's avatar
xster committed
165
    this.transitionOnUserGestures = false,
Ian Hickson's avatar
Ian Hickson committed
166
    @required this.child,
167
  }) : assert(tag != null),
xster's avatar
xster committed
168
       assert(transitionOnUserGestures != null),
169 170
       assert(child != null),
       super(key: key);
Hixie's avatar
Hixie committed
171

Ian Hickson's avatar
Ian Hickson committed
172
  /// The identifier for this particular hero. If the tag of this hero matches
Hans Muller's avatar
Hans Muller committed
173 174
  /// 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
175
  final Object tag;
176

177 178 179 180
  /// 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
181
  /// starting hero's child. The [Tween<Rect>] returned by this callback is used
182 183 184 185 186
  /// 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
187
  /// [MaterialApp] creates a [MaterialRectArcTween].
188 189
  final CreateRectTween createRectTween;

Hans Muller's avatar
Hans Muller committed
190
  /// The widget subtree that will "fly" from one route to another during a
191
  /// [Navigator] push or pop transition.
Ian Hickson's avatar
Ian Hickson committed
192
  ///
Hans Muller's avatar
Hans Muller committed
193 194 195 196
  /// 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.
197 198
  ///
  /// {@macro flutter.widgets.child}
Ian Hickson's avatar
Ian Hickson committed
199 200
  final Widget child;

201 202 203 204 205 206
  /// Optional override to supply a widget that's shown during the hero's flight.
  ///
  /// This in-flight widget can depend on the route transition's animation as
  /// well as the incoming and outgoing routes' [Hero] descendants' widgets and
  /// layout.
  ///
207
  /// When both the source and destination [Hero]es provide a [flightShuttleBuilder],
208 209 210 211
  /// the destination's [flightShuttleBuilder] takes precedence.
  ///
  /// If none is provided, the destination route's Hero child is shown in-flight
  /// by default.
212 213 214 215 216 217 218 219 220 221 222 223 224 225
  ///
  /// ## Limitations
  ///
  /// If a widget built by [flightShuttleBuilder] takes part in a [Navigator]
  /// push transition, that widget or its descendants must not have any
  /// [GlobalKey] that is used in the source Hero's descendant widgets. That is
  /// because both subtrees will be included in the widget tree during the Hero
  /// flight animation, and [GlobalKey]s must be unique across the entire widget
  /// tree.
  ///
  /// If the said [GlobalKey] is essential to your application, consider providing
  /// a custom [placeholderBuilder] for the source Hero, to avoid the [GlobalKey]
  /// collision, such as a builder that builds an empty [SizedBox], keeping the
  /// Hero [child]'s original size.
226 227
  final HeroFlightShuttleBuilder flightShuttleBuilder;

228 229
  /// Placeholder widget left in place as the Hero's [child] once the flight takes
  /// off.
230
  ///
231 232 233 234 235
  /// By default the placeholder widget is an empty [SizedBox] keeping the Hero
  /// child's original size, unless this Hero is a source Hero of a [Navigator]
  /// push transition, in which case [child] will be a descendant of the placeholder
  /// and will be kept [Offstage] during the Hero's flight.
  final HeroPlaceholderBuilder placeholderBuilder;
236

xster's avatar
xster committed
237 238 239
  /// Whether to perform the hero transition if the [PageRoute] transition was
  /// triggered by a user gesture, such as a back swipe on iOS.
  ///
240
  /// If [Hero]es with the same [tag] on both the from and the to routes have
xster's avatar
xster committed
241 242 243 244 245 246 247 248 249 250 251
  /// [transitionOnUserGestures] set to true, a back swipe gesture will
  /// trigger the same hero animation as a programmatically triggered push or
  /// pop.
  ///
  /// The route being popped to or the bottom route must also have
  /// [PageRoute.maintainState] set to true for a gesture triggered hero
  /// transition to work.
  ///
  /// Defaults to false and cannot be null.
  final bool transitionOnUserGestures;

252 253 254 255
  // Returns a map of all of the heroes in `context` indexed by hero tag that
  // should be considered for animation when `navigator` transitions from one
  // PageRoute to another.
  static Map<Object, _HeroState> _allHeroesFor(
256 257 258
    BuildContext context,
    bool isUserGestureTransition,
    NavigatorState navigator,
259
  ) {
Hans Muller's avatar
Hans Muller committed
260
    assert(context != null);
xster's avatar
xster committed
261
    assert(isUserGestureTransition != null);
262
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
263
    final Map<Object, _HeroState> result = <Object, _HeroState>{};
264

265
    void inviteHero(StatefulElement hero, Object tag) {
266 267
      assert(() {
        if (result.containsKey(tag)) {
268 269 270 271 272 273 274 275 276
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('There are multiple heroes that share the same tag within a subtree.'),
            ErrorDescription(
              'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), '
              'each Hero must have a unique non-null tag.\n'
              'In this case, multiple heroes had the following tag: $tag\n'
            ),
            DiagnosticsProperty<StatefulElement>('Here is the subtree for one of the offending heroes', hero, linePrefix: '# ', style: DiagnosticsTreeStyle.dense),
          ]);
277 278 279
        }
        return true;
      }());
280 281
      final Hero heroWidget = hero.widget as Hero;
      final _HeroState heroState = hero.state as _HeroState;
282 283 284 285 286 287 288
      if (!isUserGestureTransition || heroWidget.transitionOnUserGestures) {
        result[tag] = heroState;
      } else {
        // If transition is not allowed, we need to make sure hero is not hidden.
        // A hero can be hidden previously due to hero transition.
        heroState.ensurePlaceholderIsHidden();
      }
289 290
    }

Hixie's avatar
Hixie committed
291
    void visitor(Element element) {
292 293 294 295
      final Widget widget = element.widget;
      if (widget is Hero) {
        final StatefulElement hero = element as StatefulElement;
        final Object tag = widget.tag;
296 297 298 299 300 301 302 303 304 305 306 307
        assert(tag != null);
        if (Navigator.of(hero) == navigator) {
          inviteHero(hero, tag);
        } else {
          // The nearest navigator to the Hero is not the Navigator that is
          // currently transitioning from one route to another. This means
          // the Hero is inside a nested Navigator and should only be
          // considered for animation if it is part of the top-most route in
          // that nested Navigator and if that route is also a PageRoute.
          final ModalRoute<dynamic> heroRoute = ModalRoute.of(hero);
          if (heroRoute != null && heroRoute is PageRoute && heroRoute.isCurrent) {
            inviteHero(hero, tag);
308
          }
xster's avatar
xster committed
309
        }
Hixie's avatar
Hixie committed
310 311 312
      }
      element.visitChildren(visitor);
    }
313

Hixie's avatar
Hixie committed
314 315 316 317
    context.visitChildElements(visitor);
    return result;
  }

318
  @override
319
  _HeroState createState() => _HeroState();
320 321

  @override
322 323
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
324
    properties.add(DiagnosticsProperty<Object>('tag', tag));
325
  }
Hixie's avatar
Hixie committed
326 327
}

Hans Muller's avatar
Hans Muller committed
328
class _HeroState extends State<Hero> {
329
  final GlobalKey _key = GlobalKey();
Hixie's avatar
Hixie committed
330
  Size _placeholderSize;
331 332 333 334 335 336 337 338 339 340 341 342
  // Whether the placeholder widget should wrap the hero's child widget as its
  // own child, when `_placeholderSize` is non-null (i.e. the hero is currently
  // in its flight animation). See `startFlight`.
  bool _shouldIncludeChild = true;

  // The `shouldIncludeChildInPlaceholder` flag dictates if the child widget of
  // this hero should be included in the placeholder widget as a descendant.
  //
  // When a new hero flight animation takes place, a placeholder widget
  // needs to be built to replace the original hero widget. When
  // `shouldIncludeChildInPlaceholder` is set to true and `widget.placeholderBuilder`
  // is null, the placeholder widget will include the original hero's child
343
  // widget as a descendant, allowing the original element tree to be preserved.
344 345 346 347 348
  //
  // It is typically set to true for the *from* hero in a push transition,
  // and false otherwise.
  void startFlight({ bool shouldIncludedChildInPlaceholder = false }) {
    _shouldIncludeChild = shouldIncludedChildInPlaceholder;
Hixie's avatar
Hixie committed
349
    assert(mounted);
350
    final RenderBox box = context.findRenderObject() as RenderBox;
Hans Muller's avatar
Hans Muller committed
351
    assert(box != null && box.hasSize);
Hixie's avatar
Hixie committed
352
    setState(() {
Hans Muller's avatar
Hans Muller committed
353
      _placeholderSize = box.size;
Hixie's avatar
Hixie committed
354
    });
Hixie's avatar
Hixie committed
355 356
  }

357
  void ensurePlaceholderIsHidden() {
Hans Muller's avatar
Hans Muller committed
358 359 360 361 362
    if (mounted) {
      setState(() {
        _placeholderSize = null;
      });
    }
363 364
  }

365
  // When `keepPlaceholder` is true, the placeholder will continue to be shown
366 367
  // after the flight ends. Otherwise the child of the Hero will become visible
  // and its TickerMode will be re-enabled.
368 369 370 371 372 373
  void endFlight({ bool keepPlaceholder = false }) {
    if (!keepPlaceholder) {
      ensurePlaceholderIsHidden();
    }
  }

374
  @override
Hixie's avatar
Hixie committed
375
  Widget build(BuildContext context) {
376
    assert(
377
      context.findAncestorWidgetOfExactType<Hero>() == null,
378 379 380
      'A Hero widget cannot be the descendant of another Hero widget.'
    );

381
    final bool showPlaceholder = _placeholderSize != null;
382

383
    if (showPlaceholder && widget.placeholderBuilder != null) {
384
      return widget.placeholderBuilder(context, _placeholderSize, widget.child);
Hixie's avatar
Hixie committed
385
    }
386

387
    if (showPlaceholder && !_shouldIncludeChild) {
388 389 390 391 392 393 394 395 396 397
      return SizedBox(
        width: _placeholderSize.width,
        height: _placeholderSize.height,
      );
    }

    return SizedBox(
      width: _placeholderSize?.width,
      height: _placeholderSize?.height,
      child: Offstage(
398
        offstage: showPlaceholder,
399
        child: TickerMode(
400
          enabled: !showPlaceholder,
401
          child: KeyedSubtree(key: _key, child: widget.child),
402
        ),
403
      ),
Hixie's avatar
Hixie committed
404
    );
Hixie's avatar
Hixie committed
405 406 407
  }
}

408
// Everything known about a hero flight that's to be started or diverted.
Hans Muller's avatar
Hans Muller committed
409 410 411 412 413 414 415 416 417 418
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,
419
    @required this.shuttleBuilder,
xster's avatar
xster committed
420
    @required this.isUserGestureTransition,
421
    @required this.isDiverted,
422
  }) : assert(fromHero.widget.tag == toHero.widget.tag);
Hixie's avatar
Hixie committed
423

424
  final HeroFlightDirection type;
Hans Muller's avatar
Hans Muller committed
425 426 427 428 429 430 431
  final OverlayState overlay;
  final Rect navigatorRect;
  final PageRoute<dynamic> fromRoute;
  final PageRoute<dynamic> toRoute;
  final _HeroState fromHero;
  final _HeroState toHero;
  final CreateRectTween createRectTween;
432
  final HeroFlightShuttleBuilder shuttleBuilder;
xster's avatar
xster committed
433
  final bool isUserGestureTransition;
434
  final bool isDiverted;
435

436
  Object get tag => fromHero.widget.tag;
437

Hans Muller's avatar
Hans Muller committed
438
  Animation<double> get animation {
439
    return CurvedAnimation(
440
      parent: (type == HeroFlightDirection.push) ? toRoute.animation : fromRoute.animation,
Hans Muller's avatar
Hans Muller committed
441
      curve: Curves.fastOutSlowIn,
442
      reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped,
Hixie's avatar
Hixie committed
443 444 445
    );
  }

Hans Muller's avatar
Hans Muller committed
446 447
  @override
  String toString() {
448 449
    return '_HeroFlightManifest($type tag: $tag from route: ${fromRoute.settings} '
        'to route: ${toRoute.settings} with hero: $fromHero to $toHero)';
450
  }
Hans Muller's avatar
Hans Muller committed
451
}
452

Hans Muller's avatar
Hans Muller committed
453 454 455
// Builds the in-flight hero widget.
class _HeroFlight {
  _HeroFlight(this.onFlightEnded) {
456
    _proxyAnimation = ProxyAnimation()..addStatusListener(_handleAnimationUpdate);
Hixie's avatar
Hixie committed
457
  }
458

Hans Muller's avatar
Hans Muller committed
459
  final _OnFlightEnded onFlightEnded;
Hixie's avatar
Hixie committed
460

461 462 463
  Tween<Rect> heroRectTween;
  Widget shuttle;

Hans Muller's avatar
Hans Muller committed
464 465 466 467
  Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
  ProxyAnimation _proxyAnimation;
  _HeroFlightManifest manifest;
  OverlayEntry overlayEntry;
468
  bool _aborted = false;
Hixie's avatar
Hixie committed
469

470
  Tween<Rect> _doCreateRectTween(Rect begin, Rect end) {
471 472 473
    final CreateRectTween createRectTween = manifest.toHero.widget.createRectTween ?? manifest.createRectTween;
    if (createRectTween != null)
      return createRectTween(begin, end);
474
    return RectTween(begin: begin, end: end);
Hans Muller's avatar
Hans Muller committed
475
  }
476

477 478
  static final Animatable<double> _reverseTween = Tween<double>(begin: 1.0, end: 0.0);

Hans Muller's avatar
Hans Muller committed
479 480 481
  // The OverlayEntry WidgetBuilder callback for the hero's overlay.
  Widget _buildOverlay(BuildContext context) {
    assert(manifest != null);
482 483 484 485 486 487 488 489 490
    shuttle ??= manifest.shuttleBuilder(
      context,
      manifest.animation,
      manifest.type,
      manifest.fromHero.context,
      manifest.toHero.context,
    );
    assert(shuttle != null);

491
    return AnimatedBuilder(
Hans Muller's avatar
Hans Muller committed
492
      animation: _proxyAnimation,
493
      child: shuttle,
Hans Muller's avatar
Hans Muller committed
494
      builder: (BuildContext context, Widget child) {
495
        final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject() as RenderBox;
496 497 498 499
        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) {
500 501 502
            _heroOpacity = _proxyAnimation.drive(
              _reverseTween.chain(CurveTween(curve: Interval(_proxyAnimation.value, 1.0))),
            );
Hans Muller's avatar
Hans Muller committed
503 504 505
          }
        } else if (toHeroBox.hasSize) {
          // The toHero has been laid out. If it's no longer where the hero animation is
506
          // supposed to end up then recreate the heroRect tween.
507
          final RenderBox finalRouteBox = manifest.toRoute.subtreeContext?.findRenderObject() as RenderBox;
508
          final Offset toHeroOrigin = toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
509 510 511
          if (toHeroOrigin != heroRectTween.end.topLeft) {
            final Rect heroRectEnd = toHeroOrigin & heroRectTween.end.size;
            heroRectTween = _doCreateRectTween(heroRectTween.begin, heroRectEnd);
Hans Muller's avatar
Hans Muller committed
512 513 514
          }
        }

515
        final Rect rect = heroRectTween.evaluate(_proxyAnimation);
Hans Muller's avatar
Hans Muller committed
516
        final Size size = manifest.navigatorRect.size;
517
        final RelativeRect offsets = RelativeRect.fromSize(rect, size);
Hans Muller's avatar
Hans Muller committed
518

519
        return Positioned(
Hans Muller's avatar
Hans Muller committed
520 521 522 523
          top: offsets.top,
          right: offsets.right,
          bottom: offsets.bottom,
          left: offsets.left,
524 525 526
          child: IgnorePointer(
            child: RepaintBoundary(
              child: Opacity(
Hans Muller's avatar
Hans Muller committed
527 528 529 530 531 532 533 534 535
                opacity: _heroOpacity.value,
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }
Hixie's avatar
Hixie committed
536

Hans Muller's avatar
Hans Muller committed
537
  void _handleAnimationUpdate(AnimationStatus status) {
538 539 540 541 542 543 544 545
    // The animation will not finish until the user lifts their finger, so we
    // should ignore the status update if the gesture is in progress.
    //
    // This also relies on the animation to update its status at the end of the
    // gesture. See the _CupertinoBackGestureController.dragEnd for how
    // cupertino page route achieves that.
    if (manifest.fromRoute?.navigator?.userGestureInProgress == true)
      return;
Hans Muller's avatar
Hans Muller committed
546 547
    if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
      _proxyAnimation.parent = null;
Hixie's avatar
Hixie committed
548

Hans Muller's avatar
Hans Muller committed
549 550 551
      assert(overlayEntry != null);
      overlayEntry.remove();
      overlayEntry = null;
552 553 554 555 556 557 558
      // We want to keep the hero underneath the current page hidden. If
      // [AnimationStatus.completed], toHero will be the one on top and we keep
      // fromHero hidden. If [AnimationStatus.dismissed], the animation is
      // triggered but canceled before it finishes. In this case, we keep toHero
      // hidden instead.
      manifest.fromHero.endFlight(keepPlaceholder: status == AnimationStatus.completed);
      manifest.toHero.endFlight(keepPlaceholder: status == AnimationStatus.dismissed);
Hans Muller's avatar
Hans Muller committed
559 560
      onFlightEnded(this);
    }
Hixie's avatar
Hixie committed
561 562
  }

Hans Muller's avatar
Hans Muller committed
563 564
  // The simple case: we're either starting a push or a pop animation.
  void start(_HeroFlightManifest initialManifest) {
565
    assert(!_aborted);
Hans Muller's avatar
Hans Muller committed
566 567
    assert(() {
      final Animation<double> initial = initialManifest.animation;
568
      assert(initial != null);
569
      final HeroFlightDirection type = initialManifest.type;
570 571
      assert(type != null);
      switch (type) {
572
        case HeroFlightDirection.pop:
xster's avatar
xster committed
573 574 575 576 577 578
          return initial.value == 1.0 && initialManifest.isUserGestureTransition
              // During user gesture transitions, the animation controller isn't
              // driving the reverse transition, but should still be in a previously
              // completed stage with the initial value at 1.0.
              ? initial.status == AnimationStatus.completed
              : initial.status == AnimationStatus.reverse;
579
        case HeroFlightDirection.push:
Hans Muller's avatar
Hans Muller committed
580 581
          return initial.value == 0.0 && initial.status == AnimationStatus.forward;
      }
582
      return null;
583
    }());
Hixie's avatar
Hixie committed
584

Hans Muller's avatar
Hans Muller committed
585
    manifest = initialManifest;
586

587
    if (manifest.type == HeroFlightDirection.pop)
588
      _proxyAnimation.parent = ReverseAnimation(manifest.animation);
Hans Muller's avatar
Hans Muller committed
589 590
    else
      _proxyAnimation.parent = manifest.animation;
Hixie's avatar
Hixie committed
591

592
    manifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: manifest.type == HeroFlightDirection.push);
Hans Muller's avatar
Hans Muller committed
593
    manifest.toHero.startFlight();
Hixie's avatar
Hixie committed
594

595
    heroRectTween = _doCreateRectTween(
596 597
      _boundingBoxFor(manifest.fromHero.context, manifest.fromRoute.subtreeContext),
      _boundingBoxFor(manifest.toHero.context, manifest.toRoute.subtreeContext),
Hans Muller's avatar
Hans Muller committed
598
    );
Hixie's avatar
Hixie committed
599

600
    overlayEntry = OverlayEntry(builder: _buildOverlay);
Hans Muller's avatar
Hans Muller committed
601
    manifest.overlay.insert(overlayEntry);
Hixie's avatar
Hixie committed
602 603
  }

Hans Muller's avatar
Hans Muller committed
604 605
  // 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.
606
  void divert(_HeroFlightManifest newManifest) {
Hans Muller's avatar
Hans Muller committed
607
    assert(manifest.tag == newManifest.tag);
608
    if (manifest.type == HeroFlightDirection.push && newManifest.type == HeroFlightDirection.pop) {
Hans Muller's avatar
Hans Muller committed
609 610 611 612 613 614
      // 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);
615

616 617 618 619 620
      // 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.
621 622
      _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
      heroRectTween = ReverseTween<Rect>(heroRectTween);
623
    } else if (manifest.type == HeroFlightDirection.pop && newManifest.type == HeroFlightDirection.push) {
Hans Muller's avatar
Hans Muller committed
624 625 626 627 628
      // A pop flight was interrupted by a push.
      assert(newManifest.animation.status == AnimationStatus.forward);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.toRoute == newManifest.fromRoute);

629 630 631 632 633 634
      _proxyAnimation.parent = newManifest.animation.drive(
        Tween<double>(
          begin: manifest.animation.value,
          end: 1.0,
        ),
      );
Hans Muller's avatar
Hans Muller committed
635
      if (manifest.fromHero != newManifest.toHero) {
636
        manifest.fromHero.endFlight(keepPlaceholder: true);
Hans Muller's avatar
Hans Muller committed
637
        newManifest.toHero.startFlight();
638 639 640 641
        heroRectTween = _doCreateRectTween(
            heroRectTween.end,
            _boundingBoxFor(newManifest.toHero.context, newManifest.toRoute.subtreeContext),
        );
Hans Muller's avatar
Hans Muller committed
642
      } else {
643
        // TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
644
        heroRectTween = _doCreateRectTween(heroRectTween.end, heroRectTween.begin);
Hixie's avatar
Hixie committed
645
      }
Hans Muller's avatar
Hans Muller committed
646 647 648 649 650 651 652
    } 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);

653 654 655 656
      heroRectTween = _doCreateRectTween(
          heroRectTween.evaluate(_proxyAnimation),
          _boundingBoxFor(newManifest.toHero.context, newManifest.toRoute.subtreeContext),
      );
657
      shuttle = null;
Hans Muller's avatar
Hans Muller committed
658

659
      if (newManifest.type == HeroFlightDirection.pop)
660
        _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
Hans Muller's avatar
Hans Muller committed
661 662 663
      else
        _proxyAnimation.parent = newManifest.animation;

664 665
      manifest.fromHero.endFlight(keepPlaceholder: true);
      manifest.toHero.endFlight(keepPlaceholder: true);
666 667

      // Let the heroes in each of the routes rebuild with their placeholders.
668
      newManifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: newManifest.type == HeroFlightDirection.push);
Hans Muller's avatar
Hans Muller committed
669
      newManifest.toHero.startFlight();
670 671 672 673

      // Let the transition overlay on top of the routes also rebuild since
      // we cleared the old shuttle.
      overlayEntry.markNeedsBuild();
Hixie's avatar
Hixie committed
674
    }
Hans Muller's avatar
Hans Muller committed
675

676
    _aborted = false;
Hans Muller's avatar
Hans Muller committed
677
    manifest = newManifest;
Hixie's avatar
Hixie committed
678 679
  }

680 681 682 683
  void abort() {
    _aborted = true;
  }

684
  @override
Hans Muller's avatar
Hans Muller committed
685 686 687 688 689 690
  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
691
}
692

Ian Hickson's avatar
Ian Hickson committed
693 694
/// A [Navigator] observer that manages [Hero] transitions.
///
695
/// An instance of [HeroController] should be used in [Navigator.observers].
Ian Hickson's avatar
Ian Hickson committed
696
/// This is done automatically by [MaterialApp].
697
class HeroController extends NavigatorObserver {
Ian Hickson's avatar
Ian Hickson committed
698 699
  /// Creates a hero controller with the given [RectTween] constructor if any.
  ///
700
  /// The [createRectTween] argument is optional. If null, the controller uses a
701
  /// linear [Tween<Rect>].
Hans Muller's avatar
Hans Muller committed
702
  HeroController({ this.createRectTween });
703

xster's avatar
xster committed
704
  /// Used to create [RectTween]s that interpolate the position of heroes in flight.
705 706
  ///
  /// If null, the controller uses a linear [RectTween].
Hans Muller's avatar
Hans Muller committed
707
  final CreateRectTween createRectTween;
708

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

713
  @override
714
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
715
    assert(navigator != null);
716
    assert(route != null);
xster's avatar
xster committed
717
    _maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push, false);
718 719
  }

720
  @override
721
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
722
    assert(navigator != null);
723
    assert(route != null);
724 725 726 727
    // Don't trigger another flight when a pop is committed as a user gesture
    // back swipe is snapped.
    if (!navigator.userGestureInProgress)
      _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, false);
728 729
  }

730 731 732 733 734 735 736 737 738
  @override
  void didReplace({ Route<dynamic> newRoute, Route<dynamic> oldRoute }) {
    assert(navigator != null);
    if (newRoute?.isCurrent == true) {
      // Only run hero animations if the top-most route got replaced.
      _maybeStartHeroTransition(oldRoute, newRoute, HeroFlightDirection.push, false);
    }
  }

739
  @override
xster's avatar
xster committed
740 741 742 743
  void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) {
    assert(navigator != null);
    assert(route != null);
    _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, true);
744 745
  }

746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
  @override
  void didStopUserGesture() {
    if (navigator.userGestureInProgress)
      return;

    // If the user horizontal drag gesture initiated the flight (i.e. the back swipe)
    // didn't move towards the pop direction at all, the animation will not play
    // and thus the status update callback _handleAnimationUpdate will never be
    // called when the gesture finishes. In this case the initiated flight needs
    // to be manually invalidated.
    bool isInvalidFlight(_HeroFlight flight) {
      return flight.manifest.isUserGestureTransition
          && flight.manifest.type == HeroFlightDirection.pop
          && flight._proxyAnimation.isDismissed;
    }

    final List<_HeroFlight> invalidFlights = _flights.values
      .where(isInvalidFlight)
      .toList(growable: false);

    // Treat these invalidated flights as dismissed. Calling _handleAnimationUpdate
    // will also remove the flight from _flights.
768
    for (final _HeroFlight flight in invalidFlights) {
769 770 771 772
      flight._handleAnimationUpdate(AnimationStatus.dismissed);
    }
  }

Hans Muller's avatar
Hans Muller committed
773 774
  // 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.
xster's avatar
xster committed
775 776 777 778 779 780 781
  void _maybeStartHeroTransition(
    Route<dynamic> fromRoute,
    Route<dynamic> toRoute,
    HeroFlightDirection flightType,
    bool isUserGestureTransition,
  ) {
    if (toRoute != fromRoute && toRoute is PageRoute<dynamic> && fromRoute is PageRoute<dynamic>) {
Hans Muller's avatar
Hans Muller committed
782 783
      final PageRoute<dynamic> from = fromRoute;
      final PageRoute<dynamic> to = toRoute;
784
      final Animation<double> animation = (flightType == HeroFlightDirection.push) ? to.animation : from.animation;
Hans Muller's avatar
Hans Muller committed
785

786 787 788 789 790 791 792 793 794 795 796 797
      // A user gesture may have already completed the pop, or we might be the initial route
      switch (flightType) {
        case HeroFlightDirection.pop:
          if (animation.value == 0.0) {
            return;
          }
          break;
        case HeroFlightDirection.push:
          if (animation.value == 1.0) {
            return;
          }
          break;
xster's avatar
xster committed
798
      }
Hans Muller's avatar
Hans Muller committed
799

xster's avatar
xster committed
800 801 802 803 804 805 806 807
      // For pop transitions driven by a user gesture: if the "to" page has
      // maintainState = true, then the hero's final dimensions can be measured
      // immediately because their page's layout is still valid.
      if (isUserGestureTransition && flightType == HeroFlightDirection.pop && to.maintainState) {
        _startHeroTransition(from, to, animation, flightType, isUserGestureTransition);
      } else {
        // Otherwise, delay measuring until the end of the next frame to allow
        // the 'to' route to build and layout.
Hans Muller's avatar
Hans Muller committed
808

xster's avatar
xster committed
809 810 811 812 813 814 815 816 817
        // 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;

        WidgetsBinding.instance.addPostFrameCallback((Duration value) {
          _startHeroTransition(from, to, animation, flightType, isUserGestureTransition);
        });
      }
818 819 820
    }
  }

xster's avatar
xster committed
821
  // Find the matching pairs of heroes in from and to and either start or a new
822
  // hero flight, or divert an existing one.
823 824 825 826 827
  void _startHeroTransition(
    PageRoute<dynamic> from,
    PageRoute<dynamic> to,
    Animation<double> animation,
    HeroFlightDirection flightType,
xster's avatar
xster committed
828
    bool isUserGestureTransition,
829
  ) {
830 831
    // 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
832
    if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
833
      to.offstage = false; // in case we set this in _maybeStartHeroTransition
834 835
      return;
    }
836

837
    final Rect navigatorRect = _boundingBoxFor(navigator.context);
Hans Muller's avatar
Hans Muller committed
838 839

    // At this point the toHeroes may have been built and laid out for the first time.
840 841
    final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext, isUserGestureTransition, navigator);
    final Map<Object, _HeroState> toHeroes = Hero._allHeroesFor(to.subtreeContext, isUserGestureTransition, navigator);
Hans Muller's avatar
Hans Muller committed
842

843
    // If the `to` route was offstage, then we're implicitly restoring its
Hans Muller's avatar
Hans Muller committed
844 845 846
    // animation value back to what it was before it was "moved" offstage.
    to.offstage = false;

847
    for (final Object tag in fromHeroes.keys) {
Hans Muller's avatar
Hans Muller committed
848
      if (toHeroes[tag] != null) {
849 850
        final HeroFlightShuttleBuilder fromShuttleBuilder = fromHeroes[tag].widget.flightShuttleBuilder;
        final HeroFlightShuttleBuilder toShuttleBuilder = toHeroes[tag].widget.flightShuttleBuilder;
851
        final bool isDiverted = _flights[tag] != null;
852

853
        final _HeroFlightManifest manifest = _HeroFlightManifest(
Hans Muller's avatar
Hans Muller committed
854 855 856 857 858 859 860 861
          type: flightType,
          overlay: navigator.overlay,
          navigatorRect: navigatorRect,
          fromRoute: from,
          toRoute: to,
          fromHero: fromHeroes[tag],
          toHero: toHeroes[tag],
          createRectTween: createRectTween,
862 863
          shuttleBuilder:
              toShuttleBuilder ?? fromShuttleBuilder ?? _defaultHeroFlightShuttleBuilder,
xster's avatar
xster committed
864
          isUserGestureTransition: isUserGestureTransition,
865
          isDiverted: isDiverted,
Hans Muller's avatar
Hans Muller committed
866
        );
867

868
        if (isDiverted)
869
          _flights[tag].divert(manifest);
Hans Muller's avatar
Hans Muller committed
870
        else
871
          _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);
872 873
      } else if (_flights[tag] != null) {
        _flights[tag].abort();
Hans Muller's avatar
Hans Muller committed
874
      }
875
    }
876 877 878

    // If the from hero is gone, the flight won't start and the to hero needs to
    // be put on stage again.
879
    for (final Object tag in toHeroes.keys) {
880 881 882
      if (fromHeroes[tag] == null)
        toHeroes[tag].ensurePlaceholderIsHidden();
    }
883 884
  }

Hans Muller's avatar
Hans Muller committed
885 886
  void _handleFlightEnded(_HeroFlight flight) {
    _flights.remove(flight.manifest.tag);
887
  }
888 889 890 891 892 893 894 895

  static final HeroFlightShuttleBuilder _defaultHeroFlightShuttleBuilder = (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
896
    final Hero toHero = toHeroContext.widget as Hero;
897 898
    return toHero.child;
  };
899
}