heroes.dart 42.2 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
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';
13
import 'routes.dart';
14
import 'ticker_provider.dart' show TickerMode;
Hixie's avatar
Hixie committed
15 16
import 'transitions.dart';

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

25 26 27 28 29 30 31 32
/// 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:
33
///
34 35 36 37 38 39 40 41
///  * [TransitionBuilder], which is similar but only takes a [BuildContext]
///    and a child widget.
typedef HeroPlaceholderBuilder = Widget Function(
  BuildContext context,
  Size heroSize,
  Widget child,
);

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

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

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
/// 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
72 73 74
}


75 76
/// 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
77
///
Hans Muller's avatar
Hans Muller committed
78 79 80 81 82 83
/// 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
84 85
/// 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
86
///
Hans Muller's avatar
Hans Muller committed
87 88 89 90
/// 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
91
///
Hans Muller's avatar
Hans Muller committed
92
/// If a [Hero] is already in flight when navigation occurs, its
93 94
/// flight animation will be redirected to its new destination. The
/// widget shown in-flight during the transition is, by default, the
95
/// destination route's [Hero]'s child.
Ian Hickson's avatar
Ian Hickson committed
96
///
97 98 99
/// 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
100
/// Routes must not contain more than one [Hero] for each [tag].
Ian Hickson's avatar
Ian Hickson committed
101
///
102 103
/// {@youtube 560 315 https://www.youtube.com/watch?v=Be9UH1kXFDw}
///
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
/// {@tool dartpad --template=stateless_widget_material}
/// This sample shows a [Hero] used within a [ListTile].
///
/// Tapping on the Hero-wrapped rectangle triggers a hero
/// animation as a new [MaterialPageRoute] is pushed. Both the size
/// and location of the rectangle animates.
///
/// Both widgets use the same [Hero.tag].
///
/// The Hero widget uses the matching tags to identify and execute this
/// animation.
///
/// ```dart
///  Widget build(BuildContext context) {
///    return Column(
///      crossAxisAlignment: CrossAxisAlignment.start,
///      children: <Widget>[
121
///        const SizedBox(
122 123 124 125 126 127 128 129
///          height: 20.0,
///        ),
///        ListTile(
///          leading: Hero(
///            tag: 'hero-rectangle',
///            child: _blueRectangle(Size(50,50)),
///          ),
///          onTap: () => _gotoDetailsPage(context),
130
///          title: const Text('Tap on the icon to view hero animation transition.'),
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
///        ),
///      ],
///    );
///  }
///
///  Widget _blueRectangle(Size size) {
///   return Container(
///     width: size.width,
///     height: size.height,
///     color: Colors.blue,
///    );
///  }
///
///  void _gotoDetailsPage(BuildContext context) {
///    Navigator.of(context).push(MaterialPageRoute(
///      builder: (BuildContext context) => Scaffold(
///        appBar: AppBar(
148
///          title: const Text('second Page'),
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
///        ),
///        body: Center(
///          child: Column(
///            mainAxisAlignment: MainAxisAlignment.center,
///            children: <Widget>[
///              Hero(
///                tag: 'hero-rectangle',
///                child: _blueRectangle(Size(200,200)),
///              ),
///            ],
///          ),
///        ),
///      ),
///    ));
///  }
///
/// ```
/// {@end-tool}
///
Ian Hickson's avatar
Ian Hickson committed
168 169
/// ## Discussion
///
Hans Muller's avatar
Hans Muller committed
170
/// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for
Ian Hickson's avatar
Ian Hickson committed
171
/// all this to work. The top left and bottom right coordinates of each animated
Hans Muller's avatar
Hans Muller committed
172
/// Hero will be converted to global coordinates and then from there converted
Ian Hickson's avatar
Ian Hickson committed
173 174 175 176 177 178 179
/// 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*
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
/// 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.
///
195 196
/// ### Nested Navigators
///
197
/// If either or both routes contain nested [Navigator]s, only [Hero]es
198 199
/// contained in the top-most routes (as defined by [Route.isCurrent]) *of those
/// nested [Navigator]s* are considered for animation. Just like in the
200
/// non-nested case the top-most routes containing these [Hero]es in the nested
201 202
/// [Navigator]s have to be [PageRoute]s.
///
203 204 205
/// ## Parts of a Hero Transition
///
/// ![Diagrams with parts of the Hero transition.](https://flutter.github.io/assets-for-api-docs/assets/interaction/heroes.png)
206
class Hero extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
207 208
  /// Create a hero.
  ///
Hans Muller's avatar
Hans Muller committed
209
  /// The [tag] and [child] parameters must not be null.
210
  /// The [child] parameter and all of the its descendants must not be [Hero]es.
