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:
parent
ffaec10986
commit
cd433d4d36
@ -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
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user