diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index c69db7c57c..cd50fc332d 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart'; import 'material.dart'; import 'theme.dart'; +const double _kMinFlingVelocity = 1.0; // screen width per second + // Used for Android and Fuchsia. class _MountainViewPageTransition extends AnimatedWidget { static final FractionalOffsetTween _kTween = new FractionalOffsetTween( @@ -48,14 +50,13 @@ class _CupertinoPageTransition extends AnimatedWidget { _CupertinoPageTransition({ Key key, - Curve curve, Animation animation, this.child }) : super( key: key, animation: _kTween.animate(new CurvedAnimation( parent: animation, - curve: new _CupertinoTransitionCurve(curve) + curve: new _CupertinoTransitionCurve(null) ) )); @@ -142,18 +143,23 @@ class _CupertinoBackGestureController extends NavigationGestureController { } @override - void dragEnd() { - if (controller.value <= 0.5) { - navigator.pop(); + void dragEnd(double velocity) { + if (velocity.abs() >= _kMinFlingVelocity) { + controller.fling(velocity: -velocity); + } else if (controller.value <= 0.5) { + controller.fling(velocity: -1.0); } else { - controller.forward(); + controller.fling(velocity: 1.0); } + // Don't end the gesture until the transition completes. handleStatusChanged(controller.status); controller?.addStatusListener(handleStatusChanged); } void handleStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.dismissed) + navigator.pop(); if (status == AnimationStatus.dismissed || status == AnimationStatus.completed) dispose(); } @@ -164,9 +170,6 @@ class _CupertinoBackGestureController extends NavigationGestureController { /// The entrance transition for the page slides the page upwards and fades it /// in. The exit transition is the same, but in reverse. /// -/// [MaterialApp] creates material page routes for entries in the -/// [MaterialApp.routes] map. -/// /// By default, when a modal route is replaced by another, the previous route /// remains in memory. To free all the resources when this is not necessary, set /// [maintainState] to false. @@ -235,33 +238,18 @@ class MaterialPageRoute extends PageRoute { @override Widget buildTransitions(BuildContext context, Animation animation, Animation forwardAnimation, Widget child) { - // TODO(mpcomplete): This hack prevents the previousRoute from animating - // when we pop(). Remove once we fix this bug: - // https://github.com/flutter/flutter/issues/5577 - bool userGesture = Navigator.of(context).userGestureInProgress; - if (!userGesture) - forwardAnimation = kAlwaysDismissedAnimation; - - ThemeData theme = Theme.of(context); - switch (theme.platform) { - case TargetPlatform.fuchsia: - case TargetPlatform.android: - return new _MountainViewPageTransition( - animation: animation, - child: child - ); - case TargetPlatform.iOS: - return new _CupertinoPageTransition( - // Use a linear curve when controlled by a user gesture. This ensures - // the animation tracks the user's finger 1:1. - // See https://github.com/flutter/flutter/issues/5664 - curve: userGesture ? null : Curves.fastOutSlowIn, - animation: new AnimationMean(left: animation, right: forwardAnimation), - child: child - ); + if (Theme.of(context).platform == TargetPlatform.iOS && + Navigator.of(context).userGestureInProgress) { + return new _CupertinoPageTransition( + animation: new AnimationMean(left: animation, right: forwardAnimation), + child: child + ); + } else { + return new _MountainViewPageTransition( + animation: animation, + child: child + ); } - - return null; } @override diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 603c87d739..1778bf46be 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -710,12 +710,13 @@ class ScaffoldState extends State { } void _handleDragEnd(DragEndDetails details) { - _backGestureController?.dragEnd(); + final RenderBox box = context.findRenderObject(); + _backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / box.size.width); _backGestureController = null; } void _handleDragCancel() { - _backGestureController?.dragEnd(); + _backGestureController?.dragEnd(0.0); _backGestureController = null; } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index b4f3ad587c..4f5246ebb5 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -176,8 +176,9 @@ abstract class NavigationGestureController { // drag should be 0.0 to 1.0. void dragUpdate(double fractionalDelta); - // The drag gesture has ended. - void dragEnd(); + // The drag gesture has ended with a horizontal motion of + // [fractionalVelocity] as a fraction of screen width per second. + void dragEnd(double fractionalVelocity); @protected NavigatorState get navigator => _navigator; diff --git a/packages/flutter/test/widget/page_transitions_test.dart b/packages/flutter/test/widget/page_transitions_test.dart index 50cd48844e..ee3c3a9a05 100644 --- a/packages/flutter/test/widget/page_transitions_test.dart +++ b/packages/flutter/test/widget/page_transitions_test.dart @@ -155,4 +155,135 @@ void main() { expect(find.text('Home'), findsNothing); expect(find.text('Settings'), isOnstage); }); + + testWidgets('Check page transition positioning on iOS', (WidgetTester tester) async { + GlobalKey containerKey1 = new GlobalKey(); + GlobalKey containerKey2 = new GlobalKey(); + final Map routes = { + '/': (_) => new Scaffold(key: containerKey1, body: new Text('Home')), + '/settings': (_) => new Scaffold(key: containerKey2, body: new Text('Settings')), + }; + + await tester.pumpWidget(new MaterialApp( + routes: routes, + theme: new ThemeData(platform: TargetPlatform.iOS), + )); + + Navigator.pushNamed(containerKey1.currentContext, '/settings'); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 16)); + + expect(find.text('Home'), isOnstage); + expect(find.text('Settings'), isOnstage); + + // Home page is staying in place. + Point homeOffset = tester.getTopLeft(find.text('Home')); + expect(homeOffset.x, 0.0); + expect(homeOffset.y, 0.0); + + // Settings page is sliding up from the bottom. + Point settingsOffset = tester.getTopLeft(find.text('Settings')); + expect(settingsOffset.x, 0.0); + expect(settingsOffset.y, greaterThan(0.0)); + + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Home'), findsNothing); + expect(find.text('Settings'), isOnstage); + + // Settings page is in position. + settingsOffset = tester.getTopLeft(find.text('Settings')); + expect(settingsOffset.x, 0.0); + expect(settingsOffset.y, 0.0); + + Navigator.pop(containerKey1.currentContext); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 16)); + + // Home page is staying in place. + homeOffset = tester.getTopLeft(find.text('Home')); + expect(homeOffset.x, 0.0); + expect(homeOffset.y, 0.0); + + // Settings page is sliding down off the bottom. + settingsOffset = tester.getTopLeft(find.text('Settings')); + expect(settingsOffset.x, 0.0); + expect(settingsOffset.y, greaterThan(0.0)); + + await tester.pump(const Duration(seconds: 1)); + }); + + testWidgets('Check back gesture disables Heroes', (WidgetTester tester) async { + GlobalKey containerKey1 = new GlobalKey(); + GlobalKey containerKey2 = new GlobalKey(); + const String kHeroTag = 'hero'; + final Map routes = { + '/': (_) => new Scaffold( + key: containerKey1, + body: new Container( + decoration: new BoxDecoration(backgroundColor: const Color(0xff00ffff)), + child: new Hero( + tag: kHeroTag, + child: new Text('Home') + ) + ) + ), + '/settings': (_) => new Scaffold( + key: containerKey2, + body: new Container( + padding: const EdgeInsets.all(100.0), + decoration: new BoxDecoration(backgroundColor: const Color(0xffff00ff)), + child: new Hero( + tag: kHeroTag, + child: new Text('Settings') + ) + ) + ), + }; + + await tester.pumpWidget(new MaterialApp( + routes: routes, + theme: new ThemeData(platform: TargetPlatform.iOS), + )); + + Navigator.pushNamed(containerKey1.currentContext, '/settings'); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 16)); + + expect(find.text('Settings'), isOnstage); + + // Settings text is heroing to its new location + Point settingsOffset = tester.getTopLeft(find.text('Settings')); + expect(settingsOffset.x, greaterThan(0.0)); + expect(settingsOffset.x, lessThan(100.0)); + expect(settingsOffset.y, greaterThan(0.0)); + expect(settingsOffset.y, lessThan(100.0)); + + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Home'), findsNothing); + expect(find.text('Settings'), isOnstage); + + // Drag from left edge to invoke the gesture. + TestGesture gesture = await tester.startGesture(new Point(5.0, 100.0)); + await gesture.moveBy(new Offset(50.0, 0.0)); + await tester.pump(); + + // Home is now visible. + expect(find.text('Home'), isOnstage); + expect(find.text('Settings'), isOnstage); + + // Home page is sliding in from the left, no heroes. + Point homeOffset = tester.getTopLeft(find.text('Home')); + expect(homeOffset.x, lessThan(0.0)); + expect(homeOffset.y, 0.0); + + // Settings page is sliding off to the right, no heroes. + settingsOffset = tester.getTopLeft(find.text('Settings')); + expect(settingsOffset.x, greaterThan(100.0)); + expect(settingsOffset.y, 100.0); + }); }