211
  const Hero({
212 213
    Key? key,
    required this.tag,
214
    this.createRectTween,
215 216
    this.flightShuttleBuilder,
    this.placeholderBuilder,
xster's avatar
xster committed
217
    this.transitionOnUserGestures = false,
218
    required this.child,
219
  }) : assert(tag != null),
xster's avatar
xster committed
220
       assert(transitionOnUserGestures != null),
221 222
       assert(child != null),
       super(key: key);
Hixie's avatar
Hixie committed
223

Ian Hickson's avatar
Ian Hickson committed
224
  /// The identifier for this particular hero. If the tag of this hero matches
Hans Muller's avatar
Hans Muller committed
225 226
  /// 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
227
  final Object tag;
228

229 230 231 232
  /// 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
233
  /// starting hero's child. The [Tween<Rect>] returned by this callback is used
234 235 236 237 238
  /// 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
239
  /// [MaterialApp] creates a [MaterialRectArcTween].
240
  final CreateRectTween? createRectTween;
241

Hans Muller's avatar
Hans Muller committed
242
  /// The widget subtree that will "fly" from one route to another during a
243
  /// [Navigator] push or pop transition.
Ian Hickson's avatar
Ian Hickson committed
244
  ///
Hans Muller's avatar
Hans Muller committed
245 246 247 248
  /// 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.
249
  ///
250
  /// {@macro flutter.widgets.ProxyWidget.child}
Ian Hickson's avatar
Ian Hickson committed
251 252
  final Widget child;

253 254 255 256 257 258
  /// 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.
  ///
259
  /// When both the source and destination [Hero]es provide a [flightShuttleBuilder],
260 261 262 263
  /// the destination's [flightShuttleBuilder] takes precedence.
  ///
  /// If none is provided, the destination route's Hero child is shown in-flight
  /// by default.
264 265 266 267 268 269 270 271 272 273 274 275 276 277
  ///
  /// ## 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.
278
  final HeroFlightShuttleBuilder? flightShuttleBuilder;
279

280 281
  /// Placeholder widget left in place as the Hero's [child] once the flight takes
  /// off.
282
  ///
283 284 285 286
  /// 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.
287
  final HeroPlaceholderBuilder? placeholderBuilder;
288

xster's avatar
xster committed
289 290 291
  /// Whether to perform the hero transition if the [PageRoute] transition was
  /// triggered by a user gesture, such as a back swipe on iOS.
  ///
292
  /// If [Hero]es with the same [tag] on both the from and the to routes have
xster's avatar
xster committed
293 294 295 296 297 298 299 300 301 302 303
  /// [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;

304 305 306 307
  // 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(
308 309 310
    BuildContext context,
    bool isUserGestureTransition,
    NavigatorState navigator,
311
  ) {
Hans Muller's avatar
Hans Muller committed
312
    assert(context != null);
xster's avatar
xster committed
313
    assert(isUserGestureTransition != null);
314
    assert(navigator != null);
Hans Muller's avatar
Hans Muller committed
315
    final Map<Object, _HeroState> result = <Object, _HeroState>{};
316

317
    void inviteHero(StatefulElement hero, Object tag) {
318 319
      assert(() {
        if (result.containsKey(tag)) {
320 321 322 323 324 325 326 327 328
          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),
          ]);
329 330 331
        }
        return true;
      }());
332 333
      final Hero heroWidget = hero.widget as Hero;
      final _HeroState heroState = hero.state as _HeroState;
334 335 336 337 338
      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.
339
        heroState.endFlight();
340
      }
341 342
    }

Hixie's avatar
Hixie committed
343
    void visitor(Element element) {
344 345 346 347
      final Widget widget = element.widget;
      if (widget is Hero) {
        final StatefulElement hero = element as StatefulElement;
        final Object tag = widget.tag;
348 349 350 351 352 353 354 355 356
        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.
357
          final ModalRoute<Object?>? heroRoute = ModalRoute.of(hero);
358 359
          if (heroRoute != null && heroRoute is PageRoute && heroRoute.isCurrent) {
            inviteHero(hero, tag);
360
          }
xster's avatar
xster committed
361
        }
najeira's avatar
najeira committed
362 363
      } else if (widget is HeroMode && !widget.enabled) {
        return;
Hixie's avatar
Hixie committed
364 365 366
      }
      element.visitChildren(visitor);
    }
367

Hixie's avatar
Hixie committed
368 369 370 371
    context.visitChildElements(visitor);
    return result;
  }

372
  @override
373
  _HeroState createState() => _HeroState();
374 375

  @override
376 377
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
378
    properties.add(DiagnosticsProperty<Object>('tag', tag));
379
  }
Hixie's avatar
Hixie committed
380 381
}

