Cupertino navigation bars transitionBetweenRoutes fidelity update (#164956)
In the before Flutter video, drag happens after the 17 second mark. ## Flutter before https://github.com/user-attachments/assets/9fbcd59e-68aa-4975-9a66-f05e72071223 ## Flutter after https://github.com/user-attachments/assets/784d7f46-a8ce-4e5f-a849-3b19df6668c9 ## Flutter back swipe drag gesture https://github.com/user-attachments/assets/09d38c01-aeea-46c1-90b4-590861f8f3a2 ## Native iOS https://github.com/user-attachments/assets/f2ab96c0-766b-4452-b6d5-3f92e6b6785f ## Flutter scaled back chevron and large title after https://github.com/user-attachments/assets/ba87add7-affa-4dcc-b2f0-abbc3487d677 ## Native iOS scaled back chevron and large title https://github.com/user-attachments/assets/5c7bfe5b-5789-4ab9-8e36-770cf802b1b1 Native iOS is probably using a spring simulation. This is the closest curve we have to the native transition, but should be updated in the future with the exact values. Fixes [Cupertino navigation bars transitionBetweenRoutes fidelity update](https://github.com/flutter/flutter/issues/164662)
This commit is contained in:
parent
2db84b20f9
commit
1fa9254076
@ -95,6 +95,18 @@ const Border _kTransparentNavBarBorder = Border(
|
||||
bottom: BorderSide(color: Color(0x00000000), width: 0.0),
|
||||
);
|
||||
|
||||
/// The curve of the animation of the top nav bar regardless of push/pop
|
||||
/// direction in the hero transition between two nav bars.
|
||||
///
|
||||
/// Eyeballed on an iPhone 15 Pro simulator running iOS 17.5.
|
||||
const Curve _kTopNavBarHeaderTransitionCurve = Cubic(0.0, 0.45, 0.45, 0.98);
|
||||
|
||||
/// The curve of the animation of the bottom nav bar regardless of push/pop
|
||||
/// direction in the hero transition between two nav bars.
|
||||
///
|
||||
/// Eyeballed on an iPhone 15 Pro simulator running iOS 17.5.
|
||||
const Curve _kBottomNavBarHeaderTransitionCurve = Cubic(0.05, 0.90, 0.90, 0.95);
|
||||
|
||||
// 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(null);
|
||||
@ -2431,6 +2443,10 @@ class _TransitionableNavigationBar extends StatelessWidget {
|
||||
return box;
|
||||
}
|
||||
|
||||
bool get userGestureInProgress {
|
||||
return Navigator.of(componentsKeys.navBarBoxKey.currentContext!).userGestureInProgress;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(() {
|
||||
@ -2485,20 +2501,13 @@ class _NavigationBarTransition extends StatelessWidget {
|
||||
}) : heightTween = Tween<double>(
|
||||
begin: bottomNavBar.renderBox.size.height,
|
||||
end: topNavBar.renderBox.size.height,
|
||||
),
|
||||
backgroundTween = ColorTween(
|
||||
begin: bottomNavBar.backgroundColor,
|
||||
end: topNavBar.backgroundColor,
|
||||
),
|
||||
borderTween = BorderTween(begin: bottomNavBar.border, end: topNavBar.border);
|
||||
);
|
||||
|
||||
final Animation<double> animation;
|
||||
final _TransitionableNavigationBar topNavBar;
|
||||
final _TransitionableNavigationBar bottomNavBar;
|
||||
|
||||
final Tween<double> heightTween;
|
||||
final ColorTween backgroundTween;
|
||||
final BorderTween borderTween;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -2511,21 +2520,8 @@ class _NavigationBarTransition extends StatelessWidget {
|
||||
);
|
||||
|
||||
final List<Widget> children = <Widget>[
|
||||
// Draw an empty navigation bar box with changing shape behind all the
|
||||
// moving components without any components inside it itself.
|
||||
AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return _wrapWithBackground(
|
||||
// Don't update the system status bar color mid-flight.
|
||||
updateSystemUiOverlay: false,
|
||||
backgroundColor: backgroundTween.evaluate(animation)!,
|
||||
border: borderTween.evaluate(animation),
|
||||
child: SizedBox(height: heightTween.evaluate(animation), width: double.infinity),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Draw all the components on top of the empty bar box.
|
||||
if (componentsTransition.bottomNavBarBackground != null)
|
||||
componentsTransition.bottomNavBarBackground!,
|
||||
if (componentsTransition.bottomBackChevron != null) componentsTransition.bottomBackChevron!,
|
||||
if (componentsTransition.bottomBackLabel != null) componentsTransition.bottomBackLabel!,
|
||||
if (componentsTransition.bottomLeading != null) componentsTransition.bottomLeading!,
|
||||
@ -2534,6 +2530,8 @@ class _NavigationBarTransition extends StatelessWidget {
|
||||
if (componentsTransition.bottomTrailing != null) componentsTransition.bottomTrailing!,
|
||||
if (componentsTransition.bottomNavBarBottom != null) componentsTransition.bottomNavBarBottom!,
|
||||
// Draw top components on top of the bottom components.
|
||||
if (componentsTransition.topNavBarBackground != null)
|
||||
componentsTransition.topNavBarBackground!,
|
||||
if (componentsTransition.topLeading != null) componentsTransition.topLeading!,
|
||||
if (componentsTransition.topBackChevron != null) componentsTransition.topBackChevron!,
|
||||
if (componentsTransition.topBackLabel != null) componentsTransition.topBackLabel!,
|
||||
@ -2600,6 +2598,12 @@ class _NavigationBarComponentsTransition {
|
||||
topHasUserMiddle = topNavBar.hasUserMiddle,
|
||||
bottomLargeExpanded = bottomNavBar.largeExpanded,
|
||||
topLargeExpanded = topNavBar.largeExpanded,
|
||||
bottomBackgroundColor = bottomNavBar.backgroundColor,
|
||||
topBackgroundColor = topNavBar.backgroundColor,
|
||||
bottomBorder = bottomNavBar.border,
|
||||
topBorder = topNavBar.border,
|
||||
userGestureInProgress =
|
||||
topNavBar.userGestureInProgress || bottomNavBar.userGestureInProgress,
|
||||
transitionBox =
|
||||
// paintBounds are based on offset zero so it's ok to expand the Rects.
|
||||
bottomNavBar.renderBox.paintBounds.expandToInclude(topNavBar.renderBox.paintBounds),
|
||||
@ -2629,6 +2633,12 @@ class _NavigationBarComponentsTransition {
|
||||
final bool topHasUserMiddle;
|
||||
final bool bottomLargeExpanded;
|
||||
final bool topLargeExpanded;
|
||||
final bool userGestureInProgress;
|
||||
|
||||
final Color? bottomBackgroundColor;
|
||||
final Color? topBackgroundColor;
|
||||
final Border? bottomBorder;
|
||||
final Border? topBorder;
|
||||
|
||||
// This is the outer box in which all the components will be fitted. The
|
||||
// sizing component of RelativeRects will be based on this rect's size.
|
||||
@ -2637,7 +2647,7 @@ class _NavigationBarComponentsTransition {
|
||||
// x-axis unity number representing the direction of growth for text.
|
||||
final double forwardDirection;
|
||||
|
||||
// Take a widget it its original ancestor navigation bar render box and
|
||||
// Take a widget in its original ancestor navigation bar render box and
|
||||
// translate it into a RelativeBox in the transition navigation bar box.
|
||||
RelativeRect positionInTransitionBox(GlobalKey key, {required RenderBox from}) {
|
||||
final RenderBox componentBox = key.currentContext!.findRenderObject()! as RenderBox;
|
||||
@ -2667,6 +2677,7 @@ class _NavigationBarComponentsTransition {
|
||||
required RenderBox fromNavBarBox,
|
||||
required GlobalKey toKey,
|
||||
required RenderBox toNavBarBox,
|
||||
Curve curve = const Interval(0.0, 1.0),
|
||||
required Widget child,
|
||||
}) {
|
||||
final RenderBox fromBox = fromKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
@ -2711,7 +2722,9 @@ class _NavigationBarComponentsTransition {
|
||||
|
||||
return _FixedSizeSlidingTransition(
|
||||
isLTR: isLTR,
|
||||
offsetAnimation: animation.drive(anchorMovementInTransitionBox),
|
||||
offsetAnimation: animation
|
||||
.drive(CurveTween(curve: curve))
|
||||
.drive(anchorMovementInTransitionBox),
|
||||
size: fromBox.size,
|
||||
child: child,
|
||||
);
|
||||
@ -2725,6 +2738,48 @@ class _NavigationBarComponentsTransition {
|
||||
return animation.drive(fadeOut.chain(CurveTween(curve: Interval(0.0, t, curve: curve))));
|
||||
}
|
||||
|
||||
// The parent of the hero animation, which is the route animation.
|
||||
Animation<double> get routeAnimation {
|
||||
// The hero animation is a CurvedAnimation.
|
||||
assert(animation is CurvedAnimation);
|
||||
return (animation as CurvedAnimation).parent;
|
||||
}
|
||||
|
||||
Widget? get bottomNavBarBackground {
|
||||
if (bottomBackgroundColor == null) {
|
||||
return null;
|
||||
}
|
||||
final Curve animationCurve =
|
||||
animation.status == AnimationStatus.forward
|
||||
? Curves.fastEaseInToSlowEaseOut
|
||||
: Curves.fastEaseInToSlowEaseOut.flipped;
|
||||
|
||||
final Animation<double> pageTransitionAnimation = routeAnimation.drive(
|
||||
CurveTween(curve: userGestureInProgress ? Curves.linear : animationCurve),
|
||||
);
|
||||
|
||||
final RelativeRect from = positionInTransitionBox(
|
||||
bottomComponents.navBarBoxKey,
|
||||
from: bottomNavBarBox,
|
||||
);
|
||||
|
||||
final RelativeRectTween positionTween = RelativeRectTween(
|
||||
end: from.shift(Offset(forwardDirection * -bottomNavBarBox.size.width, 0.0)),
|
||||
begin: from,
|
||||
);
|
||||
|
||||
return PositionedTransition(
|
||||
rect: pageTransitionAnimation.drive(positionTween),
|
||||
child: _wrapWithBackground(
|
||||
// Don't update the system status bar color mid-flight.
|
||||
updateSystemUiOverlay: false,
|
||||
backgroundColor: bottomBackgroundColor!,
|
||||
border: topBorder,
|
||||
child: SizedBox(height: bottomNavBarBox.size.height, width: double.infinity),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get bottomLeading {
|
||||
final KeyedSubtree? bottomLeading = bottomComponents.leadingKey.currentWidget as KeyedSubtree?;
|
||||
|
||||
@ -2853,6 +2908,7 @@ class _NavigationBarComponentsTransition {
|
||||
fromNavBarBox: bottomNavBarBox,
|
||||
toKey: topComponents.backLabelKey,
|
||||
toNavBarBox: topNavBarBox,
|
||||
curve: Interval(0.0, animation.status == AnimationStatus.forward ? 0.7 : 1.0),
|
||||
child: FadeTransition(
|
||||
opacity: fadeOutBy(0.6),
|
||||
child: Align(
|
||||
@ -2931,20 +2987,64 @@ class _NavigationBarComponentsTransition {
|
||||
// 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)),
|
||||
end: from.shift(Offset(forwardDirection * -bottomNavBarBox.size.width, 0.0)),
|
||||
);
|
||||
|
||||
Widget child = bottomNavBarBottom.child;
|
||||
final Curve animationCurve =
|
||||
animation.status == AnimationStatus.forward
|
||||
? _kBottomNavBarHeaderTransitionCurve
|
||||
: _kBottomNavBarHeaderTransitionCurve.flipped;
|
||||
|
||||
// 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);
|
||||
child = FadeTransition(opacity: fadeOutBy(0.8, curve: animationCurve), child: child);
|
||||
}
|
||||
|
||||
return PositionedTransition(rect: animation.drive(positionTween), child: child);
|
||||
return PositionedTransition(
|
||||
rect:
|
||||
// The bottom widget animates linearly during a backswipe by a user gesture.
|
||||
userGestureInProgress
|
||||
? routeAnimation.drive(CurveTween(curve: Curves.linear)).drive(positionTween)
|
||||
: animation.drive(CurveTween(curve: animationCurve)).drive(positionTween),
|
||||
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get topNavBarBackground {
|
||||
if (topBackgroundColor == null) {
|
||||
return null;
|
||||
}
|
||||
final Curve animationCurve =
|
||||
animation.status == AnimationStatus.forward
|
||||
? Curves.fastEaseInToSlowEaseOut
|
||||
: Curves.fastEaseInToSlowEaseOut.flipped;
|
||||
|
||||
final Animation<double> pageTransitionAnimation = routeAnimation.drive(
|
||||
CurveTween(curve: userGestureInProgress ? Curves.linear : animationCurve),
|
||||
);
|
||||
|
||||
final RelativeRect to = positionInTransitionBox(topComponents.navBarBoxKey, from: topNavBarBox);
|
||||
|
||||
final RelativeRectTween positionTween = RelativeRectTween(
|
||||
begin: to.shift(Offset(forwardDirection * topNavBarBox.size.width, 0.0)),
|
||||
end: to,
|
||||
);
|
||||
|
||||
return PositionedTransition(
|
||||
rect: pageTransitionAnimation.drive(positionTween),
|
||||
child: _wrapWithBackground(
|
||||
// Don't update the system status bar color mid-flight.
|
||||
updateSystemUiOverlay: false,
|
||||
backgroundColor: topBackgroundColor!,
|
||||
border: topBorder,
|
||||
child: SizedBox(height: topNavBarBox.size.height, width: double.infinity),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get topLeading {
|
||||
@ -2976,21 +3076,50 @@ class _NavigationBarComponentsTransition {
|
||||
);
|
||||
RelativeRect from = to;
|
||||
|
||||
// If it's the first page with a back chevron, shift in slightly from the
|
||||
// right.
|
||||
Widget child = topBackChevron.child;
|
||||
// Values eyeballed from an iPhone 15 simulator running iOS 17.5.
|
||||
const Curve forwardScaleCurve = Interval(0.0, 0.2);
|
||||
const Curve backwardScaleCurve = Interval(0.8, 1.0);
|
||||
const Curve forwardPositionCurve = Interval(0.0, 0.5);
|
||||
const Curve backwardPositionCurve = Interval(0.5, 1.0);
|
||||
final Curve effectiveScaleCurve;
|
||||
final Curve effectivePositionCurve;
|
||||
|
||||
if (animation.status == AnimationStatus.forward) {
|
||||
effectiveScaleCurve = forwardScaleCurve;
|
||||
effectivePositionCurve = forwardPositionCurve;
|
||||
} else {
|
||||
effectiveScaleCurve = backwardScaleCurve;
|
||||
effectivePositionCurve = backwardPositionCurve;
|
||||
}
|
||||
|
||||
// If it's the first page with a back chevron, shrink and shift in slightly
|
||||
// from the right.
|
||||
if (bottomBackChevron == null) {
|
||||
final RenderBox topBackChevronBox =
|
||||
topComponents.backChevronKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
from = to.shift(Offset(forwardDirection * topBackChevronBox.size.width * 2.0, 0.0));
|
||||
child = ScaleTransition(
|
||||
scale: routeAnimation.drive(CurveTween(curve: effectiveScaleCurve)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final RelativeRectTween positionTween = RelativeRectTween(begin: from, end: to);
|
||||
|
||||
return PositionedTransition(
|
||||
rect: animation.drive(positionTween),
|
||||
rect: routeAnimation.drive(CurveTween(curve: effectivePositionCurve)).drive(positionTween),
|
||||
child: FadeTransition(
|
||||
opacity: fadeInFrom(bottomBackChevron == null ? 0.7 : 0.4),
|
||||
child: DefaultTextStyle(style: topBackButtonTextStyle, child: topBackChevron.child),
|
||||
opacity: routeAnimation.drive(
|
||||
CurveTween(
|
||||
curve: Interval(
|
||||
// Fades faster going back from the first page with a back chevron.
|
||||
bottomBackChevron == null && animation.status != AnimationStatus.forward ? 0.9 : 0.4,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: DefaultTextStyle(style: topBackButtonTextStyle, child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -3027,6 +3156,7 @@ class _NavigationBarComponentsTransition {
|
||||
fromNavBarBox: bottomNavBarBox,
|
||||
toKey: topComponents.backLabelKey,
|
||||
toNavBarBox: topNavBarBox,
|
||||
curve: Interval(0.0, animation.status == AnimationStatus.forward ? 0.7 : 1.0),
|
||||
child: FadeTransition(
|
||||
opacity: midClickOpacity ?? fadeInFrom(0.4),
|
||||
child: DefaultTextStyleTransition(
|
||||
@ -3140,10 +3270,19 @@ class _NavigationBarComponentsTransition {
|
||||
end: to,
|
||||
);
|
||||
|
||||
final Curve animationCurve =
|
||||
animation.status == AnimationStatus.forward
|
||||
? _kTopNavBarHeaderTransitionCurve
|
||||
: _kTopNavBarHeaderTransitionCurve.flipped;
|
||||
|
||||
return PositionedTransition(
|
||||
rect: animation.drive(positionTween),
|
||||
rect:
|
||||
// The large title animates linearly during a backswipe by a user gesture.
|
||||
userGestureInProgress
|
||||
? routeAnimation.drive(CurveTween(curve: Curves.linear)).drive(positionTween)
|
||||
: animation.drive(CurveTween(curve: animationCurve)).drive(positionTween),
|
||||
child: FadeTransition(
|
||||
opacity: fadeInFrom(0.0),
|
||||
opacity: fadeInFrom(0.0, curve: animationCurve),
|
||||
child: DefaultTextStyle(
|
||||
style: topLargeTitleTextStyle!,
|
||||
maxLines: 1,
|
||||
@ -3176,15 +3315,27 @@ class _NavigationBarComponentsTransition {
|
||||
|
||||
Widget child = topNavBarBottom.child;
|
||||
|
||||
final Curve animationCurve =
|
||||
animation.status == AnimationStatus.forward
|
||||
? _kTopNavBarHeaderTransitionCurve
|
||||
: _kTopNavBarHeaderTransitionCurve.flipped;
|
||||
|
||||
// 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);
|
||||
child = FadeTransition(opacity: fadeInFrom(0.0, curve: animationCurve), child: child);
|
||||
}
|
||||
|
||||
return PositionedTransition(rect: animation.drive(positionTween), child: child);
|
||||
return PositionedTransition(
|
||||
rect:
|
||||
// The bottom widget animates linearly during a backswipe by a user gesture.
|
||||
userGestureInProgress
|
||||
? routeAnimation.drive(CurveTween(curve: Curves.linear)).drive(positionTween)
|
||||
: animation.drive(CurveTween(curve: animationCurve)).drive(positionTween),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,20 +107,14 @@ Finder flying(WidgetTester tester, Finder finder) {
|
||||
return find.descendant(of: lastOverlayFinder, matching: finder);
|
||||
}
|
||||
|
||||
void checkBackgroundBoxHeight(WidgetTester tester, double height) {
|
||||
void checkBackgroundBoxOffset(WidgetTester tester, int boxIndex, Offset offset) {
|
||||
final Widget transitionBackgroundBox =
|
||||
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0];
|
||||
expect(
|
||||
tester
|
||||
.widget<SizedBox>(
|
||||
find.descendant(
|
||||
of: find.byWidget(transitionBackgroundBox),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
)
|
||||
.height,
|
||||
height,
|
||||
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[boxIndex];
|
||||
final Offset testOffset = tester.getBottomRight(
|
||||
find.descendant(of: find.byWidget(transitionBackgroundBox), matching: find.byType(SizedBox)),
|
||||
);
|
||||
expect(testOffset.dx, moreOrLessEquals(offset.dx, epsilon: 0.01));
|
||||
expect(testOffset.dy, moreOrLessEquals(offset.dy, epsilon: 0.01));
|
||||
}
|
||||
|
||||
void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
|
||||
@ -584,52 +578,57 @@ void main() {
|
||||
expect(find.text('Tab 1 Page 2', skipOffstage: false), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Transition box grows to large title size', (WidgetTester tester) async {
|
||||
testWidgets('Bottom nav bar transition background box', (WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
fromTitle: 'Page 1',
|
||||
to: const CupertinoSliverNavigationBar(),
|
||||
to: const CupertinoNavigationBar(),
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 45.3376561999321);
|
||||
// The top nav bar background box is the first component in the stack.
|
||||
checkBackgroundBoxOffset(tester, 0, const Offset(609.14, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 51.012951374053955);
|
||||
checkBackgroundBoxOffset(tester, 0, const Offset(362.91, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 63.06760931015015);
|
||||
checkBackgroundBoxOffset(tester, 0, const Offset(192.14, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 75.89544230699539);
|
||||
checkBackgroundBoxOffset(tester, 0, const Offset(95.30, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 84.33018499612808);
|
||||
checkBackgroundBoxOffset(tester, 0, const Offset(46.12, 44.0));
|
||||
});
|
||||
|
||||
testWidgets('Large transition box shrinks to standard nav bar size', (WidgetTester tester) async {
|
||||
testWidgets('Top nav bar transition background box', (WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(),
|
||||
// Only the large title and background box are in the bottom nav bar.
|
||||
from: const CupertinoNavigationBar(automaticallyImplyLeading: false),
|
||||
to: const CupertinoNavigationBar(),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 94.6623438000679);
|
||||
// The component stack only contains the bottom box background (at index 0)
|
||||
// and the large title (at index 1).
|
||||
checkBackgroundBoxOffset(tester, 2, const Offset(1409.14, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 88.98704862594604);
|
||||
checkBackgroundBoxOffset(tester, 2, const Offset(1162.91, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 76.93239068984985);
|
||||
checkBackgroundBoxOffset(tester, 2, const Offset(992.14, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 64.10455769300461);
|
||||
checkBackgroundBoxOffset(tester, 2, const Offset(895.30, 44.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 55.66981500387192);
|
||||
checkBackgroundBoxOffset(tester, 2, const Offset(846.12, 44.0));
|
||||
});
|
||||
|
||||
testWidgets('Hero flight removed at the end of page transition', (WidgetTester tester) async {
|
||||
@ -756,11 +755,13 @@ void main() {
|
||||
);
|
||||
// Come in from the right and fade in.
|
||||
checkOpacity(tester, backChevron, 0.0);
|
||||
expect(tester.getTopLeft(backChevron), const Offset(87.2460581221158690823, 7.0));
|
||||
expect(tester.getTopLeft(backChevron).dx, moreOrLessEquals(80.54, epsilon: 0.01));
|
||||
expect(tester.getTopLeft(backChevron).dy, moreOrLessEquals(14.5, epsilon: 0.01));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
checkOpacity(tester, backChevron, 0.09497911669313908);
|
||||
expect(tester.getTopLeft(backChevron), const Offset(30.8718595298545324113, 7.0));
|
||||
checkOpacity(tester, backChevron, 0.167);
|
||||
expect(tester.getTopLeft(backChevron).dx, moreOrLessEquals(14.0, epsilon: 0.01));
|
||||
expect(tester.getTopLeft(backChevron).dy, moreOrLessEquals(7.0, epsilon: 0.01));
|
||||
});
|
||||
|
||||
testWidgets('First appearance of back chevron fades in from the left in RTL', (
|
||||
@ -800,11 +801,13 @@ void main() {
|
||||
|
||||
// Come in from the right and fade in.
|
||||
checkOpacity(tester, backChevron, 0.0);
|
||||
expect(tester.getTopRight(backChevron), const Offset(687.163941725296126606, 7.0));
|
||||
expect(tester.getTopRight(backChevron).dx, moreOrLessEquals(706.66, epsilon: 0.01));
|
||||
expect(tester.getTopRight(backChevron).dy, moreOrLessEquals(14.5, epsilon: 0.01));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
checkOpacity(tester, backChevron, 0.09497911669313908);
|
||||
expect(tester.getTopRight(backChevron), const Offset(743.538140317557690651, 7.0));
|
||||
checkOpacity(tester, backChevron, 0.167);
|
||||
expect(tester.getTopRight(backChevron).dx, moreOrLessEquals(760.41, epsilon: 0.01));
|
||||
expect(tester.getTopRight(backChevron).dy, moreOrLessEquals(7.0, epsilon: 0.01));
|
||||
});
|
||||
|
||||
testWidgets('Back chevron fades out and in when both pages have it', (WidgetTester tester) async {
|
||||
@ -827,7 +830,7 @@ void main() {
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
checkOpacity(tester, backChevrons.first, 0.0);
|
||||
checkOpacity(tester, backChevrons.last, 0.4604858811944723);
|
||||
checkOpacity(tester, backChevrons.last, 0.167);
|
||||
// Still in the same place.
|
||||
expect(tester.getTopLeft(backChevrons.first), const Offset(14.0, 7.0));
|
||||
expect(tester.getTopLeft(backChevrons.last), const Offset(14.0, 7.0));
|
||||
@ -998,12 +1001,20 @@ void main() {
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(16.9155227761479522997, 52.73951627314091),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first).dx,
|
||||
moreOrLessEquals(17.3, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(16.9155227761479522997, 52.73951627314091),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first).dy,
|
||||
moreOrLessEquals(52.2, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last).dx,
|
||||
moreOrLessEquals(17.3, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last).dy,
|
||||
moreOrLessEquals(52.2, epsilon: 0.01),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
@ -1011,12 +1022,20 @@ void main() {
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.4604858811944723);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(43.6029094262710827934, 22.49655644595623),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first).dx,
|
||||
moreOrLessEquals(51.6, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(43.6029094262710827934, 22.49655644595623),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first).dy,
|
||||
moreOrLessEquals(11.5, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last).dx,
|
||||
moreOrLessEquals(51.6, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last).dy,
|
||||
moreOrLessEquals(11.5, epsilon: 0.01),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1038,21 +1057,21 @@ void main() {
|
||||
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);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.777);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(-20.58, epsilon: 0.01),
|
||||
moreOrLessEquals(-156.62, 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);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.011);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(-620.46, epsilon: 0.01),
|
||||
moreOrLessEquals(-751.94, epsilon: 0.01),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1074,21 +1093,21 @@ void main() {
|
||||
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);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.777);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(-20.58, epsilon: 0.01),
|
||||
moreOrLessEquals(-156.62, 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);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.011);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(-620.46, epsilon: 0.01),
|
||||
moreOrLessEquals(-751.94, epsilon: 0.01),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1109,24 +1128,40 @@ void main() {
|
||||
checkOpacity(tester, flying(tester, find.text('A title too long to fit')), 0.9280824661254883);
|
||||
checkOpacity(tester, flying(tester, find.text('Back')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
|
||||
const Offset(16.9155227761479522997, 52.73951627314091),
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))).dx,
|
||||
moreOrLessEquals(17.3, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))),
|
||||
const Offset(16.9155227761479522997, 52.73951627314091),
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))).dy,
|
||||
moreOrLessEquals(52.2, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))).dx,
|
||||
moreOrLessEquals(17.3, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))).dy,
|
||||
moreOrLessEquals(52.2, epsilon: 0.01),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
checkOpacity(tester, flying(tester, find.text('A title too long to fit')), 0.0);
|
||||
checkOpacity(tester, flying(tester, find.text('Back')), 0.4604858811944723);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
|
||||
const Offset(43.6029094262710827934, 22.49655644595623),
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))).dx,
|
||||
moreOrLessEquals(51.6, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))),
|
||||
const Offset(43.6029094262710827934, 22.49655644595623),
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))).dy,
|
||||
moreOrLessEquals(11.5, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))).dx,
|
||||
moreOrLessEquals(51.6, epsilon: 0.01),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))).dy,
|
||||
moreOrLessEquals(11.5, epsilon: 0.01),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1251,19 +1286,21 @@ void main() {
|
||||
|
||||
expect(flying(tester, find.text('Page 2')), findsOneWidget);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.001);
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.193);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(795.4206738471985, 54.0),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))).dx,
|
||||
moreOrLessEquals(661.64, epsilon: 0.01),
|
||||
);
|
||||
expect(tester.getTopLeft(flying(tester, find.text('Page 2'))).dy, 54.0);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.444);
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.899);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(325.3008875846863, 54.0),
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))).dx,
|
||||
moreOrLessEquals(96.57, epsilon: 0.01),
|
||||
);
|
||||
expect(tester.getTopLeft(flying(tester, find.text('Page 2'))).dy, 54.0);
|
||||
});
|
||||
|
||||
testWidgets('Top large title fades in and slides in from the left in RTL', (
|
||||
@ -1280,19 +1317,21 @@ void main() {
|
||||
|
||||
expect(flying(tester, find.text('Page 2')), findsOneWidget);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.001);
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.193);
|
||||
expect(
|
||||
tester.getTopRight(flying(tester, find.text('Page 2'))),
|
||||
const Offset(4.579326152801514, 54.0),
|
||||
tester.getTopRight(flying(tester, find.text('Page 2'))).dx,
|
||||
moreOrLessEquals(138.36, epsilon: 0.01),
|
||||
);
|
||||
expect(tester.getTopRight(flying(tester, find.text('Page 2'))).dy, 54.0);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.444);
|
||||
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.899);
|
||||
expect(
|
||||
tester.getTopRight(flying(tester, find.text('Page 2'))),
|
||||
const Offset(474.6991124153137, 54.0),
|
||||
tester.getTopRight(flying(tester, find.text('Page 2'))).dx,
|
||||
moreOrLessEquals(703.43, epsilon: 0.01),
|
||||
);
|
||||
expect(tester.getTopRight(flying(tester, find.text('Page 2'))).dy, 54.0);
|
||||
});
|
||||
|
||||
testWidgets('Top CupertinoSliverNavigationBar.bottom is aligned with top large title animation', (
|
||||
@ -1331,12 +1370,12 @@ void main() {
|
||||
// The nav bar bottom is horizontally aligned to the large title.
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
largeTitleOffset.dx - horizontalPadding,
|
||||
moreOrLessEquals(largeTitleOffset.dx - horizontalPadding, epsilon: 0.01),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.444);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.899);
|
||||
|
||||
largeTitleOffset = tester.getTopLeft(flying(tester, find.text('Page 2')));
|
||||
|
||||
@ -1363,20 +1402,20 @@ void main() {
|
||||
expect(flying(tester, find.text('Page 2')), findsOneWidget);
|
||||
expect(flying(tester, find.byType(Placeholder)), findsOneWidget);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.001);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.193);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(779.42, epsilon: 0.01),
|
||||
moreOrLessEquals(645.64, epsilon: 0.01),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.444);
|
||||
checkOpacity(tester, flying(tester, find.byType(Placeholder)), 0.899);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.byType(Placeholder))).dx,
|
||||
moreOrLessEquals(309.30, epsilon: 0.01),
|
||||
moreOrLessEquals(80.57, epsilon: 0.01),
|
||||
);
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user