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 00000000000..949b6789820 --- /dev/null +++ b/examples/flutter_gallery/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index 804fa14437d..de9bda442a6 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -65,13 +65,33 @@ const TextStyle _kLargeTitleTextStyle = TextStyle( // 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 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// 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 @@ class _CupertinoNavigationBarState extends State { } 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 @@ class _LargeTitleNavigationBarSliverDelegate } 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 c9d03dfc9c6..78871141902 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -222,10 +222,6 @@ class Hero extends StatefulWidget { 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 ea3d146e746..72de5c90d0e 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 @@ void main() { ); }); + 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( + 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(context, CupertinoPageRoute( + 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(