382 383 384 385 386 387 388
/// The [Hero] widget displays different content based on whether it is in an
/// animated transition ("flight"), from/to another [Hero] with the same tag:
///   * When [startFlight] is called, the real content of this [Hero] will be
///     replaced by a "placeholder" widget.
///   * When the flight ends, the "toHero"'s [endFlight] method must be called
///     by the hero controller, so the real content of that [Hero] becomes
///     visible again when the animation completes.
Hans Muller's avatar
Hans Muller committed
389
class _HeroState extends State<Hero> {
390
  final GlobalKey _key = GlobalKey();
391
  Size? _placeholderSize;
392 393 394 395 396 397 398 399 400 401 402 403
  // 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
404
  // widget as a descendant, allowing the original element tree to be preserved.
405 406 407 408 409
  //
  // 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
410
    assert(mounted);
411
    final RenderBox box = context.findRenderObject()! as RenderBox;
Hans Muller's avatar
Hans Muller committed
412
    assert(box != null && box.hasSize);
Hixie's avatar
Hixie committed
413
    setState(() {
Hans Muller's avatar
Hans Muller committed
414
      _placeholderSize = box.size;
Hixie's avatar
Hixie committed
415
    });
Hixie's avatar
Hixie committed
416 417
  }

418
  // When `keepPlaceholder` is true, the placeholder will continue to be shown
419 420
  // after the flight ends. Otherwise the child of the Hero will become visible
  // and its TickerMode will be re-enabled.
421 422 423
  //
  // This method can be safely called even when this [Hero] is currently not in
  // a flight.
424
  void endFlight({ bool keepPlaceholder = false }) {
425 426 427 428 429 430 431 432
    if (keepPlaceholder || _placeholderSize == null)
      return;

    _placeholderSize = null;
    if (mounted) {
      // Tell the widget to rebuild if it's mounted. _paceholderSize has already
      // been updated.
      setState(() {});
433 434 435
    }
  }

436
  @override
Hixie's avatar
Hixie committed
437
  Widget build(BuildContext context) {
438
    assert(
439
      context.findAncestorWidgetOfExactType<Hero>() == null,
440 441 442
      'A Hero widget cannot be the descendant of another Hero widget.'
    );

443
    final bool showPlaceholder = _placeholderSize != null;
444

445
    if (showPlaceholder && widget.placeholderBuilder != null) {
446
      return widget.placeholderBuilder!(context, _placeholderSize!, widget.child);
Hixie's avatar
Hixie committed
447
    }
448

449
    if (showPlaceholder && !_shouldIncludeChild) {
450
      return SizedBox(
451 452
        width: _placeholderSize!.width,
        height: _placeholderSize!.height,
453 454 455 456 457 458 459
      );
    }

    return SizedBox(
      width: _placeholderSize?.width,
      height: _placeholderSize?.height,
      child: Offstage(
460
        offstage: showPlaceholder,
461
        child: TickerMode(
462
          enabled: !showPlaceholder,
463
          child: KeyedSubtree(key: _key, child: widget.child),
464
        ),
465
      ),
Hixie's avatar
Hixie committed
466
    );
Hixie's avatar
Hixie committed
467 468 469
  }
}

470
// Everything known about a hero flight that's to be started or diverted.
471
@immutable
Hans Muller's avatar
Hans Muller committed
472 473
class _HeroFlightManifest {
  _HeroFlightManifest({
474 475
    required this.type,
    required this.overlay,
476
    required this.navigatorSize,
477 478 479 480 481 482 483 484
    required this.fromRoute,
    required this.toRoute,
    required this.fromHero,
    required this.toHero,
    required this.createRectTween,
    required this.shuttleBuilder,
    required this.isUserGestureTransition,
    required this.isDiverted,
485
  }) : assert(fromHero.widget.tag == toHero.widget.tag);
Hixie's avatar
Hixie committed
486

487
  final HeroFlightDirection type;
488 489
  final OverlayState overlay;
  final Size navigatorSize;
Hans Muller's avatar
Hans Muller committed
490 491 492 493
  final PageRoute<dynamic> fromRoute;
  final PageRoute<dynamic> toRoute;
  final _HeroState fromHero;
  final _HeroState toHero;
494
  final CreateRectTween? createRectTween;
495
  final HeroFlightShuttleBuilder shuttleBuilder;
xster's avatar
xster committed
496
  final bool isUserGestureTransition;
497
  final bool isDiverted;
498

499
  Object get tag => fromHero.widget.tag;
500

Hans Muller's avatar
Hans Muller committed
501
  Animation<double> get animation {
502
    return CurvedAnimation(
503
      parent: (type == HeroFlightDirection.push) ? toRoute.animation! : fromRoute.animation!,
Hans Muller's avatar
Hans Muller committed
504
      curve: Curves.fastOutSlowIn,
505
      reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped,
Hixie's avatar
Hixie committed
506 507 508
    );
  }

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
  Tween<Rect?> createHeroRectTween({ required Rect? begin, required Rect? end }) {
    final CreateRectTween? createRectTween = toHero.widget.createRectTween ?? this.createRectTween;
    return createRectTween?.call(begin, end) ?? RectTween(begin: begin, end: end);
  }

