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';
...@@ -46,95 +46,150 @@ final DecorationTween _kGradientShadowTween = new DecorationTween( ...@@ -46,95 +46,150 @@ final DecorationTween _kGradientShadowTween = new DecorationTween(
), ),
); );
/// A custom [Decoration] used to paint an extra shadow on the left edge of the /// A modal route that replaces the entire screen with an iOS transition.
/// box it's decorating. It's like a [BoxDecoration] with only a gradient except ///
/// it paints to the left of the box instead of behind the box. /// The page slides in from the right and exits in reverse.
class _CupertinoEdgeShadowDecoration extends Decoration { /// The page also shifts to the left in parallax when another page enters to cover it.
const _CupertinoEdgeShadowDecoration({ this.edgeGradient }); ///
/// The page slides in from the bottom and exits in reverse with no parallax effect
/// A Decoration with no decorating properties. /// for fullscreen dialogs.
static const _CupertinoEdgeShadowDecoration none = ///
const _CupertinoEdgeShadowDecoration(); /// 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.
///
/// See also:
///
/// * [MaterialPageRoute] for an adaptive [PageRoute] that uses a platform appropriate transition.
class CupertinoPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in an iOS designed app.
CupertinoPageRoute({
@required this.builder,
RouteSettings settings: const RouteSettings(),
this.maintainState: true,
bool fullscreenDialog: false,
}) : assert(builder != null),
assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route.
final WidgetBuilder builder;
/// A gradient to draw to the left of the box being decorated. @override
/// FractionalOffsets are relative to the original box translated one box final bool maintainState;
/// width to the left.
final LinearGradient edgeGradient;
/// Linearly interpolate between two edge shadow decorations decorations. @override
/// Duration get transitionDuration => const Duration(milliseconds: 350);
/// See also [Decoration.lerp].
static _CupertinoEdgeShadowDecoration lerp(
_CupertinoEdgeShadowDecoration a,
_CupertinoEdgeShadowDecoration b,
double t
) {
if (a == null && b == null)
return null;
return new _CupertinoEdgeShadowDecoration(
edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t),
);
}
@override @override
_CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { Color get barrierColor => null;
if (a is! _CupertinoEdgeShadowDecoration)
return _CupertinoEdgeShadowDecoration.lerp(null, this, t);
return _CupertinoEdgeShadowDecoration.lerp(a, this, t);
}
@override @override
_CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
if (b is! _CupertinoEdgeShadowDecoration) return nextRoute is CupertinoPageRoute<dynamic>;
return _CupertinoEdgeShadowDecoration.lerp(this, null, t);
return _CupertinoEdgeShadowDecoration.lerp(this, b, t);
} }
@override @override
_CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) { bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
return new _CupertinoEdgeShadowPainter(this, onChanged); // Don't perform outgoing animation if the next route is a fullscreen dialog.
return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog;
} }
@override @override
bool operator ==(dynamic other) { void dispose() {
if (identical(this, other)) _backGestureController?.dispose();
return true; // If the route is never installed (i.e. pushed into a Navigator) such as the
if (other.runtimeType != _CupertinoEdgeShadowDecoration) // case when [MaterialPageRoute] delegates transition building to [CupertinoPageRoute],
return false; // don't dispose super.
final _CupertinoEdgeShadowDecoration typedOther = other; if (overlayEntries.isNotEmpty)
return edgeGradient == typedOther.edgeGradient; super.dispose();
} }
CupertinoBackGestureController _backGestureController;
/// Support for dismissing this route with a horizontal swipe.
///
/// Swiping will be disabled if the page is a fullscreen dialog or if
/// dismissals can be overriden because a [WillPopCallback] was
/// defined for the route.
///
/// See also:
///
/// * [hasScopedWillPopCallback], which is true if a `willPop` callback
/// is defined for this route.
@override @override
int get hashCode { NavigationGestureController startPopGesture() {
return edgeGradient.hashCode; return startPopGestureForRoute(this);
} }
}
/// A [BoxPainter] used to draw the page transition shadow using gradients. /// Create a CupertinoBackGestureController using a specific PageRoute.
class _CupertinoEdgeShadowPainter extends BoxPainter { ///
_CupertinoEdgeShadowPainter( /// Used when [MaterialPageRoute] delegates the back gesture to [CupertinoPageRoute]
this._decoration, /// since the [CupertinoPageRoute] is not actually inserted into the Navigator.
VoidCallback onChange NavigationGestureController startPopGestureForRoute(PageRoute<T> hostRoute) {
) : assert(_decoration != null), // If attempts to dismiss this route might be vetoed such as in a page
super(onChange); // with forms, then do not allow the user to dismiss the route with a swipe.
if (hostRoute.hasScopedWillPopCallback)
return null;
// Fullscreen dialogs aren't dismissable by back swipe.
if (fullscreenDialog)
return null;
if (hostRoute.controller.status != AnimationStatus.completed)
return null;
assert(_backGestureController == null);
_backGestureController = new CupertinoBackGestureController(
navigator: hostRoute.navigator,
controller: hostRoute.controller,
);
final _CupertinoEdgeShadowDecoration _decoration; Function handleBackGestureEnded;
handleBackGestureEnded = (AnimationStatus status) {
if (status == AnimationStatus.completed) {
_backGestureController?.dispose();
_backGestureController = null;
hostRoute.controller.removeStatusListener(handleBackGestureEnded);
}
};
hostRoute.controller.addStatusListener(handleBackGestureEnded);
return _backGestureController;
}
@override @override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final LinearGradient gradient = _decoration.edgeGradient; final Widget result = builder(context);
if (gradient == null) assert(() {
return; if (result == null) {
// The drawable space for the gradient is a rect with the same size as throw new FlutterError(
// its parent box one box width to the left of the box. 'The builder for route "${settings.name}" returned null.\n'
final Rect rect = 'Route builders must never return null.'
(offset & configuration.size).translate(-configuration.size.width, 0.0); );
final Paint paint = new Paint() }
..shader = gradient.createShader(rect); return true;
});
return result;
}
canvas.drawRect(rect, paint); @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
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,
);
} }
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
} }
/// Provides an iOS-style page transition animation. /// Provides an iOS-style page transition animation.
...@@ -303,3 +358,94 @@ class CupertinoBackGestureController extends NavigationGestureController { ...@@ -303,3 +358,94 @@ class CupertinoBackGestureController extends NavigationGestureController {
navigator.pop(); navigator.pop();
} }
} }
/// A custom [Decoration] used to paint an extra shadow on the left edge of the
/// box it's decorating. It's like a [BoxDecoration] with only a gradient except
/// it paints to the left of the box instead of behind the box.
class _CupertinoEdgeShadowDecoration extends Decoration {
const _CupertinoEdgeShadowDecoration({ this.edgeGradient });
/// A Decoration with no decorating properties.
static const _CupertinoEdgeShadowDecoration none =
const _CupertinoEdgeShadowDecoration();
/// A gradient to draw to the left of the box being decorated.
/// FractionalOffsets are relative to the original box translated one box
/// width to the left.
final LinearGradient edgeGradient;
/// Linearly interpolate between two edge shadow decorations decorations.
///
/// See also [Decoration.lerp].
static _CupertinoEdgeShadowDecoration lerp(
_CupertinoEdgeShadowDecoration a,
_CupertinoEdgeShadowDecoration b,
double t
) {
if (a == null && b == null)
return null;
return new _CupertinoEdgeShadowDecoration(
edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t),
);
}
@override
_CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) {
if (a is! _CupertinoEdgeShadowDecoration)
return _CupertinoEdgeShadowDecoration.lerp(null, this, t);
return _CupertinoEdgeShadowDecoration.lerp(a, this, t);
}
@override
_CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) {
if (b is! _CupertinoEdgeShadowDecoration)
return _CupertinoEdgeShadowDecoration.lerp(this, null, t);
return _CupertinoEdgeShadowDecoration.lerp(this, b, t);
}
@override
_CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) {
return new _CupertinoEdgeShadowPainter(this, onChanged);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != _CupertinoEdgeShadowDecoration)
return false;
final _CupertinoEdgeShadowDecoration typedOther = other;
return edgeGradient == typedOther.edgeGradient;
}
@override
int get hashCode {
return edgeGradient.hashCode;
}
}
/// A [BoxPainter] used to draw the page transition shadow using gradients.
class _CupertinoEdgeShadowPainter extends BoxPainter {
_CupertinoEdgeShadowPainter(
this._decoration,
VoidCallback onChange
) : assert(_decoration != null),
super(onChange);
final _CupertinoEdgeShadowDecoration _decoration;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final LinearGradient gradient = _decoration.edgeGradient;
if (gradient == null)
return;
// The drawable space for the gradient is a rect with the same size as
// its parent box one box width to the left of the box.
final Rect rect =
(offset & configuration.size).translate(-configuration.size.width, 0.0);
final Paint paint = new Paint()
..shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
}
}
...@@ -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