Commit 58ddde88 authored by xster's avatar xster Committed by GitHub

Make Cupertino page transitions match native behaviours (#9138)

* Moved stuff around yet

* Fix depedencies

* Add more dartdoc comments to packages

* Remove Cupertino dependency on material

* Removed mountain_view package and added page transition test

* Fix analyze warnings

* Remove commented code

* Partial solution. Still need to stop the animation on the previous page for modal

* Some review notes

* Move the cupertino back gesture controller’s lifecycle management back to its parent

* Reviews

* Add background color

* Directional curves, full screen transition

* Don’t perform the exit animation if the incoming page is a dialog

* It works!

* Test structures

* Add a bunch of more tests and fix the gallery

* One more comment

* Review notes

* final controller

* Use that sweet sweet `is!` keyword

* Play golf, because I’m bitter that there’s no nullable `as` or something in dart

* Remove a space

* Review notes

* Remove the last deprecated test
parent c7f98efb
...@@ -192,7 +192,8 @@ class DialogDemoState extends State<DialogDemo> { ...@@ -192,7 +192,8 @@ class DialogDemoState extends State<DialogDemo> {
child: new Text('FULLSCREEN'), child: new Text('FULLSCREEN'),
onPressed: () { onPressed: () {
Navigator.push(context, new MaterialPageRoute<DismissDialogAction>( Navigator.push(context, new MaterialPageRoute<DismissDialogAction>(
builder: (BuildContext context) => new FullScreenDialogDemo() builder: (BuildContext context) => new FullScreenDialogDemo(),
fullscreenDialog: true,
)); ));
} }
), ),
......
...@@ -8,6 +8,18 @@ import 'package:flutter/widgets.dart'; ...@@ -8,6 +8,18 @@ import 'package:flutter/widgets.dart';
const double _kMinFlingVelocity = 1.0; // screen width per second. const double _kMinFlingVelocity = 1.0; // screen width per second.
const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background color. const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background color.
// Fractional offset from offscreen to the right to fully on screen.
final FractionalOffsetTween _kRightMiddleTween = new FractionalOffsetTween(
begin: FractionalOffset.topRight,
end: FractionalOffset.topLeft,
);
// Fractional offset from fully on screen to 1/3 offscreen to the left.
final FractionalOffsetTween _kMiddleLeftTween = new FractionalOffsetTween(
begin: FractionalOffset.topLeft,
end: const FractionalOffset(-1.0/3.0, 0.0),
);
/// Provides the native iOS page transition animation. /// Provides the native iOS page transition animation.
/// ///
/// Takes in a page widget and a route animation from a [TransitionRoute] and produces an /// Takes in a page widget and a route animation from a [TransitionRoute] and produces an
...@@ -18,21 +30,39 @@ const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background co ...@@ -18,21 +30,39 @@ const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background co
class CupertinoPageTransition extends AnimatedWidget { class CupertinoPageTransition extends AnimatedWidget {
CupertinoPageTransition({ CupertinoPageTransition({
Key key, Key key,
@required Animation<double> animation, // Linear route animation from 0.0 to 1.0 when this screen is being pushed.
@required Animation<double> incomingRouteAnimation,
// Linear route animation from 0.0 to 1.0 when another screen is being pushed on top of this
// one.
@required Animation<double> outgoingRouteAnimation,
@required this.child, @required this.child,
}) : super( }) :
incomingPositionAnimation = _kRightMiddleTween.animate(
new CurvedAnimation(
parent: incomingRouteAnimation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
)
),
outgoingPositionAnimation = _kMiddleLeftTween.animate(
new CurvedAnimation(
parent: outgoingRouteAnimation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
)
),
super(
key: key, key: key,
listenable: _kTween.animate(new CurvedAnimation( // Trigger a rebuild whenever any of the 2 animation route happens.
parent: animation, listenable: new Listenable.merge(
curve: new _CupertinoTransitionCurve(null), <Listenable>[incomingRouteAnimation, outgoingRouteAnimation]
), ),
));
static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
begin: FractionalOffset.topRight,
end: -FractionalOffset.topRight,
); );
// When this page is coming in to cover another page.
final Animation<FractionalOffset> incomingPositionAnimation;
// When this page is becoming covered by another page.
final Animation<FractionalOffset> outgoingPositionAnimation;
final Widget child; final Widget child;
@override @override
...@@ -40,45 +70,50 @@ class CupertinoPageTransition extends AnimatedWidget { ...@@ -40,45 +70,50 @@ class CupertinoPageTransition extends AnimatedWidget {
// TODO(ianh): tell the transform to be un-transformed for hit testing // TODO(ianh): tell the transform to be un-transformed for hit testing
// but not while being controlled by a gesture. // but not while being controlled by a gesture.
return new SlideTransition( return new SlideTransition(
position: listenable, position: outgoingPositionAnimation,
child: new SlideTransition(
position: incomingPositionAnimation,
child: new PhysicalModel( child: new PhysicalModel(
shape: BoxShape.rectangle, shape: BoxShape.rectangle,
color: _kBackgroundColor, color: _kBackgroundColor,
elevation: 16, elevation: 32,
child: child, child: child,
) ),
),
); );
} }
} }
// Custom curve for iOS page transitions. /// Transitions used for summoning fullscreen dialogs in iOS such as creating a new
class _CupertinoTransitionCurve extends Curve { /// calendar event etc by bringing in the next screen from the bottom.
_CupertinoTransitionCurve(this.curve); class CupertinoFullscreenDialogTransition extends AnimatedWidget {
CupertinoFullscreenDialogTransition({
Key key,
@required Animation<double> animation,
@required this.child,
}) : super(
key: key,
listenable: _kBottomUpTween.animate(
new CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)
),
);
final Curve curve; static final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween(
begin: FractionalOffset.bottomLeft,
end: FractionalOffset.topLeft,
);
final Widget child;
@override @override
double transform(double t) { Widget build(BuildContext context) {
// The input [t] is the average of the current and next route's animation. return new SlideTransition(
// This means t=0.5 represents when the route is fully onscreen. At position: listenable,
// t > 0.5, it is partially offscreen to the left (which happens when there child: child,
// is another route on top). At t < 0.5, the route is to the right. );
// We divide the range into two halves, each with a different transition,
// and scale each half to the range [0.0, 1.0] before applying curves so that
// each half goes through the full range of the curve.
if (t > 0.5) {
// Route is to the left of center.
t = (t - 0.5) * 2.0;
if (curve != null)
t = curve.transform(t);
t = t / 3.0;
t = t / 2.0 + 0.5;
} else {
// Route is to the right of center.
if (curve != null)
t = curve.transform(t * 2.0) / 2.0;
}
return t;
} }
} }
...@@ -96,7 +131,7 @@ class CupertinoBackGestureController extends NavigationGestureController { ...@@ -96,7 +131,7 @@ class CupertinoBackGestureController extends NavigationGestureController {
@override @override
void dispose() { void dispose() {
controller.removeStatusListener(handleStatusChanged); controller.removeStatusListener(_handleStatusChanged);
super.dispose(); super.dispose();
} }
...@@ -130,13 +165,13 @@ class CupertinoBackGestureController extends NavigationGestureController { ...@@ -130,13 +165,13 @@ class CupertinoBackGestureController extends NavigationGestureController {
// Don't end the gesture until the transition completes. // Don't end the gesture until the transition completes.
final AnimationStatus status = controller.status; final AnimationStatus status = controller.status;
handleStatusChanged(status); _handleStatusChanged(status);
controller?.addStatusListener(handleStatusChanged); controller?.addStatusListener(_handleStatusChanged);
return (status == AnimationStatus.reverse || status == AnimationStatus.dismissed); return (status == AnimationStatus.reverse || status == AnimationStatus.dismissed);
} }
void handleStatusChanged(AnimationStatus status) { void _handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed) if (status == AnimationStatus.dismissed)
navigator.pop(); navigator.pop();
} }
......
...@@ -58,12 +58,16 @@ class _MountainViewPageTransition extends AnimatedWidget { ...@@ -58,12 +58,16 @@ class _MountainViewPageTransition extends AnimatedWidget {
/// 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.
///
/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those
/// pages animate bottom->up rather than right->left.
class MaterialPageRoute<T> extends PageRoute<T> { class MaterialPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in a material design app. /// Creates a page route for use in a material design app.
MaterialPageRoute({ MaterialPageRoute({
@required this.builder, @required this.builder,
RouteSettings settings: const RouteSettings(), RouteSettings settings: const RouteSettings(),
this.maintainState: true, this.maintainState: true,
this.fullscreenDialog: false,
}) : super(settings: settings) { }) : super(settings: settings) {
assert(builder != null); assert(builder != null);
assert(opaque); assert(opaque);
...@@ -71,6 +75,7 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -71,6 +75,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
/// Builds the primary contents of the route. /// Builds the primary contents of the route.
final WidgetBuilder builder; final WidgetBuilder builder;
final bool fullscreenDialog;
@override @override
final bool maintainState; final bool maintainState;
...@@ -86,6 +91,12 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -86,6 +91,12 @@ class MaterialPageRoute<T> extends PageRoute<T> {
return nextRoute is MaterialPageRoute<dynamic>; return nextRoute is MaterialPageRoute<dynamic>;
} }
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a fullscreen dialog.
return nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog;
}
@override @override
void dispose() { void dispose() {
_backGestureController?.dispose(); _backGestureController?.dispose();
...@@ -109,6 +120,9 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -109,6 +120,9 @@ class MaterialPageRoute<T> extends PageRoute<T> {
// allow the user to dismiss the route with a swipe. // allow the user to dismiss the route with a swipe.
if (hasScopedWillPopCallback) if (hasScopedWillPopCallback)
return null; return null;
// Fullscreen dialogs aren't dismissable by back swipe.
if (fullscreenDialog)
return null;
if (controller.status != AnimationStatus.completed) if (controller.status != AnimationStatus.completed)
return null; return null;
assert(_backGestureController == null); assert(_backGestureController == null);
...@@ -146,11 +160,17 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -146,11 +160,17 @@ 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) {
if (Theme.of(context).platform == TargetPlatform.iOS && if (Theme.of(context).platform == TargetPlatform.iOS) {
Navigator.of(context).userGestureInProgress) { if (fullscreenDialog)
return new CupertinoFullscreenDialogTransition(
animation: animation,
child: child,
);
else
return new CupertinoPageTransition( return new CupertinoPageTransition(
animation: new AnimationMean(left: animation, right: forwardAnimation), incomingRouteAnimation: animation,
child: child outgoingRouteAnimation: forwardAnimation,
child: child,
); );
} else { } else {
return new _MountainViewPageTransition( return new _MountainViewPageTransition(
......
...@@ -15,7 +15,7 @@ void main() { ...@@ -15,7 +15,7 @@ void main() {
'/next': (BuildContext context) { '/next': (BuildContext context) {
return new Material(child: new Text('Page 2')); return new Material(child: new Text('Page 2'));
}, },
} },
) )
); );
...@@ -25,11 +25,11 @@ void main() { ...@@ -25,11 +25,11 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
final Opacity widget2Opacity = Opacity widget2Opacity = tester.element(find.text('Page 2')).ancestorWidgetOfExactType(Opacity);
tester.element(find.text('Page 2')).ancestorWidgetOfExactType(Opacity); Point widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
final Point widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
final Size widget2Size = tester.getSize(find.text('Page 2')); final Size widget2Size = tester.getSize(find.text('Page 2'));
// Android transition is vertical only.
expect(widget1TopLeft.x == widget2TopLeft.x, true); expect(widget1TopLeft.x == widget2TopLeft.x, true);
// Page 1 is above page 2 mid-transition. // Page 1 is above page 2 mid-transition.
expect(widget1TopLeft.y < widget2TopLeft.y, true); expect(widget1TopLeft.y < widget2TopLeft.y, true);
...@@ -37,6 +37,29 @@ void main() { ...@@ -37,6 +37,29 @@ void main() {
expect(widget2TopLeft.y < widget2Size.height / 4.0, true); expect(widget2TopLeft.y < widget2Size.height / 4.0, true);
// Animation starts with page 2 being near transparent. // Animation starts with page 2 being near transparent.
expect(widget2Opacity.opacity < 0.01, true); expect(widget2Opacity.opacity < 0.01, true);
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
widget2Opacity = tester.element(find.text('Page 2')).ancestorWidgetOfExactType(Opacity);
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 2 starts to move down.
expect(widget1TopLeft.y < widget2TopLeft.y, true);
// Page 2 starts to lose opacity.
expect(widget2Opacity.opacity < 1.0, true);
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
}); });
testWidgets('test iOS page transition', (WidgetTester tester) async { testWidgets('test iOS page transition', (WidgetTester tester) async {
...@@ -48,21 +71,210 @@ void main() { ...@@ -48,21 +71,210 @@ void main() {
'/next': (BuildContext context) { '/next': (BuildContext context) {
return new Material(child: new Text('Page 2')); return new Material(child: new Text('Page 2'));
}, },
} },
) )
); );
final Point widget1TopLeft = tester.getTopLeft(find.text('Page 1')); final Point widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 250)); await tester.pump(const Duration(milliseconds: 100));
final Point widget2TopLeft = tester.getTopLeft(find.text('Page 2')); Point widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Point widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// This is currently an incorrect behaviour and we want right to left transition instead. // Page 1 is moving to the left.
// See https://github.com/flutter/flutter/issues/8726. expect(widget1TransientTopLeft.x < widget1InitialTopLeft.x, true);
expect(widget1TopLeft.x == widget2TopLeft.x, true); // Page 1 isn't moving vertically.
expect(widget1TopLeft.y - widget2TopLeft.y < 0, true); expect(widget1TransientTopLeft.y == widget1InitialTopLeft.y, true);
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.y == widget2TopLeft.y, true);
// Page 2 is coming in from the right.
expect(widget2TopLeft.x > widget1InitialTopLeft.x, true);
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is coming back from the left.
expect(widget1TransientTopLeft.x < widget1InitialTopLeft.x, true);
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.y == widget1InitialTopLeft.y, true);
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.y == widget2TopLeft.y, true);
// Page 2 is leaving towards the right.
expect(widget2TopLeft.x > widget1InitialTopLeft.x, true);
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
// Page 1 is back where it started.
expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
});
testWidgets('test iOS fullscreen dialog transition', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Material(child: new Text('Page 1')),
)
);
final Point widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Material(child: new Text('Page 2'));
},
fullscreenDialog: true,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
Point widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Point widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 doesn't move.
expect(widget1TransientTopLeft == widget1InitialTopLeft, true);
// Fullscreen dialogs transitions vertically only.
expect(widget1InitialTopLeft.x == widget2TopLeft.x, true);
// Page 2 is coming in from the bottom.
expect(widget2TopLeft.y > widget1InitialTopLeft.y, true);
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 doesn't move.
expect(widget1TransientTopLeft == widget1InitialTopLeft, true);
// Fullscreen dialogs transitions vertically only.
expect(widget1InitialTopLeft.x == widget2TopLeft.x, true);
// Page 2 is leaving towards the bottom.
expect(widget2TopLeft.y > widget1InitialTopLeft.y, true);
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
// Page 1 is back where it started.
expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
});
testWidgets('test no back gesture on Android', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(body: new Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return new Scaffold(body: new Text('Page 2'));
},
},
)
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from left edge to invoke the gesture.
final TestGesture gesture = await tester.startGesture(const Point(5.0, 100.0));
await gesture.moveBy(const Offset(400.0, 0.0));
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Page 2 didn't move
expect(tester.getTopLeft(find.text('Page 2')), Point.origin);
});
testWidgets('test back gesture on iOS', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Scaffold(body: new Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return new Scaffold(body: new Text('Page 2'));
},
},
)
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from left edge to invoke the gesture.
final TestGesture gesture = await tester.startGesture(const Point(5.0, 100.0));
await gesture.moveBy(const Offset(400.0, 0.0));
await tester.pumpAndSettle();
// Page 1 is now visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('test no back gesture on iOS fullscreen dialogs', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Scaffold(body: new Text('Page 1')),
)
);
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Scaffold(body: new Text('Page 2'));
},
fullscreenDialog: true,
));
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from left edge to invoke the gesture.
final TestGesture gesture = await tester.startGesture(const Point(5.0, 100.0));
await gesture.moveBy(const Offset(400.0, 0.0));
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Page 2 didn't move
expect(tester.getTopLeft(find.text('Page 2')), Point.origin);
}); });
} }
...@@ -128,126 +128,6 @@ void main() { ...@@ -128,126 +128,6 @@ void main() {
expect(Navigator.canPop(containerKey1.currentContext), isFalse); expect(Navigator.canPop(containerKey1.currentContext), isFalse);
}); });
testWidgets('Check back gesture works on iOS', (WidgetTester tester) async {
final GlobalKey containerKey1 = new GlobalKey();
final 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(seconds: 1));
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnstage);
// Drag from left edge to invoke the gesture.
final TestGesture gesture = await tester.startGesture(const Point(5.0, 100.0));
await gesture.moveBy(const Offset(50.0, 0.0));
await tester.pump();
// Home is now visible.
expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOnstage);
});
testWidgets('Check back gesture does nothing on android', (WidgetTester tester) async {
final GlobalKey containerKey1 = new GlobalKey();
final 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.android),
));
Navigator.pushNamed(containerKey1.currentContext, '/settings');
await tester.pump();
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.
final TestGesture gesture = await tester.startGesture(const Point(5.0, 100.0));
await gesture.moveBy(const Offset(50.0, 0.0));
await tester.pump();
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnstage);
});
testWidgets('Check page transition positioning on iOS', (WidgetTester tester) async {
final GlobalKey containerKey1 = new GlobalKey();
final 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 { testWidgets('Check back gesture disables Heroes', (WidgetTester tester) async {
final GlobalKey containerKey1 = new GlobalKey(); final GlobalKey containerKey1 = new GlobalKey();
final GlobalKey containerKey2 = new GlobalKey(); final GlobalKey containerKey2 = new GlobalKey();
......
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