  // The bounding box for `context`'s render object,  in `ancestorContext`'s
  // render object's coordinate space.
  static Rect _boundingBoxFor(BuildContext context, BuildContext? ancestorContext) {
    assert(ancestorContext != null);
    final RenderBox box = context.findRenderObject()! as RenderBox;
    assert(box != null && box.hasSize && box.size.isFinite);
    return MatrixUtils.transformRect(
      box.getTransformTo(ancestorContext?.findRenderObject()),
      Offset.zero & box.size,
    );
  }

  /// The bounding box of [fromHero], in [fromRoute]'s coordinate space.
  ///
  /// This property should only be accessed in [_HeroFlight.start].
  late final Rect fromHeroLocation = _boundingBoxFor(fromHero.context, fromRoute.subtreeContext);

  /// The bounding box of [toHero], in [toRoute]'s coordinate space.
  ///
  /// This property should only be accessed in [_HeroFlight.start] or
  /// [_HeroFlight.divert].
  late final Rect toHeroLocation = _boundingBoxFor(toHero.context, toRoute.subtreeContext);

  /// Whether this [_HeroFlightManifest] is valid and can be used to start or
  /// divert a [_HeroFlight].
  ///
  /// When starting or diverting a [_HeroFlight] with a brand new
  /// [_HeroFlightManifest], this flag must be checked to ensure the [RectTween]
  /// the [_HeroFlightManifest] produces does not contain coordinates that have
  /// [double.infinity] or [double.nan].
  late final bool isValid = toHeroLocation.isFinite && (isDiverted || fromHeroLocation.isFinite);

Hans Muller's avatar
Hans Muller committed
546 547
  @override
  String toString() {
548
    return '_HeroFlightManifest($type tag: $tag from route: ${fromRoute.settings} '
549
        'to route: ${toRoute.settings} with hero: $fromHero to $toHero)${isValid ? '' : ', INVALID'}';
550
  }
Hans Muller's avatar
Hans Muller committed
551
}
552

