| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/material.dart'; |
| 6 | library; |
| 7 | |
| 8 | import 'package:flutter/foundation.dart'; |
| 9 | import 'basic.dart'; |
| 10 | import 'binding.dart'; |
| 11 | import 'framework.dart'; |
| 12 | import 'implicit_animations.dart'; |
| 13 | import 'media_query.dart'; |
| 14 | import 'navigator.dart'; |
| 15 | import 'overlay.dart'; |
| 16 | import 'pages.dart'; |
| 17 | import 'routes.dart'; |
| 18 | import 'ticker_provider.dart' show TickerMode; |
| 19 | import 'transitions.dart'; |
| 20 | |
| 21 | /// Signature for a function that takes two [Rect] instances and returns a |
| 22 | /// [RectTween] that transitions between them. |
| 23 | /// |
| 24 | /// This is typically used with a [HeroController] to provide an animation for |
| 25 | /// [Hero] positions that looks nicer than a linear movement. For example, see |
| 26 | /// [MaterialRectArcTween]. |
| 27 | typedef CreateRectTween = Tween<Rect?> Function(Rect? begin, Rect? end); |
| 28 | |
| 29 | /// Signature for a function that builds a [Hero] placeholder widget given a |
| 30 | /// child and a [Size]. |
| 31 | /// |
| 32 | /// The child can optionally be part of the returned widget tree. The returned |
| 33 | /// widget should typically be constrained to [heroSize], if it doesn't do so |
| 34 | /// implicitly. |
| 35 | /// |
| 36 | /// See also: |
| 37 | /// |
| 38 | /// * [TransitionBuilder], which is similar but only takes a [BuildContext] |
| 39 | /// and a child widget. |
| 40 | typedef HeroPlaceholderBuilder = Widget Function(BuildContext context, Size heroSize, Widget child); |
| 41 | |
| 42 | /// A function that lets [Hero]es self supply a [Widget] that is shown during the |
| 43 | /// hero's flight from one route to another instead of default (which is to |
| 44 | /// show the destination route's instance of the Hero). |
| 45 | typedef HeroFlightShuttleBuilder = |
| 46 | Widget Function( |
| 47 | BuildContext flightContext, |
| 48 | Animation<double> animation, |
| 49 | HeroFlightDirection flightDirection, |
| 50 | BuildContext fromHeroContext, |
| 51 | BuildContext toHeroContext, |
| 52 | ); |
| 53 | |
| 54 | typedef _OnFlightEnded = void Function(_HeroFlight flight); |
| 55 | |
| 56 | /// Direction of the hero's flight based on the navigation operation. |
| 57 | enum HeroFlightDirection { |
| 58 | /// A flight triggered by a route push. |
| 59 | /// |
| 60 | /// The animation goes from 0 to 1. |
| 61 | /// |
| 62 | /// If no custom [HeroFlightShuttleBuilder] is supplied, the top route's |
| 63 | /// [Hero] child is shown in flight. |
| 64 | push, |
| 65 | |
| 66 | /// A flight triggered by a route pop. |
| 67 | /// |
| 68 | /// The animation goes from 1 to 0. |
| 69 | /// |
| 70 | /// If no custom [HeroFlightShuttleBuilder] is supplied, the bottom route's |
| 71 | /// [Hero] child is shown in flight. |
| 72 | pop, |
| 73 | } |
| 74 | |
| 75 | /// A widget that marks its child as being a candidate for |
| 76 | /// [hero animations](https://docs.flutter.dev/ui/animations/hero-animations). |
| 77 | /// |
| 78 | /// When a [PageRoute] is pushed or popped with the [Navigator], the entire |
| 79 | /// screen's content is replaced. An old route disappears and a new route |
| 80 | /// appears. If there's a common visual feature on both routes then it can |
| 81 | /// be helpful for orienting the user for the feature to physically move from |
| 82 | /// one page to the other during the routes' transition. Such an animation |
| 83 | /// is called a *hero animation*. The hero widgets "fly" in the Navigator's |
| 84 | /// overlay during the transition and while they're in-flight they're, by |
| 85 | /// default, not shown in their original locations in the old and new routes. |
| 86 | /// |
| 87 | /// To label a widget as such a feature, wrap it in a [Hero] widget. When |
| 88 | /// navigation happens, the [Hero] widgets on each route are identified |
| 89 | /// by the [HeroController]. For each pair of [Hero] widgets that have the |
| 90 | /// same tag, a hero animation is triggered. |
| 91 | /// |
| 92 | /// If a [Hero] is already in flight when navigation occurs, its |
| 93 | /// flight animation will be redirected to its new destination. The |
| 94 | /// widget shown in-flight during the transition is, by default, the |
| 95 | /// destination route's [Hero]'s child. |
| 96 | /// |
| 97 | /// For a Hero animation to trigger, the Hero has to exist on the very first |
| 98 | /// frame of the new page's animation. |
| 99 | /// |
| 100 | /// Routes must not contain more than one [Hero] for each [tag]. |
| 101 | /// |
| 102 | /// {@youtube 560 315 https://www.youtube.com/watch?v=Be9UH1kXFDw} |
| 103 | /// |
| 104 | /// {@tool dartpad} |
| 105 | /// This sample shows a [Hero] used within a [ListTile]. |
| 106 | /// |
| 107 | /// Tapping on the Hero-wrapped rectangle triggers a hero |
| 108 | /// animation as a new [MaterialPageRoute] is pushed. Both the size |
| 109 | /// and location of the rectangle animates. |
| 110 | /// |
| 111 | /// Both widgets use the same [Hero.tag]. |
| 112 | /// |
| 113 | /// The Hero widget uses the matching tags to identify and execute this |
| 114 | /// animation. |
| 115 | /// |
| 116 | /// ** See code in examples/api/lib/widgets/heroes/hero.0.dart ** |
| 117 | /// {@end-tool} |
| 118 | /// |
| 119 | /// {@tool dartpad} |
| 120 | /// This sample shows [Hero] flight animations using default tween |
| 121 | /// and custom rect tween. |
| 122 | /// |
| 123 | /// ** See code in examples/api/lib/widgets/heroes/hero.1.dart ** |
| 124 | /// {@end-tool} |
| 125 | /// |
| 126 | /// ## Discussion |
| 127 | /// |
| 128 | /// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for |
| 129 | /// all this to work. The top left and bottom right coordinates of each animated |
| 130 | /// Hero will be converted to global coordinates and then from there converted |
| 131 | /// to that [Stack]'s coordinate space, and the entire Hero subtree will, for |
| 132 | /// the duration of the animation, be lifted out of its original place, and |
| 133 | /// positioned on that stack. If the [Hero] isn't axis aligned, this is going to |
| 134 | /// fail in a rather ugly fashion. Don't rotate your heroes! |
| 135 | /// |
| 136 | /// To make the animations look good, it's critical that the widget tree for the |
| 137 | /// hero in both locations be essentially identical. The widget of the *target* |
| 138 | /// is, by default, used to do the transition: when going from route A to route |
| 139 | /// B, route B's hero's widget is placed over route A's hero's widget. Additionally, |
| 140 | /// if the [Hero] subtree changes appearance based on an [InheritedWidget] (such |
| 141 | /// as [MediaQuery] or [Theme]), then the hero animation may have discontinuity |
| 142 | /// at the start or the end of the animation because route A and route B provides |
| 143 | /// different such [InheritedWidget]s. Consider providing a custom [flightShuttleBuilder] |
| 144 | /// to ensure smooth transitions. The default [flightShuttleBuilder] interpolates |
| 145 | /// [MediaQuery]'s paddings. If your [Hero] widget uses custom [InheritedWidget]s |
| 146 | /// and displays a discontinuity in the animation, try to provide custom in-flight |
| 147 | /// transition using [flightShuttleBuilder]. |
| 148 | /// |
| 149 | /// By default, both route A and route B's heroes are hidden while the |
| 150 | /// transitioning widget is animating in-flight above the 2 routes. |
| 151 | /// [placeholderBuilder] can be used to show a custom widget in their place |
| 152 | /// instead once the transition has taken flight. |
| 153 | /// |
| 154 | /// During the transition, the transition widget is animated to route B's hero's |
| 155 | /// position, and then the widget is inserted into route B. When going back from |
| 156 | /// B to A, route A's hero's widget is, by default, placed over where route B's |
| 157 | /// hero's widget was, and then the animation goes the other way. |
| 158 | /// |
| 159 | /// ### Nested Navigators |
| 160 | /// |
| 161 | /// If either or both routes contain nested [Navigator]s, only [Hero]es |
| 162 | /// contained in the top-most routes (as defined by [Route.isCurrent]) *of those |
| 163 | /// nested [Navigator]s* are considered for animation. Just like in the |
| 164 | /// non-nested case the top-most routes containing these [Hero]es in the nested |
| 165 | /// [Navigator]s have to be [PageRoute]s. |
| 166 | /// |
| 167 | /// ## Parts of a Hero Transition |
| 168 | /// |
| 169 | ///  |
| 170 | class Hero extends StatefulWidget { |
| 171 | /// Create a hero. |
| 172 | /// |
| 173 | /// The [child] parameter and all of the its descendants must not be [Hero]es. |
| 174 | const Hero({ |
| 175 | super.key, |
| 176 | required this.tag, |
| 177 | this.createRectTween, |
| 178 | this.flightShuttleBuilder, |
| 179 | this.placeholderBuilder, |
| 180 | this.transitionOnUserGestures = false, |
| 181 | required this.child, |
| 182 | }); |
| 183 | |
| 184 | /// The identifier for this particular hero. If the tag of this hero matches |
| 185 | /// the tag of a hero on a [PageRoute] that we're navigating to or from, then |
| 186 | /// a hero animation will be triggered. |
| 187 | final Object tag; |
| 188 | |
| 189 | /// Defines how the destination hero's bounds change as it flies from the starting |
| 190 | /// route to the destination route. |
| 191 | /// |
| 192 | /// A hero flight begins with the destination hero's [child] aligned with the |
| 193 | /// starting hero's child. The [Tween<Rect>] returned by this callback is used |
| 194 | /// to compute the hero's bounds as the flight animation's value goes from 0.0 |
| 195 | /// to 1.0. |
| 196 | /// |
| 197 | /// If this property is null, the default, then the value of |
| 198 | /// [HeroController.createRectTween] is used. The [HeroController] created by |
| 199 | /// [MaterialApp] creates a [MaterialRectArcTween]. |
| 200 | final CreateRectTween? createRectTween; |
| 201 | |
| 202 | /// The widget subtree that will "fly" from one route to another during a |
| 203 | /// [Navigator] push or pop transition. |
| 204 | /// |
| 205 | /// The appearance of this subtree should be similar to the appearance of |
| 206 | /// the subtrees of any other heroes in the application with the same [tag]. |
| 207 | /// Changes in scale and aspect ratio work well in hero animations, changes |
| 208 | /// in layout or composition do not. |
| 209 | /// |
| 210 | /// {@macro flutter.widgets.ProxyWidget.child} |
| 211 | final Widget child; |
| 212 | |
| 213 | /// Optional override to supply a widget that's shown during the hero's flight. |
| 214 | /// |
| 215 | /// This in-flight widget can depend on the route transition's animation as |
| 216 | /// well as the incoming and outgoing routes' [Hero] descendants' widgets and |
| 217 | /// layout. |
| 218 | /// |
| 219 | /// When both the source and destination [Hero]es provide a [flightShuttleBuilder], |
| 220 | /// the destination's [flightShuttleBuilder] takes precedence. |
| 221 | /// |
| 222 | /// If none is provided, the destination route's Hero child is shown in-flight |
| 223 | /// by default. |
| 224 | /// |
| 225 | /// ## Limitations |
| 226 | /// |
| 227 | /// If a widget built by [flightShuttleBuilder] takes part in a [Navigator] |
| 228 | /// push transition, that widget or its descendants must not have any |
| 229 | /// [GlobalKey] that is used in the source Hero's descendant widgets. That is |
| 230 | /// because both subtrees will be included in the widget tree during the Hero |
| 231 | /// flight animation, and [GlobalKey]s must be unique across the entire widget |
| 232 | /// tree. |
| 233 | /// |
| 234 | /// If the said [GlobalKey] is essential to your application, consider providing |
| 235 | /// a custom [placeholderBuilder] for the source Hero, to avoid the [GlobalKey] |
| 236 | /// collision, such as a builder that builds an empty [SizedBox], keeping the |
| 237 | /// Hero [child]'s original size. |
| 238 | final HeroFlightShuttleBuilder? flightShuttleBuilder; |
| 239 | |
| 240 | /// Placeholder widget left in place as the Hero's [child] once the flight takes |
| 241 | /// off. |
| 242 | /// |
| 243 | /// By default the placeholder widget is an empty [SizedBox] keeping the Hero |
| 244 | /// child's original size, unless this Hero is a source Hero of a [Navigator] |
| 245 | /// push transition, in which case [child] will be a descendant of the placeholder |
| 246 | /// and will be kept [Offstage] during the Hero's flight. |
| 247 | final HeroPlaceholderBuilder? placeholderBuilder; |
| 248 | |
| 249 | /// Whether to perform the hero transition if the [PageRoute] transition was |
| 250 | /// triggered by a user gesture, such as a back swipe on iOS. |
| 251 | /// |
| 252 | /// If [Hero]es with the same [tag] on both the from and the to routes have |
| 253 | /// [transitionOnUserGestures] set to true, a back swipe gesture will |
| 254 | /// trigger the same hero animation as a programmatically triggered push or |
| 255 | /// pop. |
| 256 | /// |
| 257 | /// The route being popped to or the bottom route must also have |
| 258 | /// [PageRoute.maintainState] set to true for a gesture triggered hero |
| 259 | /// transition to work. |
| 260 | /// |
| 261 | /// Defaults to false. |
| 262 | final bool transitionOnUserGestures; |
| 263 | |
| 264 | // Returns a map of all of the heroes in `context` indexed by hero tag that |
| 265 | // should be considered for animation when `navigator` transitions from one |
| 266 | // PageRoute to another. |
| 267 | static Map<Object, _HeroState> _allHeroesFor( |
| 268 | BuildContext context, |
| 269 | bool isUserGestureTransition, |
| 270 | NavigatorState navigator, |
| 271 | ) { |
| 272 | final Map<Object, _HeroState> result = <Object, _HeroState>{}; |
| 273 | |
| 274 | void inviteHero(StatefulElement hero, Object tag) { |
| 275 | assert(() { |
| 276 | if (result.containsKey(tag)) { |
| 277 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
| 278 | ErrorSummary('There are multiple heroes that share the same tag within a subtree.'), |
| 279 | ErrorDescription( |
| 280 | 'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), ' |
| 281 | 'each Hero must have a unique non-null tag.\n' |
| 282 | 'In this case, multiple heroes had the following tag:$tag ', |
| 283 | ), |
| 284 | DiagnosticsProperty<StatefulElement>( |
| 285 | 'Here is the subtree for one of the offending heroes', |
| 286 | hero, |
| 287 | linePrefix: '# ', |
| 288 | style: DiagnosticsTreeStyle.dense, |
| 289 | ), |
| 290 | ]); |
| 291 | } |
| 292 | return true; |
| 293 | }()); |
| 294 | final Hero heroWidget = hero.widget as Hero; |
| 295 | final _HeroState heroState = hero.state as _HeroState; |
| 296 | if (!isUserGestureTransition || heroWidget.transitionOnUserGestures) { |
| 297 | result[tag] = heroState; |
| 298 | } else { |
| 299 | // If transition is not allowed, we need to make sure hero is not hidden. |
| 300 | // A hero can be hidden previously due to hero transition. |
| 301 | heroState.endFlight(); |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | void visitor(Element element) { |
| 306 | final Widget widget = element.widget; |
| 307 | if (widget is Hero) { |
| 308 | final StatefulElement hero = element as StatefulElement; |
| 309 | final Object tag = widget.tag; |
| 310 | if (Navigator.of(hero) == navigator) { |
| 311 | inviteHero(hero, tag); |
| 312 | } else { |
| 313 | // The nearest navigator to the Hero is not the Navigator that is |
| 314 | // currently transitioning from one route to another. This means |
| 315 | // the Hero is inside a nested Navigator and should only be |
| 316 | // considered for animation if it is part of the top-most route in |
| 317 | // that nested Navigator and if that route is also a PageRoute. |
| 318 | final ModalRoute<Object?>? heroRoute = ModalRoute.of(hero); |
| 319 | if (heroRoute != null && heroRoute is PageRoute && heroRoute.isCurrent) { |
| 320 | inviteHero(hero, tag); |
| 321 | } |
| 322 | } |
| 323 | } else if (widget is HeroMode && !widget.enabled) { |
| 324 | return; |
| 325 | } |
| 326 | element.visitChildren(visitor); |
| 327 | } |
| 328 | |
| 329 | context.visitChildElements(visitor); |
| 330 | return result; |
| 331 | } |
| 332 | |
| 333 | @override |
| 334 | State<Hero> createState() => _HeroState(); |
| 335 | |
| 336 | @override |
| 337 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 338 | super.debugFillProperties(properties); |
| 339 | properties.add(DiagnosticsProperty<Object>('tag', tag)); |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | /// The [Hero] widget displays different content based on whether it is in an |
| 344 | /// animated transition ("flight"), from/to another [Hero] with the same tag: |
| 345 | /// * When [startFlight] is called, the real content of this [Hero] will be |
| 346 | /// replaced by a "placeholder" widget. |
| 347 | /// * When the flight ends, the "toHero"'s [endFlight] method must be called |
| 348 | /// by the hero controller, so the real content of that [Hero] becomes |
| 349 | /// visible again when the animation completes. |
| 350 | class _HeroState extends State<Hero> { |
| 351 | final GlobalKey _key = GlobalKey(); |
| 352 | Size? _placeholderSize; |
| 353 | // Whether the placeholder widget should wrap the hero's child widget as its |
| 354 | // own child, when `_placeholderSize` is non-null (i.e. the hero is currently |
| 355 | // in its flight animation). See `startFlight`. |
| 356 | bool _shouldIncludeChild = true; |
| 357 | |
| 358 | // The `shouldIncludeChildInPlaceholder` flag dictates if the child widget of |
| 359 | // this hero should be included in the placeholder widget as a descendant. |
| 360 | // |
| 361 | // When a new hero flight animation takes place, a placeholder widget |
| 362 | // needs to be built to replace the original hero widget. When |
| 363 | // `shouldIncludeChildInPlaceholder` is set to true and `widget.placeholderBuilder` |
| 364 | // is null, the placeholder widget will include the original hero's child |
| 365 | // widget as a descendant, allowing the original element tree to be preserved. |
| 366 | // |
| 367 | // It is typically set to true for the *from* hero in a push transition, |
| 368 | // and false otherwise. |
| 369 | void startFlight({bool shouldIncludedChildInPlaceholder = false}) { |
| 370 | _shouldIncludeChild = shouldIncludedChildInPlaceholder; |
| 371 | assert(mounted); |
| 372 | final RenderBox box = context.findRenderObject()! as RenderBox; |
| 373 | assert(box.hasSize); |
| 374 | setState(() { |
| 375 | _placeholderSize = box.size; |
| 376 | }); |
| 377 | } |
| 378 | |
| 379 | // When `keepPlaceholder` is true, the placeholder will continue to be shown |
| 380 | // after the flight ends. Otherwise the child of the Hero will become visible |
| 381 | // and its TickerMode will be re-enabled. |
| 382 | // |
| 383 | // This method can be safely called even when this [Hero] is currently not in |
| 384 | // a flight. |
| 385 | void endFlight({bool keepPlaceholder = false}) { |
| 386 | if (keepPlaceholder || _placeholderSize == null) { |
| 387 | return; |
| 388 | } |
| 389 | |
| 390 | _placeholderSize = null; |
| 391 | if (mounted) { |
| 392 | // Tell the widget to rebuild if it's mounted. _placeholderSize has already |
| 393 | // been updated. |
| 394 | setState(() {}); |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | @override |
| 399 | Widget build(BuildContext context) { |
| 400 | assert( |
| 401 | context.findAncestorWidgetOfExactType<Hero>() == null, |
| 402 | 'A Hero widget cannot be the descendant of another Hero widget.', |
| 403 | ); |
| 404 | |
| 405 | final bool showPlaceholder = _placeholderSize != null; |
| 406 | |
| 407 | if (showPlaceholder && widget.placeholderBuilder != null) { |
| 408 | return widget.placeholderBuilder!(context, _placeholderSize!, widget.child); |
| 409 | } |
| 410 | |
| 411 | if (showPlaceholder && !_shouldIncludeChild) { |
| 412 | return SizedBox(width: _placeholderSize!.width, height: _placeholderSize!.height); |
| 413 | } |
| 414 | |
| 415 | return SizedBox( |
| 416 | width: _placeholderSize?.width, |
| 417 | height: _placeholderSize?.height, |
| 418 | child: Offstage( |
| 419 | offstage: showPlaceholder, |
| 420 | child: TickerMode( |
| 421 | enabled: !showPlaceholder, |
| 422 | child: KeyedSubtree(key: _key, child: widget.child), |
| 423 | ), |
| 424 | ), |
| 425 | ); |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | // Everything known about a hero flight that's to be started or diverted. |
| 430 | class _HeroFlightManifest { |
| 431 | _HeroFlightManifest({ |
| 432 | required this.type, |
| 433 | required this.overlay, |
| 434 | required this.navigatorSize, |
| 435 | required this.fromRoute, |
| 436 | required this.toRoute, |
| 437 | required this.fromHero, |
| 438 | required this.toHero, |
| 439 | required this.createRectTween, |
| 440 | required this.shuttleBuilder, |
| 441 | required this.isUserGestureTransition, |
| 442 | required this.isDiverted, |
| 443 | }) : assert(fromHero.widget.tag == toHero.widget.tag); |
| 444 | |
| 445 | final HeroFlightDirection type; |
| 446 | final OverlayState overlay; |
| 447 | final Size navigatorSize; |
| 448 | final PageRoute<dynamic> fromRoute; |
| 449 | final PageRoute<dynamic> toRoute; |
| 450 | final _HeroState fromHero; |
| 451 | final _HeroState toHero; |
| 452 | final CreateRectTween? createRectTween; |
| 453 | final HeroFlightShuttleBuilder shuttleBuilder; |
| 454 | final bool isUserGestureTransition; |
| 455 | final bool isDiverted; |
| 456 | |
| 457 | Object get tag => fromHero.widget.tag; |
| 458 | |
| 459 | CurvedAnimation? _animation; |
| 460 | |
| 461 | Animation<double> get animation { |
| 462 | return _animation ??= CurvedAnimation( |
| 463 | parent: (type == HeroFlightDirection.push) ? toRoute.animation! : fromRoute.animation!, |
| 464 | curve: Curves.fastOutSlowIn, |
| 465 | reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped, |
| 466 | ); |
| 467 | } |
| 468 | |
| 469 | Tween<Rect?> createHeroRectTween({required Rect? begin, required Rect? end}) { |
| 470 | final CreateRectTween? createRectTween = toHero.widget.createRectTween ?? this.createRectTween; |
| 471 | return createRectTween?.call(begin, end) ?? RectTween(begin: begin, end: end); |
| 472 | } |
| 473 | |
| 474 | // The bounding box for `context`'s render object, in `ancestorContext`'s |
| 475 | // render object's coordinate space. |
| 476 | static Rect _boundingBoxFor(BuildContext context, BuildContext? ancestorContext) { |
| 477 | assert(ancestorContext != null); |
| 478 | final RenderBox box = context.findRenderObject()! as RenderBox; |
| 479 | assert(box.hasSize && box.size.isFinite); |
| 480 | return MatrixUtils.transformRect( |
| 481 | box.getTransformTo(ancestorContext?.findRenderObject()), |
| 482 | Offset.zero & box.size, |
| 483 | ); |
| 484 | } |
| 485 | |
| 486 | /// The bounding box of [fromHero], in [fromRoute]'s coordinate space. |
| 487 | /// |
| 488 | /// This property should only be accessed in [_HeroFlight.start]. |
| 489 | late final Rect fromHeroLocation = _boundingBoxFor(fromHero.context, fromRoute.subtreeContext); |
| 490 | |
| 491 | /// The bounding box of [toHero], in [toRoute]'s coordinate space. |
| 492 | /// |
| 493 | /// This property should only be accessed in [_HeroFlight.start] or |
| 494 | /// [_HeroFlight.divert]. |
| 495 | late final Rect toHeroLocation = _boundingBoxFor(toHero.context, toRoute.subtreeContext); |
| 496 | |
| 497 | /// Whether this [_HeroFlightManifest] is valid and can be used to start or |
| 498 | /// divert a [_HeroFlight]. |
| 499 | /// |
| 500 | /// When starting or diverting a [_HeroFlight] with a brand new |
| 501 | /// [_HeroFlightManifest], this flag must be checked to ensure the [RectTween] |
| 502 | /// the [_HeroFlightManifest] produces does not contain coordinates that have |
| 503 | /// [double.infinity] or [double.nan]. |
| 504 | late final bool isValid = toHeroLocation.isFinite && (isDiverted || fromHeroLocation.isFinite); |
| 505 | |
| 506 | @override |
| 507 | String toString() { |
| 508 | return '_HeroFlightManifest($type tag:$tag from route:${fromRoute.settings} ' |
| 509 | 'to route:${toRoute.settings} with hero:$fromHero to$toHero )${isValid ? '': ', INVALID'} '; |
| 510 | } |
| 511 | |
| 512 | @mustCallSuper |
| 513 | void dispose() { |
| 514 | _animation?.dispose(); |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | // Builds the in-flight hero widget. |
| 519 | class _HeroFlight { |
| 520 | _HeroFlight(this.onFlightEnded) { |
| 521 | assert(debugMaybeDispatchCreated('widgets', '_HeroFlight', this)); |
| 522 | _proxyAnimation = ProxyAnimation()..addStatusListener(_handleAnimationUpdate); |
| 523 | } |
| 524 | |
| 525 | final _OnFlightEnded onFlightEnded; |
| 526 | |
| 527 | late Tween<Rect?> heroRectTween; |
| 528 | Widget? shuttle; |
| 529 | |
| 530 | Animation<double> _heroOpacity = kAlwaysCompleteAnimation; |
| 531 | late ProxyAnimation _proxyAnimation; |
| 532 | // The manifest will be available once `start` is called, throughout the |
| 533 | // flight's lifecycle. |
| 534 | _HeroFlightManifest? _manifest; |
| 535 | _HeroFlightManifest get manifest => _manifest!; |
| 536 | set manifest(_HeroFlightManifest value) { |
| 537 | _manifest?.dispose(); |
| 538 | _manifest = value; |
| 539 | } |
| 540 | |
| 541 | OverlayEntry? overlayEntry; |
| 542 | bool _aborted = false; |
| 543 | |
| 544 | static final Animatable<double> _reverseTween = Tween<double>(begin: 1.0, end: 0.0); |
| 545 | |
| 546 | // The OverlayEntry WidgetBuilder callback for the hero's overlay. |
| 547 | Widget _buildOverlay(BuildContext context) { |
| 548 | shuttle ??= manifest.shuttleBuilder( |
| 549 | context, |
| 550 | manifest.animation, |
| 551 | manifest.type, |
| 552 | manifest.fromHero.context, |
| 553 | manifest.toHero.context, |
| 554 | ); |
| 555 | assert(shuttle != null); |
| 556 | |
| 557 | return AnimatedBuilder( |
| 558 | animation: _proxyAnimation, |
| 559 | child: shuttle, |
| 560 | builder: (BuildContext context, Widget? child) { |
| 561 | final Rect rect = heroRectTween.evaluate(_proxyAnimation)!; |
| 562 | final RelativeRect offsets = RelativeRect.fromSize(rect, manifest.navigatorSize); |
| 563 | return Positioned( |
| 564 | top: offsets.top, |
| 565 | right: offsets.right, |
| 566 | bottom: offsets.bottom, |
| 567 | left: offsets.left, |
| 568 | child: IgnorePointer( |
| 569 | child: FadeTransition(opacity: _heroOpacity, child: child), |
| 570 | ), |
| 571 | ); |
| 572 | }, |
| 573 | ); |
| 574 | } |
| 575 | |
| 576 | void _performAnimationUpdate(AnimationStatus status) { |
| 577 | if (!status.isAnimating) { |
| 578 | _proxyAnimation.parent = null; |
| 579 | |
| 580 | assert(overlayEntry != null); |
| 581 | overlayEntry!.remove(); |
| 582 | overlayEntry!.dispose(); |
| 583 | overlayEntry = null; |
| 584 | // We want to keep the hero underneath the current page hidden. If |
| 585 | // [AnimationStatus.completed], toHero will be the one on top and we keep |
| 586 | // fromHero hidden. If [AnimationStatus.dismissed], the animation is |
| 587 | // triggered but canceled before it finishes. In this case, we keep toHero |
| 588 | // hidden instead. |
| 589 | manifest.fromHero.endFlight(keepPlaceholder: status.isCompleted); |
| 590 | manifest.toHero.endFlight(keepPlaceholder: status.isDismissed); |
| 591 | onFlightEnded(this); |
| 592 | _proxyAnimation.removeListener(onTick); |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | bool _scheduledPerformAnimationUpdate = false; |
| 597 | void _handleAnimationUpdate(AnimationStatus status) { |
| 598 | // The animation will not finish until the user lifts their finger, so we |
| 599 | // should suppress the status update if the gesture is in progress, and |
| 600 | // delay it until the finger is lifted. |
| 601 | if (manifest.fromRoute.navigator?.userGestureInProgress != true) { |
| 602 | _performAnimationUpdate(status); |
| 603 | return; |
| 604 | } |
| 605 | |
| 606 | if (_scheduledPerformAnimationUpdate) { |
| 607 | return; |
| 608 | } |
| 609 | |
| 610 | // The `navigator` must be non-null here, or the first if clause above would |
| 611 | // have returned from this method. |
| 612 | final NavigatorState navigator = manifest.fromRoute.navigator!; |
| 613 | |
| 614 | void delayedPerformAnimationUpdate() { |
| 615 | assert(!navigator.userGestureInProgress); |
| 616 | assert(_scheduledPerformAnimationUpdate); |
| 617 | _scheduledPerformAnimationUpdate = false; |
| 618 | navigator.userGestureInProgressNotifier.removeListener(delayedPerformAnimationUpdate); |
| 619 | _performAnimationUpdate(_proxyAnimation.status); |
| 620 | } |
| 621 | |
| 622 | assert(navigator.userGestureInProgress); |
| 623 | _scheduledPerformAnimationUpdate = true; |
| 624 | navigator.userGestureInProgressNotifier.addListener(delayedPerformAnimationUpdate); |
| 625 | } |
| 626 | |
| 627 | /// Releases resources. |
| 628 | @mustCallSuper |
| 629 | void dispose() { |
| 630 | assert(debugMaybeDispatchDisposed(this)); |
| 631 | if (overlayEntry != null) { |
| 632 | overlayEntry!.remove(); |
| 633 | overlayEntry!.dispose(); |
| 634 | overlayEntry = null; |
| 635 | _proxyAnimation.parent = null; |
| 636 | _proxyAnimation.removeListener(onTick); |
| 637 | _proxyAnimation.removeStatusListener(_handleAnimationUpdate); |
| 638 | } |
| 639 | _manifest?.dispose(); |
| 640 | } |
| 641 | |
| 642 | void onTick() { |
| 643 | final RenderBox? toHeroBox = (!_aborted && manifest.toHero.mounted) |
| 644 | ? manifest.toHero.context.findRenderObject() as RenderBox? |
| 645 | : null; |
| 646 | // Try to find the new origin of the toHero, if the flight isn't aborted. |
| 647 | final Offset? toHeroOrigin = toHeroBox != null && toHeroBox.attached && toHeroBox.hasSize |
| 648 | ? toHeroBox.localToGlobal( |
| 649 | Offset.zero, |
| 650 | ancestor: manifest.toRoute.subtreeContext?.findRenderObject() as RenderBox?, |
| 651 | ) |
| 652 | : null; |
| 653 | |
| 654 | if (toHeroOrigin != null && toHeroOrigin.isFinite) { |
| 655 | // If the new origin of toHero is available and also paintable, try to |
| 656 | // update heroRectTween with it. |
| 657 | if (toHeroOrigin != heroRectTween.end!.topLeft) { |
| 658 | final Rect heroRectEnd = toHeroOrigin & heroRectTween.end!.size; |
| 659 | heroRectTween = manifest.createHeroRectTween(begin: heroRectTween.begin, end: heroRectEnd); |
| 660 | } |
| 661 | } else if (_heroOpacity.isCompleted) { |
| 662 | // The toHero no longer exists or it's no longer the flight's destination. |
| 663 | // Continue flying while fading out. |
| 664 | _heroOpacity = _proxyAnimation.drive( |
| 665 | _reverseTween.chain(CurveTween(curve: Interval(_proxyAnimation.value, 1.0))), |
| 666 | ); |
| 667 | } |
| 668 | // Update _aborted for the next animation tick. |
| 669 | _aborted = toHeroOrigin == null || !toHeroOrigin.isFinite; |
| 670 | } |
| 671 | |
| 672 | // The simple case: we're either starting a push or a pop animation. |
| 673 | void start(_HeroFlightManifest initialManifest) { |
| 674 | assert(!_aborted); |
| 675 | assert(() { |
| 676 | final Animation<double> initial = initialManifest.animation; |
| 677 | final HeroFlightDirection type = initialManifest.type; |
| 678 | switch (type) { |
| 679 | case HeroFlightDirection.pop: |
| 680 | return initialManifest.isUserGestureTransition |
| 681 | // During user gesture transitions, the animation controller isn't |
| 682 | // driving the reverse transition, so the status is not important. |
| 683 | || |
| 684 | initial.status == AnimationStatus.reverse; |
| 685 | case HeroFlightDirection.push: |
| 686 | return initial.value == 0.0 && initial.status == AnimationStatus.forward; |
| 687 | } |
| 688 | }()); |
| 689 | |
| 690 | manifest = initialManifest; |
| 691 | |
| 692 | final bool shouldIncludeChildInPlaceholder; |
| 693 | switch (manifest.type) { |
| 694 | case HeroFlightDirection.pop: |
| 695 | _proxyAnimation.parent = ReverseAnimation(manifest.animation); |
| 696 | shouldIncludeChildInPlaceholder = false; |
| 697 | case HeroFlightDirection.push: |
| 698 | _proxyAnimation.parent = manifest.animation; |
| 699 | shouldIncludeChildInPlaceholder = true; |
| 700 | } |
| 701 | |
| 702 | heroRectTween = manifest.createHeroRectTween( |
| 703 | begin: manifest.fromHeroLocation, |
| 704 | end: manifest.toHeroLocation, |
| 705 | ); |
| 706 | manifest.fromHero.startFlight( |
| 707 | shouldIncludedChildInPlaceholder: shouldIncludeChildInPlaceholder, |
| 708 | ); |
| 709 | manifest.toHero.startFlight(); |
| 710 | manifest.overlay.insert(overlayEntry = OverlayEntry(builder: _buildOverlay)); |
| 711 | _proxyAnimation.addListener(onTick); |
| 712 | } |
| 713 | |
| 714 | // While this flight's hero was in transition a push or a pop occurred for |
| 715 | // routes with the same hero. Redirect the in-flight hero to the new toRoute. |
| 716 | void divert(_HeroFlightManifest newManifest) { |
| 717 | assert(manifest.tag == newManifest.tag); |
| 718 | if (manifest.type == HeroFlightDirection.push && newManifest.type == HeroFlightDirection.pop) { |
| 719 | // A push flight was interrupted by a pop. |
| 720 | assert(newManifest.animation.status == AnimationStatus.reverse); |
| 721 | assert(manifest.fromHero == newManifest.toHero); |
| 722 | assert(manifest.toHero == newManifest.fromHero); |
| 723 | assert(manifest.fromRoute == newManifest.toRoute); |
| 724 | assert(manifest.toRoute == newManifest.fromRoute); |
| 725 | |
| 726 | // The same heroRect tween is used in reverse, rather than creating |
| 727 | // a new heroRect with _doCreateRectTween(heroRect.end, heroRect.begin). |
| 728 | // That's because tweens like MaterialRectArcTween may create a different |
| 729 | // path for swapped begin and end parameters. We want the pop flight |
| 730 | // path to be the same (in reverse) as the push flight path. |
| 731 | _proxyAnimation.parent = ReverseAnimation(newManifest.animation); |
| 732 | heroRectTween = ReverseTween<Rect?>(heroRectTween); |
| 733 | } else if (manifest.type == HeroFlightDirection.pop && |
| 734 | newManifest.type == HeroFlightDirection.push) { |
| 735 | // A pop flight was interrupted by a push. |
| 736 | assert(newManifest.animation.status == AnimationStatus.forward); |
| 737 | assert(manifest.toHero == newManifest.fromHero); |
| 738 | assert(manifest.toRoute == newManifest.fromRoute); |
| 739 | |
| 740 | _proxyAnimation.parent = newManifest.animation.drive( |
| 741 | Tween<double>(begin: manifest.animation.value, end: 1.0), |
| 742 | ); |
| 743 | if (manifest.fromHero != newManifest.toHero) { |
| 744 | manifest.fromHero.endFlight(keepPlaceholder: true); |
| 745 | newManifest.toHero.startFlight(); |
| 746 | heroRectTween = manifest.createHeroRectTween( |
| 747 | begin: heroRectTween.end, |
| 748 | end: newManifest.toHeroLocation, |
| 749 | ); |
| 750 | } else { |
| 751 | // TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203. |
| 752 | heroRectTween = manifest.createHeroRectTween( |
| 753 | begin: heroRectTween.end, |
| 754 | end: heroRectTween.begin, |
| 755 | ); |
| 756 | } |
| 757 | } else { |
| 758 | // A push or a pop flight is heading to a new route, i.e. |
| 759 | // manifest.type == _HeroFlightType.push && newManifest.type == _HeroFlightType.push || |
| 760 | // manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.pop |
| 761 | assert(manifest.fromHero != newManifest.fromHero); |
| 762 | assert(manifest.toHero != newManifest.toHero); |
| 763 | |
| 764 | heroRectTween = manifest.createHeroRectTween( |
| 765 | begin: heroRectTween.evaluate(_proxyAnimation), |
| 766 | end: newManifest.toHeroLocation, |
| 767 | ); |
| 768 | shuttle = null; |
| 769 | |
| 770 | if (newManifest.type == HeroFlightDirection.pop) { |
| 771 | _proxyAnimation.parent = ReverseAnimation(newManifest.animation); |
| 772 | } else { |
| 773 | _proxyAnimation.parent = newManifest.animation; |
| 774 | } |
| 775 | |
| 776 | manifest.fromHero.endFlight(keepPlaceholder: true); |
| 777 | manifest.toHero.endFlight(keepPlaceholder: true); |
| 778 | |
| 779 | // Let the heroes in each of the routes rebuild with their placeholders. |
| 780 | newManifest.fromHero.startFlight( |
| 781 | shouldIncludedChildInPlaceholder: newManifest.type == HeroFlightDirection.push, |
| 782 | ); |
| 783 | newManifest.toHero.startFlight(); |
| 784 | |
| 785 | // Let the transition overlay on top of the routes also rebuild since |
| 786 | // we cleared the old shuttle. |
| 787 | overlayEntry!.markNeedsBuild(); |
| 788 | } |
| 789 | |
| 790 | manifest = newManifest; |
| 791 | } |
| 792 | |
| 793 | void abort() { |
| 794 | _aborted = true; |
| 795 | } |
| 796 | |
| 797 | @override |
| 798 | String toString() { |
| 799 | final RouteSettings from = manifest.fromRoute.settings; |
| 800 | final RouteSettings to = manifest.toRoute.settings; |
| 801 | final Object tag = manifest.tag; |
| 802 | return 'HeroFlight(for:$tag , from:$from , to:$to ${_proxyAnimation.parent} )'; |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | /// A [Navigator] observer that manages [Hero] transitions. |
| 807 | /// |
| 808 | /// An instance of [HeroController] should be used in [Navigator.observers]. |
| 809 | /// This is done automatically by [MaterialApp]. |
| 810 | class HeroController extends NavigatorObserver { |
| 811 | /// Creates a hero controller with the given [RectTween] constructor if any. |
| 812 | /// |
| 813 | /// The [createRectTween] argument is optional. If null, the controller uses a |
| 814 | /// linear [Tween<Rect>]. |
| 815 | HeroController({this.createRectTween}) { |
| 816 | assert(debugMaybeDispatchCreated('widgets', 'HeroController', this)); |
| 817 | } |
| 818 | |
| 819 | /// Used to create [RectTween]s that interpolate the position of heroes in flight. |
| 820 | /// |
| 821 | /// If null, the controller uses a linear [RectTween]. |
| 822 | final CreateRectTween? createRectTween; |
| 823 | |
| 824 | // All of the heroes that are currently in the overlay and in motion. |
| 825 | // Indexed by the hero tag. |
| 826 | final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{}; |
| 827 | |
| 828 | @override |
| 829 | void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) { |
| 830 | assert(topRoute.isCurrent); |
| 831 | assert(navigator != null); |
| 832 | if (previousTopRoute == null) { |
| 833 | return; |
| 834 | } |
| 835 | // Don't trigger another flight when a pop is committed as a user gesture |
| 836 | // back swipe is snapped. |
| 837 | if (!navigator!.userGestureInProgress) { |
| 838 | _maybeStartHeroTransition( |
| 839 | fromRoute: previousTopRoute, |
| 840 | toRoute: topRoute, |
| 841 | isUserGestureTransition: false, |
| 842 | ); |
| 843 | } |
| 844 | } |
| 845 | |
| 846 | @override |
| 847 | void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) { |
| 848 | assert(navigator != null); |
| 849 | _maybeStartHeroTransition( |
| 850 | fromRoute: route, |
| 851 | toRoute: previousRoute, |
| 852 | isUserGestureTransition: true, |
| 853 | ); |
| 854 | } |
| 855 | |
| 856 | @override |
| 857 | void didStopUserGesture() { |
| 858 | if (navigator!.userGestureInProgress) { |
| 859 | return; |
| 860 | } |
| 861 | |
| 862 | // When the user gesture ends, if the user horizontal drag gesture initiated |
| 863 | // the flight (i.e. the back swipe) didn't move towards the pop direction at |
| 864 | // all, the animation will not play and thus the status update callback |
| 865 | // _handleAnimationUpdate will never be called when the gesture finishes. In |
| 866 | // this case the initiated flight needs to be manually invalidated. |
| 867 | bool isInvalidFlight(_HeroFlight flight) { |
| 868 | return flight.manifest.isUserGestureTransition && |
| 869 | flight.manifest.type == HeroFlightDirection.pop && |
| 870 | flight._proxyAnimation.isDismissed; |
| 871 | } |
| 872 | |
| 873 | final List<_HeroFlight> invalidFlights = _flights.values |
| 874 | .where(isInvalidFlight) |
| 875 | .toList(growable: false); |
| 876 | |
| 877 | // Treat these invalidated flights as dismissed. Calling _handleAnimationUpdate |
| 878 | // will also remove the flight from _flights. |
| 879 | for (final _HeroFlight flight in invalidFlights) { |
| 880 | flight._handleAnimationUpdate(AnimationStatus.dismissed); |
| 881 | } |
| 882 | } |
| 883 | |
| 884 | // If we're transitioning between different page routes, start a hero transition |
| 885 | // after the toRoute has been laid out with its animation's value at 1.0. |
| 886 | void _maybeStartHeroTransition({ |
| 887 | required Route<dynamic>? fromRoute, |
| 888 | required Route<dynamic>? toRoute, |
| 889 | required bool isUserGestureTransition, |
| 890 | }) { |
| 891 | if (toRoute == fromRoute || |
| 892 | toRoute is! PageRoute<dynamic> || |
| 893 | fromRoute is! PageRoute<dynamic>) { |
| 894 | return; |
| 895 | } |
| 896 | final Animation<double> newRouteAnimation = toRoute.animation!; |
| 897 | final Animation<double> oldRouteAnimation = fromRoute.animation!; |
| 898 | final HeroFlightDirection? flightType; |
| 899 | switch ((isUserGestureTransition, oldRouteAnimation.status, newRouteAnimation.status)) { |
| 900 | case (true, _, _): |
| 901 | case (_, AnimationStatus.reverse, _): |
| 902 | flightType = HeroFlightDirection.pop; |
| 903 | case (_, _, AnimationStatus.forward): |
| 904 | flightType = HeroFlightDirection.push; |
| 905 | default: |
| 906 | flightType = null; |
| 907 | } |
| 908 | |
| 909 | // A user gesture may have already completed the pop, or we might be the initial route |
| 910 | if (flightType != null) { |
| 911 | switch (flightType) { |
| 912 | case HeroFlightDirection.pop: |
| 913 | if (fromRoute.animation!.value == 0.0) { |
| 914 | return; |
| 915 | } |
| 916 | case HeroFlightDirection.push: |
| 917 | if (toRoute.animation!.value == 1.0) { |
| 918 | return; |
| 919 | } |
| 920 | } |
| 921 | } |
| 922 | |
| 923 | // For pop transitions driven by a user gesture: if the "to" page has |
| 924 | // maintainState = true, then the hero's final dimensions can be measured |
| 925 | // immediately because their page's layout is still valid. Unless due to directly |
| 926 | // adding routes to the pages stack causing the route to never get laid out. |
| 927 | final RenderBox? fromRouteRenderBox = toRoute.subtreeContext?.findRenderObject() as RenderBox?; |
| 928 | final bool hasValidSize = |
| 929 | (fromRouteRenderBox?.hasSize ?? false) && fromRouteRenderBox!.size.isFinite; |
| 930 | if (isUserGestureTransition && |
| 931 | flightType == HeroFlightDirection.pop && |
| 932 | toRoute.maintainState && |
| 933 | hasValidSize) { |
| 934 | _startHeroTransition(fromRoute, toRoute, flightType, isUserGestureTransition); |
| 935 | } else { |
| 936 | // Otherwise, delay measuring until the end of the next frame to allow |
| 937 | // the 'to' route to build and layout. |
| 938 | |
| 939 | // Putting a route offstage changes its animation value to 1.0. Once this |
| 940 | // frame completes, we'll know where the heroes in the `to` route are |
| 941 | // going to end up, and the `to` route will go back onstage. |
| 942 | toRoute.offstage = toRoute.animation!.value == 0.0; |
| 943 | WidgetsBinding.instance.addPostFrameCallback((Duration value) { |
| 944 | if (fromRoute.navigator == null || toRoute.navigator == null) { |
| 945 | return; |
| 946 | } |
| 947 | _startHeroTransition(fromRoute, toRoute, flightType, isUserGestureTransition); |
| 948 | }, debugLabel: 'HeroController.startTransition'); |
| 949 | } |
| 950 | } |
| 951 | |
| 952 | // Find the matching pairs of heroes in from and to and either start or a new |
| 953 | // hero flight, or divert an existing one. |
| 954 | void _startHeroTransition( |
| 955 | PageRoute<dynamic> from, |
| 956 | PageRoute<dynamic> to, |
| 957 | HeroFlightDirection? flightType, |
| 958 | bool isUserGestureTransition, |
| 959 | ) { |
| 960 | // If the `to` route was offstage, then we're implicitly restoring its |
| 961 | // animation value back to what it was before it was "moved" offstage. |
| 962 | to.offstage = false; |
| 963 | |
| 964 | final NavigatorState? navigator = this.navigator; |
| 965 | final OverlayState? overlay = navigator?.overlay; |
| 966 | // If the navigator or the overlay was removed before this end-of-frame |
| 967 | // callback was called, then don't actually start a transition, and we don't |
| 968 | // have to worry about any Hero widget we might have hidden in a previous |
| 969 | // flight, or ongoing flights. |
| 970 | if (navigator == null || overlay == null) { |
| 971 | return; |
| 972 | } |
| 973 | |
| 974 | final RenderObject? navigatorRenderObject = navigator.context.findRenderObject(); |
| 975 | |
| 976 | if (navigatorRenderObject is! RenderBox) { |
| 977 | assert( |
| 978 | false, |
| 979 | 'Navigator$navigator has an invalid RenderObject type${navigatorRenderObject.runtimeType} .', |
| 980 | ); |
| 981 | return; |
| 982 | } |
| 983 | assert(navigatorRenderObject.hasSize); |
| 984 | |
| 985 | // At this point, the toHeroes may have been built and laid out for the first time. |
| 986 | // |
| 987 | // If `fromSubtreeContext` is null, call endFlight on all toHeroes, for good measure. |
| 988 | // If `toSubtreeContext` is null abort existingFlights. |
| 989 | final BuildContext? fromSubtreeContext = from.subtreeContext; |
| 990 | final Map<Object, _HeroState> fromHeroes = fromSubtreeContext != null |
| 991 | ? Hero._allHeroesFor(fromSubtreeContext, isUserGestureTransition, navigator) |
| 992 | : const <Object, _HeroState>{}; |
| 993 | final BuildContext? toSubtreeContext = to.subtreeContext; |
| 994 | final Map<Object, _HeroState> toHeroes = toSubtreeContext != null |
| 995 | ? Hero._allHeroesFor(toSubtreeContext, isUserGestureTransition, navigator) |
| 996 | : const <Object, _HeroState>{}; |
| 997 | |
| 998 | for (final MapEntry<Object, _HeroState> fromHeroEntry in fromHeroes.entries) { |
| 999 | final Object tag = fromHeroEntry.key; |
| 1000 | final _HeroState fromHero = fromHeroEntry.value; |
| 1001 | final _HeroState? toHero = toHeroes[tag]; |
| 1002 | final _HeroFlight? existingFlight = _flights[tag]; |
| 1003 | final _HeroFlightManifest? manifest = toHero == null || flightType == null |
| 1004 | ? null |
| 1005 | : _HeroFlightManifest( |
| 1006 | type: flightType, |
| 1007 | overlay: overlay, |
| 1008 | navigatorSize: navigatorRenderObject.size, |
| 1009 | fromRoute: from, |
| 1010 | toRoute: to, |
| 1011 | fromHero: fromHero, |
| 1012 | toHero: toHero, |
| 1013 | createRectTween: createRectTween, |
| 1014 | shuttleBuilder: |
| 1015 | toHero.widget.flightShuttleBuilder ?? |
| 1016 | fromHero.widget.flightShuttleBuilder ?? |
| 1017 | _defaultHeroFlightShuttleBuilder, |
| 1018 | isUserGestureTransition: isUserGestureTransition, |
| 1019 | isDiverted: existingFlight != null, |
| 1020 | ); |
| 1021 | |
| 1022 | // Only proceed with a valid manifest. Otherwise abort the existing |
| 1023 | // flight, and call endFlight when this for loop finishes. |
| 1024 | if (manifest != null && manifest.isValid) { |
| 1025 | toHeroes.remove(tag); |
| 1026 | if (existingFlight != null) { |
| 1027 | existingFlight.divert(manifest); |
| 1028 | } else { |
| 1029 | _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest); |
| 1030 | } |
| 1031 | } else { |
| 1032 | existingFlight?.abort(); |
| 1033 | } |
| 1034 | } |
| 1035 | |
| 1036 | // The remaining entries in toHeroes are those failed to participate in a |
| 1037 | // new flight (for not having a valid manifest). |
| 1038 | // |
| 1039 | // This can happen in a route pop transition when a fromHero is no longer |
| 1040 | // mounted, or kept alive by the [KeepAlive] mechanism but no longer visible. |
| 1041 | // TODO(LongCatIsLooong): resume aborted flights: https://github.com/flutter/flutter/issues/72947 |
| 1042 | for (final _HeroState toHero in toHeroes.values) { |
| 1043 | toHero.endFlight(); |
| 1044 | } |
| 1045 | } |
| 1046 | |
| 1047 | void _handleFlightEnded(_HeroFlight flight) { |
| 1048 | _flights.remove(flight.manifest.tag)?.dispose(); |
| 1049 | } |
| 1050 | |
| 1051 | Widget _defaultHeroFlightShuttleBuilder( |
| 1052 | BuildContext flightContext, |
| 1053 | Animation<double> animation, |
| 1054 | HeroFlightDirection flightDirection, |
| 1055 | BuildContext fromHeroContext, |
| 1056 | BuildContext toHeroContext, |
| 1057 | ) { |
| 1058 | final Hero toHero = toHeroContext.widget as Hero; |
| 1059 | |
| 1060 | final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext); |
| 1061 | final MediaQueryData? fromMediaQueryData = MediaQuery.maybeOf(fromHeroContext); |
| 1062 | |
| 1063 | if (toMediaQueryData == null || fromMediaQueryData == null) { |
| 1064 | return toHero.child; |
| 1065 | } |
| 1066 | |
| 1067 | final EdgeInsets fromHeroPadding = fromMediaQueryData.padding; |
| 1068 | final EdgeInsets toHeroPadding = toMediaQueryData.padding; |
| 1069 | |
| 1070 | return AnimatedBuilder( |
| 1071 | animation: animation, |
| 1072 | builder: (BuildContext context, Widget? child) { |
| 1073 | return MediaQuery( |
| 1074 | data: toMediaQueryData.copyWith( |
| 1075 | padding: (flightDirection == HeroFlightDirection.push) |
| 1076 | ? EdgeInsetsTween(begin: fromHeroPadding, end: toHeroPadding).evaluate(animation) |
| 1077 | : EdgeInsetsTween(begin: toHeroPadding, end: fromHeroPadding).evaluate(animation), |
| 1078 | ), |
| 1079 | child: toHero.child, |
| 1080 | ); |
| 1081 | }, |
| 1082 | ); |
| 1083 | } |
| 1084 | |
| 1085 | /// Releases resources. |
| 1086 | @mustCallSuper |
| 1087 | void dispose() { |
| 1088 | assert(debugMaybeDispatchDisposed(this)); |
| 1089 | for (final _HeroFlight flight in _flights.values) { |
| 1090 | flight.dispose(); |
| 1091 | } |
| 1092 | } |
| 1093 | } |
| 1094 | |
| 1095 | /// Enables or disables [Hero]es in the widget subtree. |
| 1096 | /// |
| 1097 | /// {@youtube 560 315 https://www.youtube.com/watch?v=AaIASk2u1C0} |
| 1098 | /// |
| 1099 | /// When [enabled] is false, all [Hero] widgets in this subtree will not be |
| 1100 | /// involved in hero animations. |
| 1101 | /// |
| 1102 | /// When [enabled] is true (the default), [Hero] widgets may be involved in |
| 1103 | /// hero animations, as usual. |
| 1104 | class HeroMode extends StatelessWidget { |
| 1105 | /// Creates a widget that enables or disables [Hero]es. |
| 1106 | const HeroMode({super.key, required this.child, this.enabled = true}); |
| 1107 | |
| 1108 | /// The subtree to place inside the [HeroMode]. |
| 1109 | final Widget child; |
| 1110 | |
| 1111 | /// Whether or not [Hero]es are enabled in this subtree. |
| 1112 | /// |
| 1113 | /// If this property is false, the [Hero]es in this subtree will not animate |
| 1114 | /// on route changes. Otherwise, they will animate as usual. |
| 1115 | /// |
| 1116 | /// Defaults to true. |
| 1117 | final bool enabled; |
| 1118 | |
| 1119 | @override |
| 1120 | Widget build(BuildContext context) => child; |
| 1121 | |
| 1122 | @override |
| 1123 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 1124 | super.debugFillProperties(properties); |
| 1125 | properties.add( |
| 1126 | FlagProperty('mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true), |
| 1127 | ); |
| 1128 | } |
| 1129 | } |
| 1130 |
Definitions
- HeroFlightDirection
- Hero
- Hero
- _allHeroesFor
- inviteHero
- visitor
- createState
- debugFillProperties
- _HeroState
- startFlight
- endFlight
- build
- _HeroFlightManifest
- _HeroFlightManifest
- tag
- animation
- createHeroRectTween
- _boundingBoxFor
- toString
- dispose
- _HeroFlight
- _HeroFlight
- manifest
- manifest
- _buildOverlay
- _performAnimationUpdate
- _handleAnimationUpdate
- delayedPerformAnimationUpdate
- dispose
- onTick
- start
- divert
- abort
- toString
- HeroController
- HeroController
- didChangeTop
- didStartUserGesture
- didStopUserGesture
- isInvalidFlight
- _maybeStartHeroTransition
- _startHeroTransition
- _handleFlightEnded
- _defaultHeroFlightShuttleBuilder
- dispose
- HeroMode
- HeroMode
- build
Learn more about Flutter for embedded and desktop on industrialflutter.com
