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].

<!-- Links -->
[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
This commit is contained in:
Victor Sanni 2025-03-10 12:36:04 -07:00 committed by GitHub
parent ffaec10986
commit cd433d4d36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 307 additions and 31 deletions

View File

@ -728,6 +728,7 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
userTrailing: widget.trailing, userTrailing: widget.trailing,
padding: widget.padding, padding: widget.padding,
userLargeTitle: widget.largeTitle, userLargeTitle: widget.largeTitle,
userBottom: widget.bottom,
large: widget.largeTitle != null, large: widget.largeTitle != null,
staticBar: true, // This one does not scroll staticBar: true, // This one does not scroll
context: context, context: context,
@ -764,7 +765,8 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
), ),
), ),
), ),
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<CupertinoNavigationBar> {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
navBar, 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<CupertinoSliverNavigation
? Visibility(visible: !searchIsActive, child: widget.trailing!) ? Visibility(visible: !searchIsActive, child: widget.trailing!)
: null, : null,
userLargeTitle: widget.largeTitle, userLargeTitle: widget.largeTitle,
userBottom:
(widget._searchable
? searchIsActive
? _ActiveSearchableBottom(
animationController: _animationController,
animation: persistentHeightAnimation,
searchField: widget.searchField,
onSearchFieldTap: _onSearchFieldTap,
)
: _InactiveSearchableBottom(
animationController: _animationController,
animation: persistentHeightAnimation,
searchField: preferredSizeSearchField,
onSearchFieldTap: _onSearchFieldTap,
)
: widget.bottom) ??
const SizedBox.shrink(),
padding: widget.padding, padding: widget.padding,
large: true, large: true,
staticBar: false, // This one scrolls. staticBar: false, // This one scrolls.
@ -1297,23 +1317,6 @@ class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigation
stretchConfiguration: stretchConfiguration:
widget.stretch && !searchIsActive ? OverScrollHeaderStretchConfiguration() : null, widget.stretch && !searchIsActive ? OverScrollHeaderStretchConfiguration() : null,
enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur, enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur,
bottom:
(widget._searchable
? searchIsActive
? _ActiveSearchableBottom(
animationController: _animationController,
animation: persistentHeightAnimation,
searchField: widget.searchField,
onSearchFieldTap: _onSearchFieldTap,
)
: _InactiveSearchableBottom(
animationController: _animationController,
animation: persistentHeightAnimation,
searchField: preferredSizeSearchField,
onSearchFieldTap: _onSearchFieldTap,
)
: widget.bottom) ??
const SizedBox.shrink(),
bottomMode: bottomMode:
searchIsActive searchIsActive
? NavigationBarBottomMode.always ? NavigationBarBottomMode.always
@ -1347,7 +1350,6 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
required this.alwaysShowMiddle, required this.alwaysShowMiddle,
required this.stretchConfiguration, required this.stretchConfiguration,
required this.enableBackgroundFilterBlur, required this.enableBackgroundFilterBlur,
required this.bottom,
required this.bottomMode, required this.bottomMode,
required this.bottomHeight, required this.bottomHeight,
required this.controller, required this.controller,
@ -1368,7 +1370,6 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
final double largeTitleHeight; final double largeTitleHeight;
final bool alwaysShowMiddle; final bool alwaysShowMiddle;
final bool enableBackgroundFilterBlur; final bool enableBackgroundFilterBlur;
final Widget bottom;
final NavigationBarBottomMode bottomMode; final NavigationBarBottomMode bottomMode;
final double bottomHeight; final double bottomHeight;
final AnimationController controller; final AnimationController controller;
@ -1482,14 +1483,14 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
bottom: 0.0, bottom: 0.0,
child: SizedBox( child: SizedBox(
height: bottomHeight * (1.0 - bottomShrinkFactor), height: bottomHeight * (1.0 - bottomShrinkFactor),
child: ClipRect(child: bottom), child: ClipRect(child: components.navBarBottom),
), ),
), ),
], ],
), ),
), ),
if (bottomMode == NavigationBarBottomMode.always) if (bottomMode == NavigationBarBottomMode.always)
SizedBox(height: bottomHeight, child: bottom), SizedBox(height: bottomHeight, child: components.navBarBottom),
], ],
), ),
), ),
@ -1537,7 +1538,6 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
alwaysShowMiddle != oldDelegate.alwaysShowMiddle || alwaysShowMiddle != oldDelegate.alwaysShowMiddle ||
heroTag != oldDelegate.heroTag || heroTag != oldDelegate.heroTag ||
enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur || enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur ||
bottom != oldDelegate.bottom ||
bottomMode != oldDelegate.bottomMode || bottomMode != oldDelegate.bottomMode ||
bottomHeight != oldDelegate.bottomHeight || bottomHeight != oldDelegate.bottomHeight ||
controller != oldDelegate.controller; controller != oldDelegate.controller;
@ -1775,7 +1775,8 @@ class _NavigationBarStaticComponentsKeys {
backLabelKey = GlobalKey(debugLabel: 'Back label'), backLabelKey = GlobalKey(debugLabel: 'Back label'),
middleKey = GlobalKey(debugLabel: 'Middle'), middleKey = GlobalKey(debugLabel: 'Middle'),
trailingKey = GlobalKey(debugLabel: 'Trailing'), trailingKey = GlobalKey(debugLabel: 'Trailing'),
largeTitleKey = GlobalKey(debugLabel: 'Large title'); largeTitleKey = GlobalKey(debugLabel: 'Large title'),
navBarBottomKey = GlobalKey(debugLabel: 'Navigation bar bottom');
final GlobalKey navBarBoxKey; final GlobalKey navBarBoxKey;
final GlobalKey leadingKey; final GlobalKey leadingKey;
@ -1784,6 +1785,7 @@ class _NavigationBarStaticComponentsKeys {
final GlobalKey middleKey; final GlobalKey middleKey;
final GlobalKey trailingKey; final GlobalKey trailingKey;
final GlobalKey largeTitleKey; final GlobalKey largeTitleKey;
final GlobalKey navBarBottomKey;
} }
// Based on various user Widgets and other parameters, construct KeyedSubtree // Based on various user Widgets and other parameters, construct KeyedSubtree
@ -1802,6 +1804,7 @@ class _NavigationBarStaticComponents {
required Widget? userMiddle, required Widget? userMiddle,
required Widget? userTrailing, required Widget? userTrailing,
required Widget? userLargeTitle, required Widget? userLargeTitle,
required Widget? userBottom,
required EdgeInsetsDirectional? padding, required EdgeInsetsDirectional? padding,
required bool large, required bool large,
required bool staticBar, required bool staticBar,
@ -1847,6 +1850,10 @@ class _NavigationBarStaticComponents {
route: route, route: route,
automaticImplyTitle: automaticallyImplyTitle, automaticImplyTitle: automaticallyImplyTitle,
large: large, large: large,
),
navBarBottom = createNavBarBottom(
navBarBottomKey: keys.navBarBottomKey,
userBottom: userBottom,
); );
static Widget? _derivedTitle({ static Widget? _derivedTitle({
@ -2024,6 +2031,14 @@ class _NavigationBarStaticComponents {
return KeyedSubtree(key: largeTitleKey, child: largeTitleContent!); return KeyedSubtree(key: largeTitleKey, child: largeTitleContent!);
} }
final KeyedSubtree? navBarBottom;
static KeyedSubtree? createNavBarBottom({
required GlobalKey navBarBottomKey,
required Widget? userBottom,
}) {
return KeyedSubtree(key: navBarBottomKey, child: userBottom ?? const SizedBox.shrink());
}
} }
/// A nav bar back button typically used in [CupertinoNavigationBar]. /// A nav bar back button typically used in [CupertinoNavigationBar].
@ -2517,6 +2532,7 @@ class _NavigationBarTransition extends StatelessWidget {
if (componentsTransition.bottomMiddle != null) componentsTransition.bottomMiddle!, if (componentsTransition.bottomMiddle != null) componentsTransition.bottomMiddle!,
if (componentsTransition.bottomLargeTitle != null) componentsTransition.bottomLargeTitle!, if (componentsTransition.bottomLargeTitle != null) componentsTransition.bottomLargeTitle!,
if (componentsTransition.bottomTrailing != null) componentsTransition.bottomTrailing!, if (componentsTransition.bottomTrailing != null) componentsTransition.bottomTrailing!,
if (componentsTransition.bottomNavBarBottom != null) componentsTransition.bottomNavBarBottom!,
// Draw top components on top of the bottom components. // Draw top components on top of the bottom components.
if (componentsTransition.topLeading != null) componentsTransition.topLeading!, if (componentsTransition.topLeading != null) componentsTransition.topLeading!,
if (componentsTransition.topBackChevron != null) componentsTransition.topBackChevron!, if (componentsTransition.topBackChevron != null) componentsTransition.topBackChevron!,
@ -2524,6 +2540,7 @@ class _NavigationBarTransition extends StatelessWidget {
if (componentsTransition.topMiddle != null) componentsTransition.topMiddle!, if (componentsTransition.topMiddle != null) componentsTransition.topMiddle!,
if (componentsTransition.topLargeTitle != null) componentsTransition.topLargeTitle!, if (componentsTransition.topLargeTitle != null) componentsTransition.topLargeTitle!,
if (componentsTransition.topTrailing != null) componentsTransition.topTrailing!, if (componentsTransition.topTrailing != null) componentsTransition.topTrailing!,
if (componentsTransition.topNavBarBottom != null) componentsTransition.topNavBarBottom!,
]; ];
// The actual outer box is big enough to contain both the bottom and top // The actual outer box is big enough to contain both the bottom and top
@ -2897,6 +2914,39 @@ class _NavigationBarComponentsTransition {
); );
} }
Widget? get bottomNavBarBottom {
final KeyedSubtree? bottomNavBarBottom =
bottomComponents.navBarBottomKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topNavBarBottom =
topComponents.navBarBottomKey.currentWidget as KeyedSubtree?;
if (bottomNavBarBottom == null) {
return null;
}
final RelativeRect from = positionInTransitionBox(
bottomComponents.navBarBottomKey,
from: bottomNavBarBox,
);
// Shift in from the leading edge of the screen.
final RelativeRectTween positionTween = RelativeRectTween(
begin: from,
end: from.shift(Offset(-forwardDirection * bottomNavBarBox.size.width, 0.0)),
);
Widget child = bottomNavBarBottom.child;
// Fade out only if this is not a CupertinoSliverNavigationBar.search to
// CupertinoSliverNavigationBar.search transition.
if (topNavBarBottom == null ||
topNavBarBottom.child is! _InactiveSearchableBottom ||
bottomNavBarBottom.child is! _InactiveSearchableBottom) {
child = FadeTransition(opacity: fadeOutBy(0.8), child: child);
}
return PositionedTransition(rect: animation.drive(positionTween), child: child);
}
Widget? get topLeading { Widget? get topLeading {
final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?; final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
@ -3093,7 +3143,7 @@ class _NavigationBarComponentsTransition {
return PositionedTransition( return PositionedTransition(
rect: animation.drive(positionTween), rect: animation.drive(positionTween),
child: FadeTransition( child: FadeTransition(
opacity: fadeInFrom(0.3), opacity: fadeInFrom(0.0),
child: DefaultTextStyle( child: DefaultTextStyle(
style: topLargeTitleTextStyle!, style: topLargeTitleTextStyle!,
maxLines: 1, maxLines: 1,
@ -3103,6 +3153,39 @@ class _NavigationBarComponentsTransition {
), ),
); );
} }
Widget? get topNavBarBottom {
final KeyedSubtree? topNavBarBottom =
topComponents.navBarBottomKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? bottomNavBarBottom =
bottomComponents.navBarBottomKey.currentWidget as KeyedSubtree?;
if (topNavBarBottom == null) {
return null;
}
final RelativeRect to = positionInTransitionBox(
topComponents.navBarBottomKey,
from: topNavBarBox,
);
// Shift in from the trailing edge of the screen.
final RelativeRectTween positionTween = RelativeRectTween(
begin: to.shift(Offset(forwardDirection * topNavBarBox.size.width, 0.0)),
end: to,
);
Widget child = topNavBarBottom.child;
// Fade in only if this is not a CupertinoSliverNavigationBar.search to
// CupertinoSliverNavigationBar.search transition.
if (bottomNavBarBottom == null ||
bottomNavBarBottom.child is! _InactiveSearchableBottom ||
topNavBarBottom.child is! _InactiveSearchableBottom) {
child = FadeTransition(opacity: fadeInFrom(0.0), child: child);
}
return PositionedTransition(rect: animation.drive(positionTween), child: child);
}
} }
/// Navigation bars' hero rect tween that will move between the static bars /// Navigation bars' hero rect tween that will move between the static bars

View File

@ -131,7 +131,7 @@ void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
) )
.opacity .opacity
.value, .value,
moreOrLessEquals(opacity), moreOrLessEquals(opacity, epsilon: 0.001),
); );
} }
@ -1020,6 +1020,78 @@ void main() {
); );
}); });
testWidgets('Bottom CupertinoSliverNavigationBar.bottom fades and slides out from the left', (
WidgetTester tester,
) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(30.0), child: Placeholder()),
),
fromTitle: 'Page 1',
);
await tester.pump(const Duration(milliseconds: 50));
// There's 2, one from the bottom large title fading out and one from the
// bottom back label fading in.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
expect(flying(tester, find.byType(Placeholder)), findsOneWidget);
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.946);
expect(
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
moreOrLessEquals(-20.58, epsilon: 0.01),
);
await tester.pump(const Duration(milliseconds: 200));
// Halfway through the transition, the bottom is only slightly visible.
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.001);
expect(
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
moreOrLessEquals(-620.46, epsilon: 0.01),
);
});
testWidgets('Bottom CupertinoNavigationBar.bottom fades and slides out from the left', (
WidgetTester tester,
) async {
await startTransitionBetween(
tester,
from: const CupertinoNavigationBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(30.0), child: Placeholder()),
),
fromTitle: 'Page 1',
);
await tester.pump(const Duration(milliseconds: 50));
// There's 2, one from the bottom large title fading out and one from the
// bottom back label fading in.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
expect(flying(tester, find.byType(Placeholder)), findsOneWidget);
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.946);
expect(
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
moreOrLessEquals(-20.58, epsilon: 0.01),
);
await tester.pump(const Duration(milliseconds: 200));
// Halfway through the transition, the bottom is only slightly visible.
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.001);
expect(
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
moreOrLessEquals(-620.46, epsilon: 0.01),
);
});
testWidgets('Long title turns into the word back mid transition', (WidgetTester tester) async { testWidgets('Long title turns into the word back mid transition', (WidgetTester tester) async {
await startTransitionBetween( await startTransitionBetween(
tester, tester,
@ -1179,7 +1251,7 @@ void main() {
expect(flying(tester, find.text('Page 2')), findsOneWidget); expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.001);
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(795.4206738471985, 54.0), const Offset(795.4206738471985, 54.0),
@ -1187,7 +1259,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.2601277381181717); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.444);
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(325.3008875846863, 54.0), const Offset(325.3008875846863, 54.0),
@ -1208,7 +1280,7 @@ void main() {
expect(flying(tester, find.text('Page 2')), findsOneWidget); expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.001);
expect( expect(
tester.getTopRight(flying(tester, find.text('Page 2'))), tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(4.579326152801514, 54.0), const Offset(4.579326152801514, 54.0),
@ -1216,13 +1288,134 @@ void main() {
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.2601277381181717); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.444);
expect( expect(
tester.getTopRight(flying(tester, find.text('Page 2'))), tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(474.6991124153137, 54.0), const Offset(474.6991124153137, 54.0),
); );
}); });
testWidgets('Top CupertinoSliverNavigationBar.bottom is aligned with top large title animation', (
WidgetTester tester,
) async {
const double horizontalPadding = 16.0; // _kNavBarEdgePadding
const double height = 30.0;
await startTransitionBetween(
tester,
toTitle: 'Page 2',
to: const CupertinoSliverNavigationBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(height), child: Placeholder()),
),
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
expect(flying(tester, find.byType(Placeholder)), findsOneWidget);
final double largeTitleOpacity =
tester
.firstRenderObject<RenderAnimatedOpacity>(
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', ( testWidgets('Components are not unnecessarily rebuilt during transitions', (
WidgetTester tester, WidgetTester tester,
) async { ) async {