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';
export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart';
export 'src/cupertino/thumb_painter.dart';
export 'widgets.dart';
......@@ -54,25 +54,34 @@ class _MountainViewPageTransition extends StatelessWidget {
///
/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those
/// 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> {
/// Creates a page route for use in a material design app.
MaterialPageRoute({
@required this.builder,
RouteSettings settings: const RouteSettings(),
this.maintainState: true,
this.fullscreenDialog: false,
bool fullscreenDialog: false,
}) : assert(builder != null),
assert(opaque),
super(settings: settings);
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route.
final WidgetBuilder builder;
/// Whether this route is a full-screen dialog.
///
/// Prevents [startPopGesture] from poping the route using an edge swipe on
/// iOS.
final bool fullscreenDialog;
/// A delegate PageRoute to which iOS themed page operations are delegated to.
/// It's lazily created on first use.
CupertinoPageRoute<T> _internalCupertinoPageRoute;
CupertinoPageRoute<T> get _cupertinoPageRoute {
_internalCupertinoPageRoute ??= new CupertinoPageRoute<T>(
builder: builder, // Not used.
fullscreenDialog: fullscreenDialog,
);
return _internalCupertinoPageRoute;
}
@override
final bool maintainState;
......@@ -85,23 +94,22 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
return nextRoute is MaterialPageRoute<dynamic>;
return nextRoute is MaterialPageRoute<dynamic> || nextRoute is CupertinoPageRoute<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;
return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog);
}
@override
void dispose() {
_backGestureController?.dispose();
_internalCupertinoPageRoute?.dispose();
super.dispose();
}
CupertinoBackGestureController _backGestureController;
/// Support for dismissing this route with a horizontal swipe is enabled
/// for [TargetPlatform.iOS]. If attempts to dismiss this route might be
/// vetoed because a [WillPopCallback] was defined for the route then the
......@@ -109,35 +117,14 @@ class MaterialPageRoute<T> extends PageRoute<T> {
///
/// See also:
///
/// * [CupertinoPageRoute] that backs the gesture for iOS.
/// * [hasScopedWillPopCallback], which is true if a `willPop` callback
/// is defined for this route.
@override
NavigationGestureController startPopGesture() {
// If attempts to dismiss this route might be vetoed, then do not
// allow the user to dismiss the route with a swipe.
if (hasScopedWillPopCallback)
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);
}
return Theme.of(navigator.context).platform == TargetPlatform.iOS
? _cupertinoPageRoute.startPopGestureForRoute(this)
: null;
}
@override
......@@ -158,20 +145,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
if (Theme.of(context).platform == TargetPlatform.iOS) {
if (fullscreenDialog)
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,
);
return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child);
} else {
return new _MountainViewPageTransition(
routeAnimation: animation,
......
......@@ -13,9 +13,18 @@ import 'routes.dart';
abstract class PageRoute<T> extends ModalRoute<T> {
/// Creates a modal route that replaces the entire screen.
PageRoute({
RouteSettings settings: const RouteSettings()
RouteSettings settings: const RouteSettings(),
this.fullscreenDialog: false,
}) : 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
bool get opaque => true;
......
......@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../services/mocks_for_image_cache.dart';
......
......@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
const TextStyle testStyle = const TextStyle(
......
......@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
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 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......
......@@ -38,7 +38,7 @@ void main() {
// Animation begins from the top of the page.
expect(widget2TopLeft.dy < widget2Size.height, true);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 300));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
......@@ -53,7 +53,7 @@ void main() {
// Page 2 starts to move down.
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 2'), findsNothing);
......@@ -302,4 +302,79 @@ void main() {
// Page 2 didn't move
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