Commit e47a421e authored by Matt Perry's avatar Matt Perry Committed by GitHub

Use slide-from-right transition only when doing back gesture

This eliminates all the issues around the horizontal page transition. I also improved the back gesture animation, and added fling support.

BUG=https://github.com/flutter/flutter/issues/5678
BUG=https://github.com/flutter/flutter/issues/5622
parent 359862bb
...@@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart'; ...@@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart'; import 'theme.dart';
const double _kMinFlingVelocity = 1.0; // screen width per second
// Used for Android and Fuchsia. // Used for Android and Fuchsia.
class _MountainViewPageTransition extends AnimatedWidget { class _MountainViewPageTransition extends AnimatedWidget {
static final FractionalOffsetTween _kTween = new FractionalOffsetTween( static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
...@@ -48,14 +50,13 @@ class _CupertinoPageTransition extends AnimatedWidget { ...@@ -48,14 +50,13 @@ class _CupertinoPageTransition extends AnimatedWidget {
_CupertinoPageTransition({ _CupertinoPageTransition({
Key key, Key key,
Curve curve,
Animation<double> animation, Animation<double> animation,
this.child this.child
}) : super( }) : super(
key: key, key: key,
animation: _kTween.animate(new CurvedAnimation( animation: _kTween.animate(new CurvedAnimation(
parent: animation, parent: animation,
curve: new _CupertinoTransitionCurve(curve) curve: new _CupertinoTransitionCurve(null)
) )
)); ));
...@@ -142,18 +143,23 @@ class _CupertinoBackGestureController extends NavigationGestureController { ...@@ -142,18 +143,23 @@ class _CupertinoBackGestureController extends NavigationGestureController {
} }
@override @override
void dragEnd() { void dragEnd(double velocity) {
if (controller.value <= 0.5) { if (velocity.abs() >= _kMinFlingVelocity) {
navigator.pop(); controller.fling(velocity: -velocity);
} else if (controller.value <= 0.5) {
controller.fling(velocity: -1.0);
} else { } else {
controller.forward(); controller.fling(velocity: 1.0);
} }
// Don't end the gesture until the transition completes. // Don't end the gesture until the transition completes.
handleStatusChanged(controller.status); handleStatusChanged(controller.status);
controller?.addStatusListener(handleStatusChanged); controller?.addStatusListener(handleStatusChanged);
} }
void handleStatusChanged(AnimationStatus status) { void handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed)
navigator.pop();
if (status == AnimationStatus.dismissed || status == AnimationStatus.completed) if (status == AnimationStatus.dismissed || status == AnimationStatus.completed)
dispose(); dispose();
} }
...@@ -164,9 +170,6 @@ class _CupertinoBackGestureController extends NavigationGestureController { ...@@ -164,9 +170,6 @@ class _CupertinoBackGestureController extends NavigationGestureController {
/// The entrance transition for the page slides the page upwards and fades it /// The entrance transition for the page slides the page upwards and fades it
/// in. The exit transition is the same, but in reverse. /// 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 /// 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 /// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false. /// [maintainState] to false.
...@@ -235,33 +238,18 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -235,33 +238,18 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) { Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
// TODO(mpcomplete): This hack prevents the previousRoute from animating if (Theme.of(context).platform == TargetPlatform.iOS &&
// when we pop(). Remove once we fix this bug: Navigator.of(context).userGestureInProgress) {
// https://github.com/flutter/flutter/issues/5577 return new _CupertinoPageTransition(
bool userGesture = Navigator.of(context).userGestureInProgress; animation: new AnimationMean(left: animation, right: forwardAnimation),
if (!userGesture) child: child
forwardAnimation = kAlwaysDismissedAnimation; );
} else {
ThemeData theme = Theme.of(context); return new _MountainViewPageTransition(
switch (theme.platform) { animation: animation,
case TargetPlatform.fuchsia: child: child
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
);
} }
return null;
} }
@override @override
......
...@@ -710,12 +710,13 @@ class ScaffoldState extends State<Scaffold> { ...@@ -710,12 +710,13 @@ class ScaffoldState extends State<Scaffold> {
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
_backGestureController?.dragEnd(); final RenderBox box = context.findRenderObject();
_backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / box.size.width);
_backGestureController = null; _backGestureController = null;
} }
void _handleDragCancel() { void _handleDragCancel() {
_backGestureController?.dragEnd(); _backGestureController?.dragEnd(0.0);
_backGestureController = null; _backGestureController = null;
} }
......
...@@ -176,8 +176,9 @@ abstract class NavigationGestureController { ...@@ -176,8 +176,9 @@ abstract class NavigationGestureController {
// drag should be 0.0 to 1.0. // drag should be 0.0 to 1.0.
void dragUpdate(double fractionalDelta); void dragUpdate(double fractionalDelta);
// The drag gesture has ended. // The drag gesture has ended with a horizontal motion of
void dragEnd(); // [fractionalVelocity] as a fraction of screen width per second.
void dragEnd(double fractionalVelocity);
@protected @protected
NavigatorState get navigator => _navigator; NavigatorState get navigator => _navigator;
......
...@@ -155,4 +155,135 @@ void main() { ...@@ -155,4 +155,135 @@ void main() {
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnstage); 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<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) => 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<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) => 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);
});
} }
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