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';
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<double> 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<T> extends PageRoute<T> {
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> 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
......
......@@ -710,12 +710,13 @@ class ScaffoldState extends State<Scaffold> {
}
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;
}
......
......@@ -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;
......
......@@ -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<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