diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index 3cfebf1fb2..eba6a78794 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -417,9 +417,13 @@ class CupertinoFullscreenDialogTransition extends StatelessWidget { Key key, @required Animation animation, @required this.child, - }) : _positionAnimation = animation - .drive(CurveTween(curve: Curves.easeInOut)) - .drive(_kBottomUpTween), + }) : _positionAnimation = CurvedAnimation( + parent: animation, + curve: Curves.linearToEaseOut, + // The curve must be flipped so that the reverse animation doesn't play + // an ease-in curve, which iOS does not use. + reverseCurve: Curves.linearToEaseOut.flipped, + ).drive(_kBottomUpTween), super(key: key); final Animation _positionAnimation; @@ -816,8 +820,11 @@ class _CupertinoModalPopupRoute extends PopupRoute { assert(_animation == null); _animation = CurvedAnimation( parent: super.createAnimation(), - curve: Curves.ease, - reverseCurve: Curves.ease.flipped, + + // These curves were initially measured from native iOS horizontal page + // route animations and seemed to be a good match here as well. + curve: Curves.linearToEaseOut, + reverseCurve: Curves.linearToEaseOut.flipped, ); _offsetTween = Tween( begin: const Offset(0.0, 1.0), @@ -879,8 +886,11 @@ Future showCupertinoModalPopup({ ); } -final Animatable _dialogTween = Tween(begin: 1.2, end: 1.0) - .chain(CurveTween(curve: Curves.fastOutSlowIn)); +// The curve and initial scale values were mostly eyeballed from iOS, however +// they reuse the same animation curve that was modelled after native page +// transitions. +final Animatable _dialogScaleTween = Tween(begin: 1.3, end: 1.0) + .chain(CurveTween(curve: Curves.linearToEaseOut)); Widget _buildCupertinoDialogTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { final CurvedAnimation fadeAnimation = CurvedAnimation( @@ -897,7 +907,7 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation opacity: fadeAnimation, child: ScaleTransition( child: child, - scale: animation.drive(_dialogTween), + scale: animation.drive(_dialogScaleTween), ), ); } @@ -941,7 +951,8 @@ Future showCupertinoDialog({ context: context, barrierDismissible: false, barrierColor: _kModalBarrierColor, - transitionDuration: const Duration(milliseconds: 300), + // This transition duration was eyeballed comparing with iOS + transitionDuration: const Duration(milliseconds: 250), pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return builder(context); }, diff --git a/packages/flutter/test/cupertino/action_sheet_test.dart b/packages/flutter/test/cupertino/action_sheet_test.dart index 0619002143..ef4410cb17 100644 --- a/packages/flutter/test/cupertino/action_sheet_test.dart +++ b/packages/flutter/test/cupertino/action_sheet_test.dart @@ -718,19 +718,19 @@ void main() { expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(470.0, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(374.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(365.0, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(337.1, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(334.0, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(325.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(321.0, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(320.8, 0.1)); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(319.3, 0.1)); @@ -745,19 +745,19 @@ void main() { expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(319.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(388.4, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(449.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(492.6, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(544.9, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(554.2, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(582.1, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(585.2, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(593.9, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(598.2, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(598.5, 0.1)); // Action sheet has disappeared await tester.pump(const Duration(milliseconds: 60)); @@ -795,23 +795,23 @@ void main() { expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(470.0, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(374.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(365.0, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(337.1, 0.1)); // Exit animation await tester.tapAt(const Offset(20.0, 20.0)); await tester.pump(const Duration(milliseconds: 60)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(374.3, 0.1)); await tester.pump(const Duration(milliseconds: 60)); - expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1)); + expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(470.0, 0.1)); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0); diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart index eb4fb45995..bb414b851c 100644 --- a/packages/flutter/test/cupertino/dialog_test.dart +++ b/packages/flutter/test/cupertino/dialog_test.dart @@ -847,15 +847,11 @@ void main() { // Enter animation. await tester.pump(); Transform transform = tester.widget(find.byType(Transform)); - expect(transform.transform[0], closeTo(1.2, 0.01)); + expect(transform.transform[0], closeTo(1.3, 0.01)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); - expect(transform.transform[0], closeTo(1.182, 0.001)); - - await tester.pump(const Duration(milliseconds: 50)); - transform = tester.widget(find.byType(Transform)); - expect(transform.transform[0], closeTo(1.108, 0.001)); + expect(transform.transform[0], closeTo(1.145, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); @@ -863,7 +859,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); - expect(transform.transform[0], closeTo(1.015, 0.001)); + expect(transform.transform[0], closeTo(1.013, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); @@ -873,6 +869,10 @@ void main() { transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.000, 0.001)); + await tester.pump(const Duration(milliseconds: 50)); + transform = tester.widget(find.byType(Transform)); + expect(transform.transform[0], closeTo(1.000, 0.001)); + await tester.tap(find.text('Delete')); await tester.pump(); @@ -951,23 +951,23 @@ void main() { // Exit animation, look at reverse FadeTransition. await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); - expect(transition.opacity.value, closeTo(0.358, 0.001)); + expect(transition.opacity.value, closeTo(0.500, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); - expect(transition.opacity.value, closeTo(0.231, 0.001)); + expect(transition.opacity.value, closeTo(0.332, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); - expect(transition.opacity.value, closeTo(0.128, 0.001)); + expect(transition.opacity.value, closeTo(0.188, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); - expect(transition.opacity.value, closeTo(0.056, 0.001)); + expect(transition.opacity.value, closeTo(0.081, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); - expect(transition.opacity.value, closeTo(0.013, 0.001)); + expect(transition.opacity.value, closeTo(0.019, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart index efa7def3e1..a24ed979a0 100644 --- a/packages/flutter/test/cupertino/route_test.dart +++ b/packages/flutter/test/cupertino/route_test.dart @@ -328,4 +328,107 @@ void main() { expect(find.text('route'), findsOneWidget); expect(find.text('push'), findsNothing); }); + + testWidgets('Fullscreen route animates correct transform values over time', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Builder( + builder: (BuildContext context) { + return CupertinoButton( + child: const Text('Button'), + onPressed: () { + Navigator.push(context, CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return Column( + children: [ + const Placeholder(), + CupertinoButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ); + }, + )); + }, + ); + } + ), + ), + ); + + // Enter animation. + await tester.tap(find.text('Button')); + await tester.pump(); + + // We use a higher number of intervals since the animation has to scale the + // entire screen. + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(443.7, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(291.9, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(168.2, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(89.5, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(48.1, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(26.1, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(14.3, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(7.41, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(3.0, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(0.0, 0.1)); + + // Exit animation + await tester.tap(find.text('Close')); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(156.3, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(308.1, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(431.7, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(510.4, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(551.8, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(573.8, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(585.6, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(592.6, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(596.9, 0.1)); + + await tester.pump(const Duration(milliseconds: 40)); + expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(600.0, 0.1)); + }); }