Allow heroes to fly across navigators and restrict Cupertino nav bars to per navigator by default (#23322)
diff --git a/examples/flutter_gallery/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/flutter_gallery/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..949b678
--- /dev/null
+++ b/examples/flutter_gallery/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>BuildSystemType</key>
+ <string>Original</string>
+</dict>
+</plist>
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index 804fa14..de9bda4 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -65,13 +65,33 @@
// There's a single tag for all instances of navigation bars because they can
// all transition between each other (per Navigator) via Hero transitions.
-const _HeroTag _defaultHeroTag = _HeroTag();
+const _HeroTag _defaultHeroTag = _HeroTag(null);
class _HeroTag {
- const _HeroTag();
+ const _HeroTag(this.navigator);
+
+ final NavigatorState navigator;
+
// Let the Hero tag be described in tree dumps.
@override
- String toString() => 'Default Hero tag for Cupertino navigation bars';
+ String toString() => 'Default Hero tag for Cupertino navigation bars with navigator $navigator';
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ final _HeroTag otherTag = other;
+ return navigator == otherTag.navigator;
+ }
+
+ @override
+ int get hashCode {
+ return identityHashCode(navigator);
+ }
}
TextStyle _navBarItemStyle(Color color) {
@@ -331,9 +351,13 @@
/// Tag for the navigation bar's Hero widget if [transitionBetweenRoutes] is true.
///
/// Defaults to a common tag between all [CupertinoNavigationBar] and
- /// [CupertinoSliverNavigationBar] instances so they can all transition
- /// between each other as long as there's only one per route. Use this tag
- /// override with different tags to have multiple navigation bars per route.
+ /// [CupertinoSliverNavigationBar] instances of the same [Navigator]. With the
+ /// default tag, all navigation bars of the same navigator can transition
+ /// between each other as long as there's only one navigation bar per route.
+ ///
+ /// This [heroTag] can be overridden to manually handle having multiple
+ /// navigation bars per route or to transition between multiple
+ /// [Navigator]s.
///
/// Cannot be null. To disable Hero transitions for this navigation bar,
/// set [transitionBetweenRoutes] to false.
@@ -398,7 +422,9 @@
}
return Hero(
- tag: widget.heroTag,
+ tag: widget.heroTag == _defaultHeroTag
+ ? _HeroTag(Navigator.of(context))
+ : widget.heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
@@ -733,7 +759,9 @@
}
return Hero(
- tag: heroTag,
+ tag: heroTag == _defaultHeroTag
+ ? _HeroTag(Navigator.of(context))
+ : heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart
index c9d03df..7887114 100644
--- a/packages/flutter/lib/src/widgets/heroes.dart
+++ b/packages/flutter/lib/src/widgets/heroes.dart
@@ -222,10 +222,6 @@
result[tag] = heroState;
}
}
- // Don't perform transitions across different Navigators.
- if (element.widget is Navigator) {
- return;
- }
element.visitChildren(visitor);
}
context.visitChildElements(visitor);
diff --git a/packages/flutter/test/cupertino/nav_bar_transition_test.dart b/packages/flutter/test/cupertino/nav_bar_transition_test.dart
index ea3d146..72de5c9 100644
--- a/packages/flutter/test/cupertino/nav_bar_transition_test.dart
+++ b/packages/flutter/test/cupertino/nav_bar_transition_test.dart
@@ -385,6 +385,82 @@
);
});
+ testWidgets('Multiple nav bars tags do not conflict if in different navigators',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ home: CupertinoTabScaffold(
+ tabBar: CupertinoTabBar(
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.search),
+ title: Text('Tab 1'),
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.settings),
+ title: Text('Tab 2'),
+ ),
+ ],
+ ),
+ tabBuilder: (BuildContext context, int tab) {
+ return CupertinoTabView(
+ builder: (BuildContext context) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: Text('Tab ${tab + 1} Page 1'),
+ ),
+ child: Center(
+ child: CupertinoButton(
+ child: const Text('Next'),
+ onPressed: () {
+ Navigator.push<void>(context, CupertinoPageRoute<void>(
+ title: 'Tab ${tab + 1} Page 2',
+ builder: (BuildContext context) {
+ return const CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(),
+ child: Placeholder(),
+ );
+ }
+ ));
+ },
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ );
+
+ await tester.tap(find.text('Tab 2'));
+ await tester.pump();
+
+ expect(find.text('Tab 1 Page 1', skipOffstage: false), findsOneWidget);
+ expect(find.text('Tab 2 Page 1'), findsOneWidget);
+
+ // At this point, there are 2 nav bars seeded with the same _defaultHeroTag.
+ // But they're inside different navigators.
+
+ await tester.tap(find.text('Next'));
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 200));
+
+ // One is inside the flight shuttle and another is invisible in the
+ // incoming route in case a new flight needs to be created midflight.
+ expect(find.text('Tab 2 Page 2'), findsNWidgets(2));
+
+ await tester.pump(const Duration(milliseconds: 500));
+
+ expect(find.text('Tab 2 Page 2'), findsOneWidget);
+ // Offstaged by tab 2's navigator.
+ expect(find.text('Tab 2 Page 1', skipOffstage: false), findsOneWidget);
+ // Offstaged by the CupertinoTabScaffold.
+ expect(find.text('Tab 1 Page 1', skipOffstage: false), findsOneWidget);
+ // Never navigated to tab 1 page 2.
+ expect(find.text('Tab 1 Page 2', skipOffstage: false), findsNothing);
+ });
+
testWidgets('Transition box grows to large title size',
(WidgetTester tester) async {
await startTransitionBetween(