Unverified Commit cf4fc966 authored by jslavitz's avatar jslavitz Committed by GitHub

Transition Curve Fix (#25488)

* Changes Cupertino page transition curves to make paging animations more similar to those of native iOS
parent 5b3e933c
......@@ -487,6 +487,23 @@ class AnimationController extends Animation<double>
return _animateToInternal(target, duration: duration, curve: curve);
}
/// Drives the animation from its current value to target.
///
/// Returns a [TickerFuture] that completes when the animation is complete.
///
/// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error.
///
/// During the animation, [status] is reported as [AnimationStatus.reverse]
/// regardless of whether `target` < [value] or not. At the end of the
/// animation, when `target` is reached, [status] is reported as
/// [AnimationStatus.dismissed].
TickerFuture animateBack(double target, { Duration duration, Curve curve = Curves.linear }) {
_direction = _AnimationDirection.reverse;
return _animateToInternal(target, duration: duration, curve: curve);
}
TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) {
final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
double scale = 1.0;
......
......@@ -316,7 +316,6 @@ class _DecelerateCurve extends Curve {
}
}
// BOUNCE CURVES
double _bounce(double t) {
......@@ -536,6 +535,15 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4}
static const Curve decelerate = _DecelerateCurve._();
/// A curve that is very steep and linear at the beginning, but quickly flattens out
/// and very slowly eases in.
///
/// By default is the curve used to animate pages on iOS back to their original
/// position if a swipe gesture is ended midway through a swipe.
///
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/fast_linear_to_slow_ease_in.mp4}
static const Cubic fastLinearToSlowEaseIn = Cubic(0.18, 1.0, 0.04, 1.0);
/// A cubic animation curve that speeds up quickly and ends slowly.
///
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
......@@ -546,6 +554,13 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4}
static const Cubic easeIn = Cubic(0.42, 0.0, 1.0, 1.0);
/// A cubic animation curve that starts starts slowly and ends linearly.
///
/// The symmetric animation to [linearToEaseOut].
///
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_to_linear.mp4}
static const Cubic easeInToLinear = Cubic(0.67, 0.03, 0.65, 0.09);
/// A cubic animation curve that starts slowly and ends quickly. This is
/// similar to [Curves.easeIn], but with sinusoidal easing for a slightly less
/// abrupt beginning and end. Nonetheless, the result is quite gentle and is
......@@ -640,6 +655,13 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4}
static const Cubic easeOut = Cubic(0.0, 0.0, 0.58, 1.0);
/// A cubic animation curve that starts linearly and ends slowly.
///
/// A symmetric animation to [easeInToLinear].
///
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/linear_to_ease_out.mp4}
static const Cubic linearToEaseOut = Cubic(0.35, 0.91, 0.33, 0.97);
/// A cubic animation curve that starts quickly and ends slowly. This is
/// similar to [Curves.easeOut], but with sinusoidal easing for a slightly
/// less abrupt beginning and end. Nonetheless, the result is quite gentle and
......
......@@ -4,15 +4,25 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/animation.dart' show Curves;
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.
// Barrier color for a Cupertino modal barrier.
const Color _kModalBarrierColor = Color(0x6604040F);
......@@ -152,7 +162,8 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
final bool maintainState;
@override
Duration get transitionDuration => const Duration(milliseconds: 350);
// A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400);
@override
Color get barrierColor => null;
......@@ -346,20 +357,26 @@ class CupertinoPageTransition extends StatelessWidget {
@required bool linearTransition,
}) : assert(linearTransition != null),
_primaryPositionAnimation = (linearTransition ? primaryRouteAnimation :
// The curves below have been rigorously derived from plots of native
// iOS animation frames. Specifically, a video was taken of a page
// transition animation and the distance in each frame that the page
// moved was measured. A best fit bezier curve was the fitted to the
// point set, which is linearToEaseIn. Conversely, easeInToLinear is the
// reflection over the origin of linearToEaseIn.
CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
)
).drive(_kRightMiddleTween),
_secondaryPositionAnimation = CurvedAnimation(
parent: secondaryRouteAnimation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(_kMiddleLeftTween),
_primaryShadowAnimation = CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.easeOut,
curve: Curves.linearToEaseOut,
).drive(_kGradientShadowTween),
super(key: key);
......@@ -593,13 +610,32 @@ class _CupertinoBackGestureController<T> {
// Fling in the appropriate direction.
// AnimationController.fling is guaranteed to
// take at least one frame.
if (velocity.abs() >= _kMinFlingVelocity) {
controller.fling(velocity: -velocity);
} else if (controller.value <= 0.5) {
controller.fling(velocity: -1.0);
//
// This curve has been determined through rigorously eyeballing native iOS
// animations.
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
bool animateForward;
// If the user releases the page before mid screen with sufficient velocity,
// or after mid screen, we should animate the page out. Otherwise, the page
// should be animated back in.
if (velocity.abs() >= _kMinFlingVelocity)
animateForward = velocity > 0 ? false : true;
else
animateForward = controller.value > 0.5 ? true : false;
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);
} else {
controller.fling(velocity: 1.0);
final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor();
controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve);
}
assert(controller.isAnimating);
assert(controller.status != AnimationStatus.completed);
assert(controller.status != AnimationStatus.dismissed);
......
......@@ -40,6 +40,9 @@ void main() {
// Page 2 is coming in from the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Will need to be changed if the animation curve or duration changes.
expect(widget1TransientTopLeft.dx, closeTo(130, 1.0));
await tester.pumpAndSettle();
// Page 2 covers page 1.
......@@ -62,6 +65,9 @@ void main() {
// Page 2 is leaving towards the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Will need to be changed if the animation curve or duration changes.
expect(widget1TransientTopLeft.dx, closeTo(249, 1.0));
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
......@@ -222,7 +228,7 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pump(const Duration(seconds: 1));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
......@@ -285,7 +291,7 @@ void main() {
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pump(const Duration(seconds: 2));
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
......@@ -293,7 +299,7 @@ void main() {
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
......@@ -332,7 +338,7 @@ void main() {
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
......@@ -341,7 +347,7 @@ void main() {
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
......@@ -379,7 +385,7 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
......
......@@ -411,7 +411,7 @@ void main() {
await tester.pageBack();
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(find.text('Next'), findsOneWidget);
expect(find.text('Page 2'), findsNothing);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment