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:
Victor Sanni 2025-03-20 12:54:59 -07:00 committed by GitHub
parent 2db84b20f9
commit 1fa9254076
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 302 additions and 112 deletions

View File

@ -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,
);
}
}

View File

@ -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),
);
});