Commit 02c21447 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

CupertinoPageRoute cleanup (#11473)

parent 35496d00
...@@ -72,19 +72,28 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -72,19 +72,28 @@ 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;
@override
final bool maintainState;
/// A delegate PageRoute to which iOS themed page operations are delegated to. /// A delegate PageRoute to which iOS themed page operations are delegated to.
/// It's lazily created on first use. /// It's lazily created on first use.
CupertinoPageRoute<T> _internalCupertinoPageRoute;
CupertinoPageRoute<T> get _cupertinoPageRoute { CupertinoPageRoute<T> get _cupertinoPageRoute {
assert(_useCupertinoTransitions);
_internalCupertinoPageRoute ??= new CupertinoPageRoute<T>( _internalCupertinoPageRoute ??= new CupertinoPageRoute<T>(
builder: builder, // Not used. builder: builder, // Not used.
fullscreenDialog: fullscreenDialog, fullscreenDialog: fullscreenDialog,
hostRoute: this,
); );
return _internalCupertinoPageRoute; return _internalCupertinoPageRoute;
} }
CupertinoPageRoute<T> _internalCupertinoPageRoute;
@override /// Whether we should currently be using Cupertino transitions. This is true
final bool maintainState; /// if the theme says we're on iOS, or if we're in an active gesture.
bool get _useCupertinoTransitions {
return _internalCupertinoPageRoute?.popGestureInProgress == true
|| Theme.of(navigator.context).platform == TargetPlatform.iOS;
}
@override @override
Duration get transitionDuration => const Duration(milliseconds: 300); Duration get transitionDuration => const Duration(milliseconds: 300);
...@@ -93,8 +102,8 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -93,8 +102,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
Color get barrierColor => null; Color get barrierColor => null;
@override @override
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) { bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) {
return nextRoute is MaterialPageRoute<dynamic> || nextRoute is CupertinoPageRoute<dynamic>; return (previousRoute is MaterialPageRoute || previousRoute is CupertinoPageRoute);
} }
@override @override
...@@ -110,23 +119,6 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -110,23 +119,6 @@ class MaterialPageRoute<T> extends PageRoute<T> {
super.dispose(); super.dispose();
} }
/// 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
/// platform-specific back gesture is disabled.
///
/// 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() {
return Theme.of(navigator.context).platform == TargetPlatform.iOS
? _cupertinoPageRoute.startPopGestureForRoute(this)
: null;
}
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget result = builder(context); final Widget result = builder(context);
...@@ -144,12 +136,12 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -144,12 +136,12 @@ 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 (_useCupertinoTransitions) {
return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child); return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child);
} else { } else {
return new _MountainViewPageTransition( return new _MountainViewPageTransition(
routeAnimation: animation, routeAnimation: animation,
child: child child: child,
); );
} }
} }
......
...@@ -23,8 +23,6 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de ...@@ -23,8 +23,6 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200); const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0); final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0);
const double _kBackGestureWidth = 20.0;
enum _ScaffoldSlot { enum _ScaffoldSlot {
body, body,
appBar, appBar,
...@@ -717,40 +715,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -717,40 +715,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
} }
final GlobalKey _backGestureKey = new GlobalKey();
NavigationGestureController _backGestureController;
bool _shouldHandleBackGesture() {
assert(mounted);
return Theme.of(context).platform == TargetPlatform.iOS && Navigator.canPop(context);
}
void _handleDragStart(DragStartDetails details) {
assert(mounted);
_backGestureController = Navigator.of(context).startPopGesture();
}
void _handleDragUpdate(DragUpdateDetails details) {
assert(mounted);
_backGestureController?.dragUpdate(details.primaryDelta / context.size.width);
}
void _handleDragEnd(DragEndDetails details) {
assert(mounted);
final bool willPop = _backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / context.size.width) ?? false;
if (willPop)
_currentBottomSheet?.close();
_backGestureController = null;
}
void _handleDragCancel() {
assert(mounted);
final bool willPop = _backGestureController?.dragEnd(0.0) ?? false;
if (willPop)
_currentBottomSheet?.close();
_backGestureController = null;
}
// INTERNALS // INTERNALS
...@@ -887,25 +851,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -887,25 +851,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
child: widget.drawer, child: widget.drawer,
) )
)); ));
} else if (_shouldHandleBackGesture()) {
assert(!hasDrawer);
// Add a gesture for navigating back.
children.add(new LayoutId(
id: _ScaffoldSlot.drawer,
child: new Align(
alignment: FractionalOffset.centerLeft,
child: new GestureDetector(
key: _backGestureKey,
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
onHorizontalDragCancel: _handleDragCancel,
behavior: HitTestBehavior.translucent,
excludeFromSemantics: true,
child: new Container(width: _kBackGestureWidth)
)
)
));
} }
return new _ScaffoldScope( return new _ScaffoldScope(
......
...@@ -3113,6 +3113,8 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3113,6 +3113,8 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
'resulting object.\n' 'resulting object.\n'
'The size getter was called for the following element:\n' 'The size getter was called for the following element:\n'
' $this\n' ' $this\n'
'The associated render sliver was:\n'
' ${renderObject.toStringShallow("\n ")}'
); );
} }
if (renderObject is! RenderBox) { if (renderObject is! RenderBox) {
...@@ -3124,10 +3126,12 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3124,10 +3126,12 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
'and extracting its size manually.\n' 'and extracting its size manually.\n'
'The size getter was called for the following element:\n' 'The size getter was called for the following element:\n'
' $this\n' ' $this\n'
'The associated render object was:\n'
' ${renderObject.toStringShallow("\n ")}'
); );
} }
final RenderBox box = renderObject; final RenderBox box = renderObject;
if (!box.hasSize || box.debugNeedsLayout) { if (!box.hasSize) {
throw new FlutterError( throw new FlutterError(
'Cannot get size from a render object that has not been through layout.\n' 'Cannot get size from a render object that has not been through layout.\n'
'The size of this render object has not yet been determined because ' 'The size of this render object has not yet been determined because '
...@@ -3137,6 +3141,24 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3137,6 +3141,24 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
'the size and position of the render objects during layout.\n' 'the size and position of the render objects during layout.\n'
'The size getter was called for the following element:\n' 'The size getter was called for the following element:\n'
' $this\n' ' $this\n'
'The render object from which the size was to be obtained was:\n'
' ${box.toStringShallow("\n ")}'
);
}
if (box.debugNeedsLayout) {
throw new FlutterError(
'Cannot get size from a render object that has been marked dirty for layout.\n'
'The size of this render object is ambiguous because this render object has '
'been modified since it was last laid out, which typically means that the size '
'getter was called too early in the pipeline (e.g., during the build phase) '
'before the framework has determined the size and position of the render '
'objects during layout.\n'
'The size getter was called for the following element:\n'
' $this\n'
'The render object from which the size was to be obtained was:\n'
' ${box.toStringShallow("\n ")}\n'
'Consider using debugPrintMarkNeedsLayoutStacks to determine why the render '
'object in question is dirty, if you did not expect this.'
); );
} }
return true; return true;
......
...@@ -178,29 +178,9 @@ abstract class Route<T> { ...@@ -178,29 +178,9 @@ abstract class Route<T> {
@mustCallSuper @mustCallSuper
@protected @protected
void dispose() { void dispose() {
assert(() {
if (navigator == null) {
throw new FlutterError(
'$runtimeType.dipose() called more than once.\n'
'A given route cannot be disposed more than once.'
);
}
return true;
});
_navigator = null; _navigator = null;
} }
/// If the route's transition can be popped via a user gesture (e.g. the iOS
/// back gesture), this should return a controller object that can be used to
/// control the transition animation's progress. Otherwise, it should return
/// null.
///
/// If attempts to dismiss this route might be vetoed, for example because
/// a [WillPopCallback] was defined for the route, then it may make sense
/// to disable the pop gesture. For example, the iOS back gesture is disabled
/// when [ModalRoute.hasScopedWillPopCallback] is true.
NavigationGestureController startPopGesture() => null;
/// Whether this route is the top-most route on the navigator. /// Whether this route is the top-most route on the navigator.
/// ///
/// If this is true, then [isActive] is also true. /// If this is true, then [isActive] is also true.
...@@ -288,54 +268,16 @@ class NavigatorObserver { ...@@ -288,54 +268,16 @@ class NavigatorObserver {
/// The [Navigator] removed `route`. /// The [Navigator] removed `route`.
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { } void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] is being controlled by a user gesture. /// The [Navigator]'s routes are being moved by a user gesture.
/// ///
/// Used for the iOS back gesture. /// For example, this is called when an iOS back gesture starts, and is used
/// to disabled hero animations during such interactions.
void didStartUserGesture() { } void didStartUserGesture() { }
/// User gesture is no longer controlling the [Navigator]. /// User gesture is no longer controlling the [Navigator].
void didStopUserGesture() { }
}
/// Interface describing an object returned by the [Route.startPopGesture]
/// method, allowing the route's transition animations to be controlled by a
/// drag or other user gesture.
abstract class NavigationGestureController {
/// Configures the NavigationGestureController and tells the given [Navigator] that
/// a gesture has started.
NavigationGestureController(this._navigator)
: assert(_navigator != null) {
// Disable Hero transitions until the gesture is complete.
_navigator.didStartUserGesture();
}
/// The navigator that this object is controlling.
@protected
NavigatorState get navigator => _navigator;
NavigatorState _navigator;
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
///
/// Must be called when the gesture is done.
/// ///
/// Calling this method notifies the navigator that the gesture has completed. /// Paired with an earlier call to [didStartUserGesture].
@mustCallSuper void didStopUserGesture() { }
void dispose() {
_navigator.didStopUserGesture();
_navigator = null;
}
/// The drag gesture has changed by [fractionalDelta]. The total range of the
/// drag should be 0.0 to 1.0.
void dragUpdate(double fractionalDelta);
/// The drag gesture has ended with a horizontal motion of
/// [fractionalVelocity] as a fraction of screen width per second.
///
/// Returns true if the gesture will complete (i.e. a back gesture will
/// result in a pop).
bool dragEnd(double fractionalVelocity);
} }
/// Signature for the [Navigator.popUntil] predicate argument. /// Signature for the [Navigator.popUntil] predicate argument.
...@@ -1326,33 +1268,35 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1326,33 +1268,35 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return _history.length > 1 || _history[0].willHandlePopInternally; return _history.length > 1 || _history[0].willHandlePopInternally;
} }
/// Starts a gesture that results in popping the navigator. /// Whether a route is currently being manipulated by the user, e.g.
NavigationGestureController startPopGesture() { /// as during an iOS back gesture.
if (canPop()) bool get userGestureInProgress => _userGesturesInProgress > 0;
return _history.last.startPopGesture(); int _userGesturesInProgress = 0;
return null;
}
/// Whether a gesture controlled by a [NavigationGestureController] is currently in progress.
bool get userGestureInProgress => _userGestureInProgress;
// TODO(mpcomplete): remove this bool when we fix
// https://github.com/flutter/flutter/issues/5577
bool _userGestureInProgress = false;
/// The navigator is being controlled by a user gesture. /// The navigator is being controlled by a user gesture.
/// ///
/// Used for the iOS back gesture. /// For example, called when the user beings an iOS back gesture.
///
/// When the gesture finishes, call [didStopUserGesture].
void didStartUserGesture() { void didStartUserGesture() {
_userGestureInProgress = true; _userGesturesInProgress += 1;
for (NavigatorObserver observer in widget.observers) if (_userGesturesInProgress == 1) {
observer.didStartUserGesture(); for (NavigatorObserver observer in widget.observers)
observer.didStartUserGesture();
}
} }
/// A user gesture is no longer controlling the navigator. /// A user gesture completed.
///
/// Notifies the navigator that a gesture regarding which the navigator was
/// previously notified with [didStartUserGesture] has completed.
void didStopUserGesture() { void didStopUserGesture() {
_userGestureInProgress = false; assert(_userGesturesInProgress > 0);
for (NavigatorObserver observer in widget.observers) _userGesturesInProgress -= 1;
observer.didStopUserGesture(); if (_userGesturesInProgress == 0) {
for (NavigatorObserver observer in widget.observers)
observer.didStopUserGesture();
}
} }
final Set<int> _activePointers = new Set<int>(); final Set<int> _activePointers = new Set<int>();
......
...@@ -32,10 +32,10 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -32,10 +32,10 @@ abstract class PageRoute<T> extends ModalRoute<T> {
bool get barrierDismissible => false; bool get barrierDismissible => false;
@override @override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => nextRoute is PageRoute<dynamic>; bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => nextRoute is PageRoute;
@override @override
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) => nextRoute is PageRoute<dynamic>; bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => previousRoute is PageRoute;
@override @override
AnimationController createAnimationController() { AnimationController createAnimationController() {
......
...@@ -272,6 +272,73 @@ void main() { ...@@ -272,6 +272,73 @@ void main() {
expect(tester.getTopLeft(find.text('Page 2')), const Offset(100.0, 0.0)); expect(tester.getTopLeft(find.text('Page 2')), const Offset(100.0, 0.0));
}); });
testWidgets('back gesture while OS changes', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new Material(
child: new FlatButton(
child: new Text('PUSH'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
),
'/b': (BuildContext context) => new Container(child: new Text('HELLO')),
};
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
routes: routes,
),
);
await tester.tap(find.text('PUSH'));
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(find.text('PUSH'), findsNothing);
expect(find.text('HELLO'), findsOneWidget);
final Offset helloPosition1 = tester.getCenter(find.text('HELLO'));
final TestGesture gesture = await tester.startGesture(const Offset(2.5, 300.0));
await tester.pump(const Duration(milliseconds: 20));
await gesture.moveBy(const Offset(100.0, 0.0));
expect(find.text('PUSH'), findsNothing);
expect(find.text('HELLO'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsOneWidget);
final Offset helloPosition2 = tester.getCenter(find.text('HELLO'));
expect(helloPosition1.dx, lessThan(helloPosition2.dx));
expect(helloPosition1.dy, helloPosition2.dy);
expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.iOS);
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
routes: routes,
),
);
// Now we have to let the theme animation run through.
// This takes three frames (including the first one above):
// 1. Start the Theme animation. It's at t=0 so everything else is identical.
// 2. Start any animations that are informed by the Theme, for example, the
// DefaultTextStyle, on the first frame that the theme is not at t=0. In
// this case, it's at t=1.0 of the theme animation, so this is also the
// frame in which the theme animation ends.
// 3. End all the other animations.
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.android);
final Offset helloPosition3 = tester.getCenter(find.text('HELLO'));
expect(helloPosition3, helloPosition2);
expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsOneWidget);
await gesture.moveBy(const Offset(100.0, 0.0));
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsOneWidget);
final Offset helloPosition4 = tester.getCenter(find.text('HELLO'));
expect(helloPosition3.dx, lessThan(helloPosition4.dx));
expect(helloPosition3.dy, helloPosition4.dy);
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsNothing);
});
testWidgets('test no back gesture on iOS fullscreen dialogs', (WidgetTester tester) async { testWidgets('test no back gesture on iOS fullscreen dialogs', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
......
...@@ -268,24 +268,39 @@ void main() { ...@@ -268,24 +268,39 @@ void main() {
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Sheet'), isOnstage); expect(find.text('Sheet'), isOnstage);
// Drag from left edge to invoke the gesture. We should go back.
TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0));
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
Navigator.pushNamed(containerKey1.currentContext, '/sheet');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing);
expect(find.text('Sheet'), isOnstage);
// Show the bottom sheet. // Show the bottom sheet.
final PersistentBottomSheetTestState sheet = containerKey2.currentState; final PersistentBottomSheetTestState sheet = containerKey2.currentState;
sheet.showBottomSheet(); sheet.showBottomSheet();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
// Drag from left edge to invoke the gesture. // Drag from left edge to invoke the gesture. Nothing should happen.
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0)); gesture = await tester.startGesture(const Offset(5.0, 100.0));
await gesture.moveBy(const Offset(500.0, 0.0)); await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), isOnstage); expect(find.text('Home'), findsNothing);
expect(find.text('Sheet'), findsNothing); expect(find.text('Sheet'), isOnstage);
// Sheet called setState and didn't crash. // Sheet did not call setState (since the gesture did nothing).
expect(sheet.setStateCalled, isTrue); expect(sheet.setStateCalled, isFalse);
}); });
testWidgets('Test completed future', (WidgetTester tester) async { testWidgets('Test completed future', (WidgetTester tester) async {
......
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