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
This commit is contained in:
yim 2024-08-24 04:45:22 +08:00 committed by GitHub
parent 81e418dd20
commit 37ba70cef5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 17 additions and 31 deletions

View File

@ -16,7 +16,7 @@
library;
import 'dart:math';
import 'dart:ui' show ImageFilter, lerpDouble;
import 'dart:ui' show ImageFilter;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@ -30,13 +30,8 @@ import 'localizations.dart';
const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
// An eyeballed value for the maximum time it takes for a page to animate forward
// if the user releases a page mid swipe.
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.
// The maximum time for a page to get reset to it's original position if the
// user releases a page mid swipe.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
// The duration for a page to animate when the user releases it mid-swipe.
const Duration _kDroppedSwipePageAnimationDuration = Duration(milliseconds: 350);
/// Barrier color used for a barrier visible during transitions for Cupertino
/// page routes.
@ -796,7 +791,7 @@ class _CupertinoBackGestureController<T> {
//
// This curve has been determined through rigorously eyeballing native iOS
// animations.
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
const Curve animationCurve = Curves.fastEaseInToSlowEaseOut;
final bool isCurrent = getIsCurrent();
final bool animateForward;
@ -818,14 +813,7 @@ class _CupertinoBackGestureController<T> {
}
if (animateForward) {
// The closer the panel is to dismissing, the shorter the animation is.
// We want to cap the animation time, but we want to use a linear curve
// to determine it.
final int droppedPageForwardAnimationTime = min(
lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!.floor(),
_kMaxPageBackAnimationTime,
);
controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
controller.animateTo(1.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve);
} else {
if (isCurrent) {
// This route is destined to pop at this point. Reuse navigator's pop.
@ -834,9 +822,7 @@ class _CupertinoBackGestureController<T> {
// The popping may have finished inline if already at the target destination.
if (controller.isAnimating) {
// Otherwise, use a custom popping animation duration and curve.
final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!.floor();
controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve);
controller.animateBack(0.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve);
}
}

View File

@ -1376,7 +1376,7 @@ void main() {
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(
749.863556146621704102,
721.4629859924316,
13.5,
),
);
@ -1443,7 +1443,7 @@ void main() {
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(
350.231143206357955933,
351.52365279197693,
13.5,
),
);

View File

@ -351,10 +351,10 @@ void main() {
const Offset(400, 0),
);
// Let the dismissing snapping animation go 60%.
await tester.pump(const Duration(milliseconds: 240));
await tester.pump(const Duration(milliseconds: 210));
expect(
tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(CupertinoPageScaffold))).dx,
moreOrLessEquals(798, epsilon: 1),
moreOrLessEquals(789, epsilon: 1),
);
// Use the navigator to push a route instead of tapping the 'push' button.
@ -1252,13 +1252,13 @@ void main() {
);
await tester.pump(const Duration(milliseconds: 50));
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-19, epsilon: 1));
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(744, epsilon: 1));
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-61, epsilon: 1));
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(614, epsilon: 1));
await tester.pump(const Duration(milliseconds: 50));
// Rate of change is slowing down.
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-4, epsilon: 1));
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(787, epsilon: 1));
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(-26, epsilon: 1));
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(721, epsilon: 1));
await tester.pumpAndSettle();
expect(
@ -1297,7 +1297,7 @@ void main() {
// Didn't drag far enough to snap into dismissing this route.
// Each 100px distance takes 100ms to snap back.
await tester.pump(const Duration(milliseconds: 101));
await tester.pump(const Duration(milliseconds: 351));
// Back to the page covering the whole screen.
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(0));
expect(navigatorKey.currentState!.userGestureInProgress, false);
@ -1312,7 +1312,7 @@ void main() {
expect(navigatorObserver.invocations.removeLast(), NavigatorInvocation.didPop);
// Did go far enough to snap out of this route.
await tester.pump(const Duration(milliseconds: 301));
await tester.pump(const Duration(milliseconds: 351));
// Back to the page covering the whole screen.
expect(find.text('2'), findsNothing);
// First route covers the whole screen.

View File

@ -881,7 +881,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 240));
expect(
tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(Scaffold))).dx,
moreOrLessEquals(798, epsilon: 1),
moreOrLessEquals(794, epsilon: 1),
);
// Use the navigator to push a route instead of tapping the 'push' button.