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
This commit is contained in:
parent
359862bb8d
commit
e47a421e42
@ -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 {
|
|||||||
|
|
||||||
_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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
|||||||
/// 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> {
|
|||||||
|
|
||||||
@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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
// 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() {
|
|||||||
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user