Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heroes and nested Navigators #29069

Merged
merged 7 commits into from Mar 12, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 45 additions & 17 deletions packages/flutter/lib/src/widgets/heroes.dart
Expand Up @@ -10,6 +10,7 @@ import 'framework.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
import 'routes.dart';
import 'transitions.dart';

/// Signature for a function that takes two [Rect] instances and returns a
Expand Down Expand Up @@ -110,6 +111,14 @@ Rect _globalBoundingBoxFor(BuildContext context) {
/// 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.
///
/// ## Nested Navigators
goderbauer marked this conversation as resolved.
Show resolved Hide resolved
///
/// If either or both routes contain nested [Navigator]s, only [Hero]s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there aren't any nested navigators we still only consider heroes from the topmost route?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important bit is that in the nested navigator case, its the topmost route of the nested navigator (and not the topmost route of the navigator controlling the hero transition)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rephrased this a bit and emphasized nested navigators.

/// contained in the top-most routes (as defined by [Route.isCurrent] of those
/// nested [Navigator]s are considered for animation. Furthermore, the top-most
/// routes containing these [Hero]s in the nested [Navigator]s also have to be
/// [PageRoute]s.
///
/// ## Parts of a Hero Transition
///
/// ![Diagrams with parts of the Hero transition.](https://flutter.github.io/assets-for-api-docs/assets/interaction/heroes.png)
Expand Down Expand Up @@ -194,36 +203,55 @@ class Hero extends StatefulWidget {
final bool transitionOnUserGestures;

// Returns a map of all of the heroes in context, indexed by hero tag.
static Map<Object, _HeroState> _allHeroesFor(BuildContext context, bool isUserGestureTransition) {
static Map<Object, _HeroState> _allHeroesFor(
BuildContext context,
bool isUserGestureTransition,
NavigatorState triggeringNavigator,
goderbauer marked this conversation as resolved.
Show resolved Hide resolved
) {
assert(context != null);
assert(isUserGestureTransition != null);
final Map<Object, _HeroState> result = <Object, _HeroState>{};

void addHero(StatefulElement hero, Object tag) {
assert(() {
if (result.containsKey(tag)) {
throw FlutterError(
'There are multiple heroes that share the same tag within a subtree.\n'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
goderbauer marked this conversation as resolved.
Show resolved Hide resolved
'each Hero must have a unique non-null tag.\n'
'In this case, multiple heroes had the following tag: $tag\n'
'Here is the subtree for one of the offending heroes:\n'
'${hero.toStringDeep(prefixLineOne: "# ")}'
);
}
return true;
}());
final _HeroState heroState = hero.state;
result[tag] = heroState;
}

void visitor(Element element) {
if (element.widget is Hero) {
final StatefulElement hero = element;
final Hero heroWidget = element.widget;
if (!isUserGestureTransition || heroWidget.transitionOnUserGestures) {
final Object tag = heroWidget.tag;
assert(tag != null);
assert(() {
if (result.containsKey(tag)) {
throw FlutterError(
'There are multiple heroes that share the same tag within a subtree.\n'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
'each Hero must have a unique non-null tag.\n'
'In this case, multiple heroes had the following tag: $tag\n'
'Here is the subtree for one of the offending heroes:\n'
'${element.toStringDeep(prefixLineOne: "# ")}'
);
if (Navigator.of(hero) == triggeringNavigator) {
goderbauer marked this conversation as resolved.
Show resolved Hide resolved
addHero(hero, tag);
} else {
// Nested navigators: only consider heros in the topmost route of
goderbauer marked this conversation as resolved.
Show resolved Hide resolved
// the nested navigator, if it is a PageRoute.
final ModalRoute<dynamic> heroRoute = ModalRoute.of(hero);
if (heroRoute != null && heroRoute is PageRoute && heroRoute.isCurrent) {
addHero(hero, tag);
}
return true;
}());
final _HeroState heroState = hero.state;
result[tag] = heroState;
}
}
}
element.visitChildren(visitor);
}

context.visitChildElements(visitor);
return result;
}
Expand Down Expand Up @@ -652,8 +680,8 @@ class HeroController extends NavigatorObserver {
final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);

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

// If the `to` route was offstage, then we're implicitly restoring its
// animation value back to what it was before it was "moved" offstage.
Expand Down