Hans Muller's avatar
Hans Muller committed
553 554 555
// Builds the in-flight hero widget.
class _HeroFlight {
  _HeroFlight(this.onFlightEnded) {
556
    _proxyAnimation = ProxyAnimation()..addStatusListener(_handleAnimationUpdate);
Hixie's avatar
Hixie committed
557
  }
558

Hans Muller's avatar
Hans Muller committed
559
  final _OnFlightEnded onFlightEnded;
Hixie's avatar
Hixie committed
560

561 562
  late Tween<Rect?> heroRectTween;
  Widget? shuttle;
563

Hans Muller's avatar
Hans Muller committed
564
  Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
565
  late ProxyAnimation _proxyAnimation;
566 567 568
  // The manifest will be available once `start` is called, throughout the
  // flight's lifecycle.
  late _HeroFlightManifest manifest;
569
  OverlayEntry? overlayEntry;
570
  bool _aborted = false;
Hixie's avatar
Hixie committed
571

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

Hans Muller's avatar
Hans Muller committed
574 575 576
  // The OverlayEntry WidgetBuilder callback for the hero's overlay.
  Widget _buildOverlay(BuildContext context) {
    assert(manifest != null);
577
    shuttle ??= manifest.shuttleBuilder(
578
      context,
579 580 581 582
      manifest.animation,
      manifest.type,
      manifest.fromHero.context,
      manifest.toHero.context,
583 584 585
    );
    assert(shuttle != null);

586
    return AnimatedBuilder(
Hans Muller's avatar
Hans Muller committed
587
      animation: _proxyAnimation,
588
      child: shuttle,
589 590
      builder: (BuildContext context, Widget? child) {
        final Rect rect = heroRectTween.evaluate(_proxyAnimation)!;
591
        final RelativeRect offsets = RelativeRect.fromSize(rect, manifest.navigatorSize);
592
        return Positioned(
Hans Muller's avatar
Hans Muller committed
593 594 595 596
          top: offsets.top,
          right: offsets.right,
          bottom: offsets.bottom,
          left: offsets.left,
597 598 599
          child: IgnorePointer(
            child: RepaintBoundary(
              child: Opacity(
Hans Muller's avatar
Hans Muller committed
600 601 602 603 604 605 606 607 608
                opacity: _heroOpacity.value,
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }
Hixie's avatar
Hixie committed
609

610
  void _performAnimationUpdate(AnimationStatus status) {
Hans Muller's avatar
Hans Muller committed
611 612
    if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
      _proxyAnimation.parent = null;
Hixie's avatar
Hixie committed
613

Hans Muller's avatar
Hans Muller committed
614
      assert(overlayEntry != null);
615
      overlayEntry!.remove();
Hans Muller's avatar
Hans Muller committed
616
      overlayEntry = null;
617 618 619 620 621
      // 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.
622 623
      manifest.fromHero.endFlight(keepPlaceholder: status == AnimationStatus.completed);
      manifest.toHero.endFlight(keepPlaceholder: status == AnimationStatus.dismissed);
Hans Muller's avatar
Hans Muller committed
624
      onFlightEnded(this);
625
      _proxyAnimation.removeListener(onTick);
Hans Muller's avatar
Hans Muller committed
626
    }
Hixie's avatar
Hixie committed
627 628
  }

629 630 631 632 633
  bool _scheduledPerformAnimtationUpdate = false;
  void _handleAnimationUpdate(AnimationStatus status) {
    // The animation will not finish until the user lifts their finger, so we
    // should suppress the status update if the gesture is in progress, and
    // delay it until the finger is lifted.
634
    if (manifest.fromRoute.navigator?.userGestureInProgress != true) {
635 636 637 638 639 640 641 642 643
      _performAnimationUpdate(status);
      return;
    }

    if (_scheduledPerformAnimtationUpdate)
      return;

    // The `navigator` must be non-null here, or the first if clause above would
    // have returned from this method.
644
    final NavigatorState navigator = manifest.fromRoute.navigator!;
645 646 647 648 649 650 651 652 653 654 655 656 657

    void delayedPerformAnimtationUpdate() {
      assert(!navigator.userGestureInProgress);
      assert(_scheduledPerformAnimtationUpdate);
      _scheduledPerformAnimtationUpdate = false;
      navigator.userGestureInProgressNotifier.removeListener(delayedPerformAnimtationUpdate);
      _performAnimationUpdate(_proxyAnimation.status);
    }
    assert(navigator.userGestureInProgress);
    _scheduledPerformAnimtationUpdate = true;
    navigator.userGestureInProgressNotifier.addListener(delayedPerformAnimtationUpdate);
  }

658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
  void onTick() {
    final RenderBox? toHeroBox = (!_aborted && manifest.toHero.mounted)
      ? manifest.toHero.context.findRenderObject() as RenderBox?
      : null;
    // Try to find the new origin of the toHero, if the flight isn't aborted.
    final Offset? toHeroOrigin = toHeroBox != null && toHeroBox.attached && toHeroBox.hasSize
      ? toHeroBox.localToGlobal(Offset.zero, ancestor: manifest.toRoute.subtreeContext?.findRenderObject() as RenderBox?)
      : null;

    if (toHeroOrigin != null && toHeroOrigin.isFinite) {
      // If the new origin of toHero is available and also paintable, try to
      // update heroRectTween with it.
      if (toHeroOrigin != heroRectTween.end!.topLeft) {
        final Rect heroRectEnd = toHeroOrigin & heroRectTween.end!.size;
        heroRectTween = manifest.createHeroRectTween(begin: heroRectTween.begin, end: heroRectEnd);
      }
    } else if (_heroOpacity.isCompleted) {
      // The toHero no longer exists or it's no longer the flight's destination.
      // Continue flying while fading out.
      _heroOpacity = _proxyAnimation.drive(
        _reverseTween.chain(CurveTween(curve: Interval(_proxyAnimation.value, 1.0))),
      );
    }
    // Update _aborted for the next animation tick.
    _aborted = toHeroOrigin == null || !toHeroOrigin.isFinite;
  }

Hans Muller's avatar
Hans Muller committed
685 686
  // The simple case: we're either starting a push or a pop animation.
  void start(_HeroFlightManifest initialManifest) {
687
    assert(!_aborted);
Hans Muller's avatar
Hans Muller committed
688 689
    assert(() {
      final Animation<double> initial = initialManifest.animation;
690
      assert(initial != null);
691
      final HeroFlightDirection type = initialManifest.type;
692 693
      assert(type != null);
      switch (type) {
694
        case HeroFlightDirection.pop:
xster's avatar
xster committed
695 696 697 698 699 700
          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;
701
        case HeroFlightDirection.push:
Hans Muller's avatar
Hans Muller committed
702 703
          return initial.value == 0.0 && initial.status == AnimationStatus.forward;
      }
704
    }());
Hixie's avatar
Hixie committed
705

Hans Muller's avatar
Hans Muller committed
706
    manifest = initialManifest;
707

708 709 710 711 712 713 714 715 716 717 718
    final bool shouldIncludeChildInPlacehold;
    switch (manifest.type) {
      case HeroFlightDirection.pop:
        _proxyAnimation.parent = ReverseAnimation(manifest.animation);
        shouldIncludeChildInPlacehold = false;
        break;
      case HeroFlightDirection.push:
        _proxyAnimation.parent = manifest.animation;
        shouldIncludeChildInPlacehold = true;
        break;
    }
Hixie's avatar
Hixie committed
719

720 721 722 723 724
    heroRectTween = manifest.createHeroRectTween(begin: manifest.fromHeroLocation, end: manifest.toHeroLocation);
    manifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: shouldIncludeChildInPlacehold);
    manifest.toHero.startFlight();
    manifest.overlay.insert(overlayEntry = OverlayEntry(builder: _buildOverlay));
    _proxyAnimation.addListener(onTick);
Hixie's avatar
Hixie committed
725 726
  }

Hans Muller's avatar
Hans Muller committed
727 728
  // 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.
729
  void divert(_HeroFlightManifest newManifest) {
730 731
    assert(manifest.tag == newManifest.tag);
    if (manifest.type == HeroFlightDirection.push && newManifest.type == HeroFlightDirection.pop) {
Hans Muller's avatar
Hans Muller committed
732 733
      // A push flight was interrupted by a pop.
      assert(newManifest.animation.status == AnimationStatus.reverse);
734 735 736 737
      assert(manifest.fromHero == newManifest.toHero);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.fromRoute == newManifest.toRoute);
      assert(manifest.toRoute == newManifest.fromRoute);
738

739 740 741 742 743
      // 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.
744
      _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
745
      heroRectTween = ReverseTween<Rect?>(heroRectTween);
746
    } else if (manifest.type == HeroFlightDirection.pop && newManifest.type == HeroFlightDirection.push) {
Hans Muller's avatar
Hans Muller committed
747 748
      // A pop flight was interrupted by a push.
      assert(newManifest.animation.status == AnimationStatus.forward);
749 750
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.toRoute == newManifest.fromRoute);
Hans Muller's avatar
Hans Muller committed
751

752 753
      _proxyAnimation.parent = newManifest.animation.drive(
        Tween<double>(
754
          begin: manifest.animation.value,
755 756 757
          end: 1.0,
        ),
      );
758 759
      if (manifest.fromHero != newManifest.toHero) {
        manifest.fromHero.endFlight(keepPlaceholder: true);
Hans Muller's avatar
Hans Muller committed
760
        newManifest.toHero.startFlight();
761
        heroRectTween = manifest.createHeroRectTween(begin: heroRectTween.end, end: newManifest.toHeroLocation);
Hans Muller's avatar
Hans Muller committed
762
      } else {
763
        // TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
764
        heroRectTween = manifest.createHeroRectTween(begin: heroRectTween.end, end: heroRectTween.begin);
Hixie's avatar
Hixie committed
765
      }
Hans Muller's avatar
Hans Muller committed
766 767 768 769
    } 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
770 771
      assert(manifest.fromHero != newManifest.fromHero);
      assert(manifest.toHero != newManifest.toHero);
Hans Muller's avatar
Hans Muller committed
772

773 774 775
      heroRectTween = manifest.createHeroRectTween(
        begin: heroRectTween.evaluate(_proxyAnimation),
        end: newManifest.toHeroLocation,
776
      );
777
      shuttle = null;
Hans Muller's avatar
Hans Muller committed
778

779
      if (newManifest.type == HeroFlightDirection.pop)
780
        _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
Hans Muller's avatar
Hans Muller committed
781 782 783
      else
        _proxyAnimation.parent = newManifest.animation;

784 785
      manifest.fromHero.endFlight(keepPlaceholder: true);
      manifest.toHero.endFlight(keepPlaceholder: true);
786 787

      // Let the heroes in each of the routes rebuild with their placeholders.
788
      newManifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: newManifest.type == HeroFlightDirection.push);
Hans Muller's avatar
Hans Muller committed
789
      newManifest.toHero.startFlight();
790 791 792

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

    manifest = newManifest;
Hixie's avatar
Hixie committed
797 798
  }

799 800 801 802
  void abort() {
    _aborted = true;
  }

803
  @override
Hans Muller's avatar
Hans Muller committed
804
  String toString() {
805 806 807
    final RouteSettings from = manifest.fromRoute.settings;
    final RouteSettings to = manifest.toRoute.settings;
    final Object tag = manifest.tag;
Hans Muller's avatar
Hans Muller committed
808 809
    return 'HeroFlight(for: $tag, from: $from, to: $to ${_proxyAnimation.parent})';
  }
Hixie's avatar
Hixie committed
810
}
811

Ian Hickson's avatar
Ian Hickson committed
812 813
/// A [Navigator] observer that manages [Hero] transitions.
///
814
/// An instance of [HeroController] should be used in [Navigator.observers].
Ian Hickson's avatar
Ian Hickson committed
815
/// This is done automatically by [MaterialApp].
816
class HeroController extends NavigatorObserver {
Ian Hickson's avatar
Ian Hickson committed
817 818
  /// Creates a hero controller with the given [RectTween] constructor if any.
  ///
819
  /// The [createRectTween] argument is optional. If null, the controller uses a
820
  /// linear [Tween<Rect>].
Hans Muller's avatar
Hans Muller committed
821
  HeroController({ this.createRectTween });
822

xster's avatar
xster committed
823
  /// Used to create [RectTween]s that interpolate the position of heroes in flight.
824 825
  ///
  /// If null, the controller uses a linear [RectTween].
826
  final CreateRectTween? createRectTween;
827

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

832
  @override
833
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
834
    assert(navigator != null);
835
    assert(route != null);
xster's avatar
xster committed
836
    _maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push, false);
837 838
  }

