Skip to content

Commit 37ba70c

Browse files
authored
Fix Cupertino route animation. (#153765)
Fixes: #48225 Fixes: #73026 Fixes: #62848 After some research, I found that the issue might simply be that our animation curve is too steep, causing the animation to continue while the page visually appears very close to popping. This PR modifies the animation curve and duration after the drag gesture is released (iOS native animation duration is not related to swipe distance). I recorded a video comparing the curves. From top to bottom, the video shows the iOS native curve, the curve modified by this PR, and the original curve. The animation duration is slowed down by 10 times. 1/2 https://github.com/user-attachments/assets/77d0a782-b56d-431b-b925-8ff4e825c14a 1/4 https://github.com/user-attachments/assets/a4c50219-e86d-4cce-8a92-b266eb6260a8 forward 1/2 https://github.com/user-attachments/assets/067fffc2-203b-4686-ba4c-3b61a2c98cf8 forward 1/4 https://github.com/user-attachments/assets/c1ae938f-76ab-42f8-a832-d2d0e6c0758d
1 parent 81e418d commit 37ba70c

File tree

4 files changed

+17
-31
lines changed

4 files changed

+17
-31
lines changed

packages/flutter/lib/src/cupertino/route.dart

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
library;
1717

1818
import 'dart:math';
19-
import 'dart:ui' show ImageFilter, lerpDouble;
19+
import 'dart:ui' show ImageFilter;
2020

2121
import 'package:flutter/foundation.dart';
2222
import 'package:flutter/gestures.dart';
@@ -30,13 +30,8 @@ import 'localizations.dart';
3030
const double _kBackGestureWidth = 20.0;
3131
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
3232

33-
// An eyeballed value for the maximum time it takes for a page to animate forward
34-
// if the user releases a page mid swipe.
35-
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.
36-
37-
// The maximum time for a page to get reset to it's original position if the
38-
// user releases a page mid swipe.
39-
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
33+
// The duration for a page to animate when the user releases it mid-swipe.
34+
const Duration _kDroppedSwipePageAnimationDuration = Duration(milliseconds: 350);
4035

4136
/// Barrier color used for a barrier visible during transitions for Cupertino
4237
/// page routes.
@@ -796,7 +791,7 @@ class _CupertinoBackGestureController<T> {
796791
//
797792
// This curve has been determined through rigorously eyeballing native iOS
798793
// animations.
799-
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
794+
const Curve animationCurve = Curves.fastEaseInToSlowEaseOut;
800795
final bool isCurrent = getIsCurrent();
801796
final bool animateForward;
802797

@@ -818,14 +813,7 @@ class _CupertinoBackGestureController<T> {
818813
}
819814

820815
if (animateForward) {
821-
// The closer the panel is to dismissing, the shorter the animation is.
822-
// We want to cap the animation time, but we want to use a linear curve
823-
// to determine it.
824-
final int droppedPageForwardAnimationTime = min(
825-
lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!.floor(),
826-
_kMaxPageBackAnimationTime,
827-
);
828-
controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
816+
controller.animateTo(1.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve);
829817
} else {
830818
if (isCurrent) {
831819
// This route is destined to pop at this point. Reuse navigator's pop.
@@ -834,9 +822,7 @@ class _CupertinoBackGestureController<T> {
834822

835823
// The popping may have finished inline if already at the target destination.
836824
if (controller.isAnimating) {
837-
// Otherwise, use a custom popping animation duration and curve.
838-
final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!.floor();
839-
controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve);
825+
controller.animateBack(0.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve);
840826
}
841827
}
842828

packages/flutter/test/cupertino/nav_bar_transition_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ void main() {
13761376
expect(
13771377
tester.getTopLeft(flying(tester, find.text('Page 2'))),
13781378
const Offset(
1379-
749.863556146621704102,
1379+
721.4629859924316,
13801380
13.5,
13811381
),
13821382
);
@@ -1443,7 +1443,7 @@ void main() {
14431443
expect(
14441444
tester.getTopLeft(flying(tester, find.text('Page 2'))),
14451445
const Offset(
1446-
350.231143206357955933,
1446+
351.52365279197693,
14471447
13.5,
14481448
),
14491449
);

packages/flutter/test/cupertino/route_test.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,10 @@ void main() {
351351
const Offset(400, 0),
352352
);
353353
// Let the dismissing snapping animation go 60%.
354-
await tester.pump(const Duration(milliseconds: 240));
354+
await tester.pump(const Duration(milliseconds: 210));
355355
expect(
356356
tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(CupertinoPageScaffold))).dx,
357-
moreOrLessEquals(798, epsilon: 1),
357+
moreOrLessEquals(789, epsilon: 1),
358358
);
359359

360360
// Use the navigator to push a route instead of tapping the 'push' button.
@@ -1252,13 +1252,13 @@ void main() {
12521252
);
12531253

12541254
await tester.pump(const Duration(milliseconds: 50));
1255-
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-19, epsilon: 1));
1256-
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(744, epsilon: 1));
1255+
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-61, epsilon: 1));
1256+
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(614, epsilon: 1));
12571257

12581258
await tester.pump(const Duration(milliseconds: 50));
12591259
// Rate of change is slowing down.
1260-
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-4, epsilon: 1));
1261-
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(787, epsilon: 1));
1260+
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-26, epsilon: 1));
1261+
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(721, epsilon: 1));
12621262

12631263
await tester.pumpAndSettle();
12641264
expect(
@@ -1297,7 +1297,7 @@ void main() {
12971297

12981298
// Didn't drag far enough to snap into dismissing this route.
12991299
// Each 100px distance takes 100ms to snap back.
1300-
await tester.pump(const Duration(milliseconds: 101));
1300+
await tester.pump(const Duration(milliseconds: 351));
13011301
// Back to the page covering the whole screen.
13021302
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(0));
13031303
expect(navigatorKey.currentState!.userGestureInProgress, false);
@@ -1312,7 +1312,7 @@ void main() {
13121312
expect(navigatorObserver.invocations.removeLast(), NavigatorInvocation.didPop);
13131313

13141314
// Did go far enough to snap out of this route.
1315-
await tester.pump(const Duration(milliseconds: 301));
1315+
await tester.pump(const Duration(milliseconds: 351));
13161316
// Back to the page covering the whole screen.
13171317
expect(find.text('2'), findsNothing);
13181318
// First route covers the whole screen.

packages/flutter/test/material/page_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ void main() {
881881
await tester.pump(const Duration(milliseconds: 240));
882882
expect(
883883
tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(Scaffold))).dx,
884-
moreOrLessEquals(798, epsilon: 1),
884+
moreOrLessEquals(794, epsilon: 1),
885885
);
886886

887887
// Use the navigator to push a route instead of tapping the 'push' button.

0 commit comments

Comments
 (0)