From cd433d4d365b25e880162ce006bda8eb838c6138 Mon Sep 17 00:00:00 2001 From: Victor Sanni Date: Mon, 10 Mar 2025 12:36:04 -0700 Subject: [PATCH] Align nav bar bottom transition with large title animation (#162097) Makes the bottom widget sync up with the large title in hero transitions between nav bars. ## Before https://github.com/user-attachments/assets/3f8c67c3-20c2-4751-b29b-7db8d3f3409f ## After https://github.com/user-attachments/assets/5e4c966f-1818-4851-87a1-0bf613ebda0b ## Native searchable-to-searchable: https://github.com/user-attachments/assets/56cf93e0-e529-4ca8-9f49-4e40f710e5ed ## Flutter searchable-to-searchable: https://github.com/user-attachments/assets/a98d9f53-8d4b-44cf-afa9-541751c21172 Fixes [CupertinoSliverNavigationBar/CupertinoNavigationBar bottom is not displayed during nav bar flying hero transitions](https://github.com/flutter/flutter/issues/162203) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/cupertino/nav_bar.dart | 135 +++++++++--- .../cupertino/nav_bar_transition_test.dart | 203 +++++++++++++++++- 2 files changed, 307 insertions(+), 31 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index 286172bd114b7..4ad073c520420 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -728,6 +728,7 @@ class _CupertinoNavigationBarState extends State { userTrailing: widget.trailing, padding: widget.padding, userLargeTitle: widget.largeTitle, + userBottom: widget.bottom, large: widget.largeTitle != null, staticBar: true, // This one does not scroll context: context, @@ -764,7 +765,8 @@ class _CupertinoNavigationBarState extends State { ), ), ), - if (widget.bottom != null) SizedBox(height: bottomHeight, child: widget.bottom), + if (widget.bottom != null) + SizedBox(height: bottomHeight, child: components.navBarBottom), ], ), ); @@ -775,7 +777,8 @@ class _CupertinoNavigationBarState extends State { child: Column( children: [ navBar, - if (widget.bottom != null) SizedBox(height: bottomHeight, child: widget.bottom), + if (widget.bottom != null) + SizedBox(height: bottomHeight, child: components.navBarBottom), ], ), ); @@ -1265,6 +1268,23 @@ class _CupertinoSliverNavigationBarState extends State( + find.ancestor( + of: flying(tester, find.text('Page 2')), + matching: find.byType(FadeTransition), + ), + ) + .opacity + .value; + + checkOpacity(tester, flying(tester, find.byType(Placeholder)), largeTitleOpacity); + + Offset largeTitleOffset = tester.getTopLeft(flying(tester, find.text('Page 2'))); + + // The nav bar bottom is horizontally aligned to the large title. + expect( + tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx, + largeTitleOffset.dx - horizontalPadding, + ); + + await tester.pump(const Duration(milliseconds: 150)); + + checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.444); + + largeTitleOffset = tester.getTopLeft(flying(tester, find.text('Page 2'))); + + // The nav bar bottom is horizontally aligned to the large title. + expect( + tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx, + largeTitleOffset.dx - horizontalPadding, + ); + }); + + testWidgets('Top CupertinoNavigationBar.bottom fades and slides in to the right', ( + WidgetTester tester, + ) async { + await startTransitionBetween( + tester, + toTitle: 'Page 2', + to: const CupertinoNavigationBar( + bottom: PreferredSize(preferredSize: Size.fromHeight(30.0), child: Placeholder()), + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + + expect(flying(tester, find.text('Page 2')), findsOneWidget); + expect(flying(tester, find.byType(Placeholder)), findsOneWidget); + + checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.001); + + expect( + tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx, + moreOrLessEquals(779.42, epsilon: 0.01), + ); + + await tester.pump(const Duration(milliseconds: 150)); + + checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.444); + + expect( + tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx, + moreOrLessEquals(309.30, epsilon: 0.01), + ); + }); + + testWidgets('Searchable-to-searchable transition does not fade', (WidgetTester tester) async { + await startTransitionBetween( + tester, + from: const CupertinoSliverNavigationBar.search(searchField: CupertinoSearchTextField()), + to: const CupertinoSliverNavigationBar.search(searchField: CupertinoSearchTextField()), + fromTitle: 'Page 1', + toTitle: 'Page 2', + ); + + await tester.pump(const Duration(milliseconds: 50)); + + expect(flying(tester, find.byType(CupertinoSearchTextField)), findsNWidgets(2)); + + // Either no FadeTransition ancestor is found, or one is found but there is no fade. + expect( + find.ancestor( + of: find.byType(CupertinoSearchTextField).first, + matching: find.byType(FadeTransition), + ), + findsNothing, + ); + checkOpacity(tester, flying(tester, find.byType(CupertinoSearchTextField).last), 1.0); + + await tester.pump(const Duration(milliseconds: 150)); + + // Either no FadeTransition ancestor is found, or one is found but there is no fade. + expect( + find.ancestor( + of: find.byType(CupertinoSearchTextField).first, + matching: find.byType(FadeTransition), + ), + findsNothing, + ); + checkOpacity(tester, flying(tester, find.byType(CupertinoSearchTextField).last), 1.0); + }); + testWidgets('Components are not unnecessarily rebuilt during transitions', ( WidgetTester tester, ) async {