839
  @override
840
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
841
    assert(navigator != null);
842
    assert(route != null);
843 844
    // Don't trigger another flight when a pop is committed as a user gesture
    // back swipe is snapped.
845
    if (!navigator!.userGestureInProgress)
846
      _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, false);
847 848
  }

849
  @override
850
  void didReplace({ Route<dynamic>? newRoute, Route<dynamic>? oldRoute }) {
851 852 853 854 855 856 857
    assert(navigator != null);
    if (newRoute?.isCurrent == true) {
      // Only run hero animations if the top-most route got replaced.
      _maybeStartHeroTransition(oldRoute, newRoute, HeroFlightDirection.push, false);
    }
  }

858
  @override
859
  void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) {
xster's avatar
xster committed
860 861 862
    assert(navigator != null);
    assert(route != null);
    _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, true);
863 864
  }

865 866
  @override
  void didStopUserGesture() {
867
    if (navigator!.userGestureInProgress)
868 869
      return;

870 871 872 873 874
    // When the user gesture ends, 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.
875
    bool isInvalidFlight(_HeroFlight flight) {
876 877
      return flight.manifest.isUserGestureTransition
          && flight.manifest.type == HeroFlightDirection.pop
878 879 880 881 882 883 884 885 886
          && 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.
887
    for (final _HeroFlight flight in invalidFlights) {
888 889 890 891
      flight._handleAnimationUpdate(AnimationStatus.dismissed);
    }
  }

Hans Muller's avatar
Hans Muller committed
892 893
  // 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
894
  void _maybeStartHeroTransition(
895 896
    Route<dynamic>? fromRoute,
    Route<dynamic>? toRoute,
xster's avatar
xster committed
897 898 899 900
    HeroFlightDirection flightType,
    bool isUserGestureTransition,
  ) {
    if (toRoute != fromRoute && toRoute is PageRoute<dynamic> && fromRoute is PageRoute<dynamic>) {
Hans Muller's avatar
Hans Muller committed
901 902
      final PageRoute<dynamic> from = fromRoute;
      final PageRoute<dynamic> to = toRoute;
903
      final Animation<double> animation = (flightType == HeroFlightDirection.push) ? to.animation! : from.animation!;
Hans Muller's avatar
Hans Muller committed
904

905 906 907 908 909 910 911 912 913 914 915 916
      // 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
917
      }
Hans Muller's avatar
Hans Muller committed
918

xster's avatar
xster committed
919 920 921 922 923 924 925 926
      // 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
927

xster's avatar
xster committed
928 929 930
        // 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.
931
        to.offstage = to.animation!.value == 0.0;
xster's avatar
xster committed
932

933
        WidgetsBinding.instance!.addPostFrameCallback((Duration value) {
xster's avatar
xster committed
934 935 936
          _startHeroTransition(from, to, animation, flightType, isUserGestureTransition);
        });
      }
937 938 939
    }
  }

