Commit 36c3a962 authored by xster's avatar xster Committed by GitHub

Create a CupertinoPageRoute (#10686)

* started copying stuff into cupertino page route

* extracted from material page route. Ready for testing

* works with button and gesture

* tests and docs

* review notes

* review notes
parent 44126cd9
...@@ -18,3 +18,4 @@ export 'src/cupertino/scaffold.dart'; ...@@ -18,3 +18,4 @@ export 'src/cupertino/scaffold.dart';
export 'src/cupertino/slider.dart'; export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart'; export 'src/cupertino/switch.dart';
export 'src/cupertino/thumb_painter.dart'; export 'src/cupertino/thumb_painter.dart';
export 'widgets.dart';
...@@ -54,25 +54,34 @@ class _MountainViewPageTransition extends StatelessWidget { ...@@ -54,25 +54,34 @@ class _MountainViewPageTransition extends StatelessWidget {
/// ///
/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those /// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those
/// pages animate bottom->up rather than right->left. /// pages animate bottom->up rather than right->left.
///
/// See also:
///
/// * [CupertinoPageRoute], that this [PageRoute] delegates transition animations to for iOS.
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, bool fullscreenDialog: false,
}) : assert(builder != null), }) : assert(builder != null),
assert(opaque), assert(opaque),
super(settings: settings); super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route. /// Builds the primary contents of the route.
final WidgetBuilder builder; final WidgetBuilder builder;
/// Whether this route is a full-screen dialog. /// A delegate PageRoute to which iOS themed page operations are delegated to.
/// /// It's lazily created on first use.
/// Prevents [startPopGesture] from poping the route using an edge swipe on CupertinoPageRoute<T> _internalCupertinoPageRoute;
/// iOS. CupertinoPageRoute<T> get _cupertinoPageRoute {
final bool fullscreenDialog; _internalCupertinoPageRoute ??= new CupertinoPageRoute<T>(
builder: builder, // Not used.
fullscreenDialog: fullscreenDialog,
);
return _internalCupertinoPageRoute;
}
@override @override
final bool maintainState; final bool maintainState;
...@@ -85,23 +94,22 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -85,23 +94,22 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override @override
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) { bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
return nextRoute is MaterialPageRoute<dynamic>; return nextRoute is MaterialPageRoute<dynamic> || nextRoute is CupertinoPageRoute<dynamic>;
} }
@override @override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a fullscreen dialog. // Don't perform outgoing animation if the next route is a fullscreen dialog.
return nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog; return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog);
} }
@override @override
void dispose() { void dispose() {
_backGestureController?.dispose(); _internalCupertinoPageRoute?.dispose();
super.dispose(); super.dispose();
} }
CupertinoBackGestureController _backGestureController;
/// Support for dismissing this route with a horizontal swipe is enabled /// Support for dismissing this route with a horizontal swipe is enabled
/// for [TargetPlatform.iOS]. If attempts to dismiss this route might be /// for [TargetPlatform.iOS]. If attempts to dismiss this route might be
/// vetoed because a [WillPopCallback] was defined for the route then the /// vetoed because a [WillPopCallback] was defined for the route then the
...@@ -109,35 +117,14 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -109,35 +117,14 @@ class MaterialPageRoute<T> extends PageRoute<T> {
/// ///
/// See also: /// See also:
/// ///
/// * [CupertinoPageRoute] that backs the gesture for iOS.
/// * [hasScopedWillPopCallback], which is true if a `willPop` callback /// * [hasScopedWillPopCallback], which is true if a `willPop` callback
/// is defined for this route. /// is defined for this route.
@override @override
NavigationGestureController startPopGesture() { NavigationGestureController startPopGesture() {
// If attempts to dismiss this route might be vetoed, then do not return Theme.of(navigator.context).platform == TargetPlatform.iOS
// allow the user to dismiss the route with a swipe. ? _cupertinoPageRoute.startPopGestureForRoute(this)
if (hasScopedWillPopCallback) : null;
return null;
// Fullscreen dialogs aren't dismissable by back swipe.
if (fullscreenDialog)
return null;
if (controller.status != AnimationStatus.completed)
return null;
assert(_backGestureController == null);
_backGestureController = new CupertinoBackGestureController(
navigator: navigator,
controller: controller,
);
controller.addStatusListener(_handleBackGestureEnded);
return _backGestureController;
}
void _handleBackGestureEnded(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_backGestureController?.dispose();
_backGestureController = null;
controller.removeStatusListener(_handleBackGestureEnded);
}
} }
@override @override
...@@ -158,20 +145,7 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -158,20 +145,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
if (Theme.of(context).platform == TargetPlatform.iOS) { if (Theme.of(context).platform == TargetPlatform.iOS) {
if (fullscreenDialog) return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child);
return new CupertinoFullscreenDialogTransition(
animation: animation,
child: child,
);
else
return new CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
child: child,
// In the middle of a back gesture drag, let the transition be linear to match finger
// motions.
linearTransition: _backGestureController != null,
);
} else { } else {
return new _MountainViewPageTransition( return new _MountainViewPageTransition(
routeAnimation: animation, routeAnimation: animation,
......
...@@ -13,9 +13,18 @@ import 'routes.dart'; ...@@ -13,9 +13,18 @@ import 'routes.dart';
abstract class PageRoute<T> extends ModalRoute<T> { abstract class PageRoute<T> extends ModalRoute<T> {
/// Creates a modal route that replaces the entire screen. /// Creates a modal route that replaces the entire screen.
PageRoute({ PageRoute({
RouteSettings settings: const RouteSettings() RouteSettings settings: const RouteSettings(),
this.fullscreenDialog: false,
}) : super(settings: settings); }) : super(settings: settings);
/// Whether this page route is a full-screen dialog.
///
/// In Material and Cupertino, being fullscreen has the effects of making
/// the app bars have a close button instead of a back button. On
/// iOS, dialogs transitions animate differently and are also not closeable
/// with the back swipe gesture.
final bool fullscreenDialog;
@override @override
bool get opaque => true; bool get opaque => true;
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../services/mocks_for_image_cache.dart'; import '../services/mocks_for_image_cache.dart';
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
const TextStyle testStyle = const TextStyle( const TextStyle testStyle = const TextStyle(
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('test iOS page transition', (WidgetTester tester) async {
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? "1" : "2";
return new Center(child: new Text('Page $pageNumber'));
}
);
},
),
);
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is moving to the left.
expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is coming in from the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, 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.dx < widget1InitialTopLeft.dx, true);
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is leaving towards the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, 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 WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return const Center(child: const Text('Page 1'));
}
);
},
),
);
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>(
builder: (BuildContext context) {
return const Center(child: const Text('Page 2'));
},
fullscreenDialog: true,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 doesn't move.
expect(widget1TransientTopLeft == widget1InitialTopLeft, true);
// Fullscreen dialogs transitions vertically only.
expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true);
// Page 2 is coming in from the bottom.
expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, 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.dx == widget2TopLeft.dx, true);
// Page 2 is leaving towards the bottom.
expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, 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);
});
}
\ No newline at end of file
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart'; import '../rendering/rendering_tester.dart';
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
......
...@@ -38,7 +38,7 @@ void main() { ...@@ -38,7 +38,7 @@ void main() {
// Animation begins from the top of the page. // Animation begins from the top of the page.
expect(widget2TopLeft.dy < widget2Size.height, true); expect(widget2TopLeft.dy < widget2Size.height, true);
await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 300));
// Page 2 covers page 1. // Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
...@@ -53,7 +53,7 @@ void main() { ...@@ -53,7 +53,7 @@ void main() {
// Page 2 starts to move down. // Page 2 starts to move down.
expect(widget1TopLeft.dy < widget2TopLeft.dy, true); expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Page 1'), isOnstage); expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing); expect(find.text('Page 2'), findsNothing);
...@@ -302,4 +302,79 @@ void main() { ...@@ -302,4 +302,79 @@ void main() {
// Page 2 didn't move // Page 2 didn't move
expect(tester.getTopLeft(find.text('Page 2')), Offset.zero); expect(tester.getTopLeft(find.text('Page 2')), Offset.zero);
}); });
testWidgets('test adaptable transitions switch during execution', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: const Material(child: const Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return const Material(child: const Text('Page 2'));
},
},
)
);
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
final Size widget2Size = tester.getSize(find.text('Page 2'));
// Android transition is vertical only.
expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true);
// Page 1 is above page 2 mid-transition.
expect(widget1InitialTopLeft.dy < widget2TopLeft.dy, true);
// Animation begins from the top of the page.
expect(widget2TopLeft.dy < widget2Size.height, true);
await tester.pump(const Duration(milliseconds: 300));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Re-pump the same app but with iOS instead of Android.
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: const Material(child: const Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return const Material(child: const Text('Page 2'));
},
},
)
);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is coming back from the left.
expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is leaving towards the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true);
await tester.pump(const Duration(milliseconds: 300));
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);
});
} }
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