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> ...@@ -487,6 +487,23 @@ class AnimationController extends Animation<double>
return _animateToInternal(target, duration: duration, curve: curve); 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 }) { TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) {
final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
double scale = 1.0; double scale = 1.0;
......
...@@ -316,7 +316,6 @@ class _DecelerateCurve extends Curve { ...@@ -316,7 +316,6 @@ class _DecelerateCurve extends Curve {
} }
} }
// BOUNCE CURVES // BOUNCE CURVES
double _bounce(double t) { double _bounce(double t) {
...@@ -536,6 +535,15 @@ class Curves { ...@@ -536,6 +535,15 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4} /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4}
static const Curve decelerate = _DecelerateCurve._(); 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. /// 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} /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
...@@ -546,6 +554,13 @@ class Curves { ...@@ -546,6 +554,13 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4} /// {@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); 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 /// A cubic animation curve that starts slowly and ends quickly. This is
/// similar to [Curves.easeIn], but with sinusoidal easing for a slightly less /// similar to [Curves.easeIn], but with sinusoidal easing for a slightly less
/// abrupt beginning and end. Nonetheless, the result is quite gentle and is /// abrupt beginning and end. Nonetheless, the result is quite gentle and is
...@@ -640,6 +655,13 @@ class Curves { ...@@ -640,6 +655,13 @@ class Curves {
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4} /// {@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); 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 /// A cubic animation curve that starts quickly and ends slowly. This is
/// similar to [Curves.easeOut], but with sinusoidal easing for a slightly /// similar to [Curves.easeOut], but with sinusoidal easing for a slightly
/// less abrupt beginning and end. Nonetheless, the result is quite gentle and /// less abrupt beginning and end. Nonetheless, the result is quite gentle and
......
...@@ -4,15 +4,25 @@ ...@@ -4,15 +4,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/animation.dart' show Curves;
const double _kBackGestureWidth = 20.0; const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second. 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. // Barrier color for a Cupertino modal barrier.
const Color _kModalBarrierColor = Color(0x6604040F); const Color _kModalBarrierColor = Color(0x6604040F);
...@@ -152,7 +162,8 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -152,7 +162,8 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
final bool maintainState; final bool maintainState;
@override @override
Duration get transitionDuration => const Duration(milliseconds: 350); // A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400);
@override @override
Color get barrierColor => null; Color get barrierColor => null;
...@@ -346,20 +357,26 @@ class CupertinoPageTransition extends StatelessWidget { ...@@ -346,20 +357,26 @@ class CupertinoPageTransition extends StatelessWidget {
@required bool linearTransition, @required bool linearTransition,
}) : assert(linearTransition != null), }) : assert(linearTransition != null),
_primaryPositionAnimation = (linearTransition ? primaryRouteAnimation : _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( CurvedAnimation(
parent: primaryRouteAnimation, parent: primaryRouteAnimation,
curve: Curves.easeOut, curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeInToLinear,
) )
).drive(_kRightMiddleTween), ).drive(_kRightMiddleTween),
_secondaryPositionAnimation = CurvedAnimation( _secondaryPositionAnimation = CurvedAnimation(
parent: secondaryRouteAnimation, parent: secondaryRouteAnimation,
curve: Curves.easeOut, curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeInToLinear,
).drive(_kMiddleLeftTween), ).drive(_kMiddleLeftTween),
_primaryShadowAnimation = CurvedAnimation( _primaryShadowAnimation = CurvedAnimation(
parent: primaryRouteAnimation, parent: primaryRouteAnimation,
curve: Curves.easeOut, curve: Curves.linearToEaseOut,
).drive(_kGradientShadowTween), ).drive(_kGradientShadowTween),
super(key: key); super(key: key);
...@@ -593,13 +610,32 @@ class _CupertinoBackGestureController<T> { ...@@ -593,13 +610,32 @@ class _CupertinoBackGestureController<T> {
// Fling in the appropriate direction. // Fling in the appropriate direction.
// AnimationController.fling is guaranteed to // AnimationController.fling is guaranteed to
// take at least one frame. // take at least one frame.
if (velocity.abs() >= _kMinFlingVelocity) { //
controller.fling(velocity: -velocity); // This curve has been determined through rigorously eyeballing native iOS
} else if (controller.value <= 0.5) { // animations.
controller.fling(velocity: -1.0); 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 { } 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.isAnimating);
assert(controller.status != AnimationStatus.completed); assert(controller.status != AnimationStatus.completed);
assert(controller.status != AnimationStatus.dismissed); assert(controller.status != AnimationStatus.dismissed);
......
...@@ -40,6 +40,9 @@ void main() { ...@@ -40,6 +40,9 @@ void main() {
// Page 2 is coming in from the right. // Page 2 is coming in from the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx)); 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(); await tester.pumpAndSettle();
// Page 2 covers page 1. // Page 2 covers page 1.
...@@ -62,6 +65,9 @@ void main() { ...@@ -62,6 +65,9 @@ void main() {
// Page 2 is leaving towards the right. // Page 2 is leaving towards the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx)); 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(); await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage); expect(find.text('Page 1'), isOnstage);
...@@ -222,7 +228,7 @@ void main() { ...@@ -222,7 +228,7 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(seconds: 1));
// Page 2 covers page 1. // Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
...@@ -285,7 +291,7 @@ void main() { ...@@ -285,7 +291,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(seconds: 2));
tester.state<NavigatorState>(find.byType(Navigator)).push( tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>( CupertinoPageRoute<void>(
...@@ -293,7 +299,7 @@ void main() { ...@@ -293,7 +299,7 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage); expect(find.text('Page 2'), isOnstage);
...@@ -332,7 +338,7 @@ void main() { ...@@ -332,7 +338,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
tester.state<NavigatorState>(find.byType(Navigator)).push( tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>( CupertinoPageRoute<void>(
...@@ -341,7 +347,7 @@ void main() { ...@@ -341,7 +347,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage); expect(find.text('Page 2'), isOnstage);
...@@ -379,7 +385,7 @@ void main() { ...@@ -379,7 +385,7 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
// Page 2 covers page 1. // Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
......
...@@ -411,7 +411,7 @@ void main() { ...@@ -411,7 +411,7 @@ void main() {
await tester.pageBack(); await tester.pageBack();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
expect(find.text('Next'), findsOneWidget); expect(find.text('Next'), findsOneWidget);
expect(find.text('Page 2'), findsNothing); 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