xster's avatar
xster committed
940
  // Find the matching pairs of heroes in from and to and either start or a new
941
  // hero flight, or divert an existing one.
942 943 944 945 946
  void _startHeroTransition(
    PageRoute<dynamic> from,
    PageRoute<dynamic> to,
    Animation<double> animation,
    HeroFlightDirection flightType,
xster's avatar
xster committed
947
    bool isUserGestureTransition,
948
  ) {
949
    // If the `to` route was offstage, then we're implicitly restoring its
Hans Muller's avatar
Hans Muller committed
950 951 952
    // animation value back to what it was before it was "moved" offstage.
    to.offstage = false;

953 954 955 956 957 958 959 960
    final NavigatorState? navigator = this.navigator;
    final OverlayState? overlay = navigator?.overlay;
    // If the navigator or the overlay was removed before this end-of-frame
    // callback was called, then don't actually start a transition, and we don'
    // t have to worry about any Hero widget we might have hidden in a previous
    // flight, or onging flights.
    if (navigator == null || overlay == null)
      return;
961

962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
    final RenderObject? navigatorRenderObject = navigator.context.findRenderObject();

    if (navigatorRenderObject is! RenderBox) {
      assert(false, 'Navigator $navigator has an invalid RenderObject type ${navigatorRenderObject.runtimeType}.');
      return;
    }
    assert(navigatorRenderObject.hasSize);

    // At this point, the toHeroes may have been built and laid out for the first time.
    //
    // If `fromSubtreeContext` is null, call endFlight on all toHeroes, for good measure.
    // If `toSubtreeContext` is null abort existingFlights.
    final BuildContext? fromSubtreeContext = from.subtreeContext;
    final Map<Object, _HeroState> fromHeroes = fromSubtreeContext != null
      ? Hero._allHeroesFor(fromSubtreeContext, isUserGestureTransition, navigator)
      : const <Object, _HeroState>{};
    final BuildContext? toSubtreeContext = to.subtreeContext;
    final Map<Object, _HeroState> toHeroes = toSubtreeContext != null
      ? Hero._allHeroesFor(toSubtreeContext, isUserGestureTransition, navigator)
      : const <Object, _HeroState>{};

    for (final MapEntry<Object, _HeroState> fromHeroEntry in fromHeroes.entries) {
      final Object tag = fromHeroEntry.key;
      final _HeroState fromHero = fromHeroEntry.value;
      final _HeroState? toHero = toHeroes[tag];
      final _HeroFlight? existingFlight = _flights[tag];
      final _HeroFlightManifest? manifest = toHero == null
        ? null
        : _HeroFlightManifest(
            type: flightType,
            overlay: overlay,
            navigatorSize: navigatorRenderObject.size,
            fromRoute: from,
            toRoute: to,
            fromHero: fromHero,
            toHero: toHero,
            createRectTween: createRectTween,
            shuttleBuilder: fromHero.widget.flightShuttleBuilder
                          ?? toHero.widget.flightShuttleBuilder
                          ?? _defaultHeroFlightShuttleBuilder,
            isUserGestureTransition: isUserGestureTransition,
            isDiverted: existingFlight != null,
          );

      // Only proceed with a valid manifest. Otherwise abort the existing
      // flight, and call endFlight when this for loop finishes.
      if (manifest != null && manifest.isValid) {
        toHeroes.remove(tag);
        if (existingFlight != null) {
          existingFlight.divert(manifest);
        } else {
1013
          _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);
1014 1015 1016
        }
      } else {
        existingFlight?.abort();
Hans Muller's avatar
Hans Muller committed
1017
      }
1018
    }
1019

1020 1021 1022 1023 1024 1025 1026 1027
    // The remaining entries in toHeroes are those failed to participate in a
    // new flight (for not having a valid manifest).
    //
    // This can happen in a route pop transition when a fromHero is no longer
    // mounted, or kept alive by the [KeepAlive] mechanism but no longer visible.
    // TODO(LongCatIsLooong): resume aborted flights: https://github.com/flutter/flutter/issues/72947
    for (final _HeroState toHero in toHeroes.values)
      toHero.endFlight();
1028 1029
  }

Hans Muller's avatar
Hans Muller committed
1030
  void _handleFlightEnded(_HeroFlight flight) {
1031
    _flights.remove(flight.manifest.tag);
1032
  }
1033 1034 1035 1036 1037 1038 1039 1040

  static final HeroFlightShuttleBuilder _defaultHeroFlightShuttleBuilder = (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
1041
    final Hero toHero = toHeroContext.widget as Hero;
1042 1043
    return toHero.child;
  };
1044
}
najeira's avatar
najeira committed
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084

/// Enables or disables [Hero]es in the widget subtree.
///
/// When [enabled] is false, all [Hero] widgets in this subtree will not be
/// involved in hero animations.
///
/// When [enabled] is true (the default), [Hero] widgets may be involved in
/// hero animations, as usual.
class HeroMode extends StatelessWidget {
  /// Creates a widget that enables or disables [Hero]es.
  ///
  /// The [child] and [enabled] arguments must not be null.
  const HeroMode({
    Key? key,
    required this.child,
    this.enabled = true,
  }) : assert(child != null),
       assert(enabled != null),
       super(key: key);

  /// The subtree to place inside the [HeroMode].
  final Widget child;

  /// Whether or not [Hero]es are enabled in this subtree.
  ///
  /// If this property is false, the [Hero]es in this subtree will not animate
  /// on route changes. Otherwise, they will animate as usual.
  ///
  /// Defaults to true and must not be null.
  final bool enabled;

  @override
  Widget build(BuildContext context) => child;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagProperty('mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
  }
}