Commit c7f98efb authored by xster's avatar xster Committed by GitHub

Extract cupertino page transition out of material (#9059)

* 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

* Some review notes

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

* Reviews

* Add background color

* final controller

* Review notes
parent 9095d762
......@@ -10,6 +10,7 @@ library cupertino;
export 'src/cupertino/activity_indicator.dart';
export 'src/cupertino/button.dart';
export 'src/cupertino/dialog.dart';
export 'src/cupertino/page.dart';
export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart';
export 'src/cupertino/thumb_painter.dart';
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
const double _kMinFlingVelocity = 1.0; // screen width per second.
const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background color.
/// Provides the native iOS page transition animation.
///
/// Takes in a page widget and a route animation from a [TransitionRoute] and produces an
/// AnimatedWidget wrapping that animates the page transition.
///
/// The page slides in from the right and exits in reverse. It also shifts to the left in
/// a parallax motion when another page enters to cover it.
class CupertinoPageTransition extends AnimatedWidget {
CupertinoPageTransition({
Key key,
@required Animation<double> animation,
@required this.child,
}) : super(
key: key,
listenable: _kTween.animate(new CurvedAnimation(
parent: animation,
curve: new _CupertinoTransitionCurve(null),
),
));
static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
begin: FractionalOffset.topRight,
end: -FractionalOffset.topRight,
);
final Widget child;
@override
Widget build(BuildContext context) {
// TODO(ianh): tell the transform to be un-transformed for hit testing
// but not while being controlled by a gesture.
return new SlideTransition(
position: listenable,
child: new PhysicalModel(
shape: BoxShape.rectangle,
color: _kBackgroundColor,
elevation: 16,
child: child,
)
);
}
}
// Custom curve for iOS page transitions.
class _CupertinoTransitionCurve extends Curve {
_CupertinoTransitionCurve(this.curve);
final Curve curve;
@override
double transform(double t) {
// The input [t] is the average of the current and next route's animation.
// This means t=0.5 represents when the route is fully onscreen. At
// t > 0.5, it is partially offscreen to the left (which happens when there
// 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;
}
}
/// This class responds to drag gestures to control the route's transition
/// animation progress. Used for iOS back gesture.
class CupertinoBackGestureController extends NavigationGestureController {
CupertinoBackGestureController({
@required NavigatorState navigator,
@required this.controller,
}) : super(navigator) {
assert(controller != null);
}
final AnimationController controller;
@override
void dispose() {
controller.removeStatusListener(handleStatusChanged);
super.dispose();
}
@override
void dragUpdate(double delta) {
// This assert can be triggered the Scaffold is reparented out of the route
// associated with this gesture controller and continues to feed it events.
// TODO(abarth): Change the ownership of the gesture controller so that the
// object feeding it these events (e.g., the Scaffold) is responsible for
// calling dispose on it as well.
assert(controller != null);
controller.value -= delta;
}
@override
bool dragEnd(double velocity) {
// This assert can be triggered the Scaffold is reparented out of the route
// associated with this gesture controller and continues to feed it events.
// TODO(abarth): Change the ownership of the gesture controller so that the
// object feeding it these events (e.g., the Scaffold) is responsible for
// calling dispose on it as well.
assert(controller != null);
if (velocity.abs() >= _kMinFlingVelocity) {
controller.fling(velocity: -velocity);
} else if (controller.value <= 0.5) {
controller.fling(velocity: -1.0);
} else {
controller.fling(velocity: 1.0);
}
// Don't end the gesture until the transition completes.
final AnimationStatus status = controller.status;
handleStatusChanged(status);
controller?.addStatusListener(handleStatusChanged);
return (status == AnimationStatus.reverse || status == AnimationStatus.dismissed);
}
void handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed)
navigator.pop();
}
}
......@@ -2,14 +2,12 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'material.dart';
import 'theme.dart';
const double _kMinFlingVelocity = 1.0; // screen width per second
// Used for Android and Fuchsia.
class _MountainViewPageTransition extends AnimatedWidget {
_MountainViewPageTransition({
......@@ -48,147 +46,15 @@ class _MountainViewPageTransition extends AnimatedWidget {
}
}
// Used for iOS.
class _CupertinoPageTransition extends AnimatedWidget {
static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
begin: FractionalOffset.topRight,
end: -FractionalOffset.topRight
);
_CupertinoPageTransition({
Key key,
Animation<double> animation,
this.child
}) : super(
key: key,
listenable: _kTween.animate(new CurvedAnimation(
parent: animation,
curve: new _CupertinoTransitionCurve(null)
)
));
final Widget child;
@override
Widget build(BuildContext context) {
// TODO(ianh): tell the transform to be un-transformed for hit testing
// but not while being controlled by a gesture.
return new SlideTransition(
position: listenable,
child: new Material(
elevation: 6,
child: child
)
);
}
}
// Custom curve for iOS page transitions.
class _CupertinoTransitionCurve extends Curve {
_CupertinoTransitionCurve(this.curve);
Curve curve;
@override
double transform(double t) {
// The input [t] is the average of the current and next route's animation.
// This means t=0.5 represents when the route is fully onscreen. At
// t > 0.5, it is partially offscreen to the left (which happens when there
// 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;
}
}
// This class responds to drag gestures to control the route's transition
// animation progress. Used for iOS back gesture.
class _CupertinoBackGestureController extends NavigationGestureController {
_CupertinoBackGestureController({
@required NavigatorState navigator,
@required this.controller,
@required this.onDisposed,
}) : super(navigator) {
assert(controller != null);
assert(onDisposed != null);
}
AnimationController controller;
final VoidCallback onDisposed;
@override
void dispose() {
controller.removeStatusListener(handleStatusChanged);
controller = null;
onDisposed();
super.dispose();
}
@override
void dragUpdate(double delta) {
// This assert can be triggered the Scaffold is reparented out of the route
// associated with this gesture controller and continues to feed it events.
// TODO(abarth): Change the ownership of the gesture controller so that the
// object feeding it these events (e.g., the Scaffold) is responsible for
// calling dispose on it as well.
assert(controller != null);
controller.value -= delta;
}
@override
bool dragEnd(double velocity) {
// This assert can be triggered the Scaffold is reparented out of the route
// associated with this gesture controller and continues to feed it events.
// TODO(abarth): Change the ownership of the gesture controller so that the
// object feeding it these events (e.g., the Scaffold) is responsible for
// calling dispose on it as well.
assert(controller != null);
if (velocity.abs() >= _kMinFlingVelocity) {
controller.fling(velocity: -velocity);
} else if (controller.value <= 0.5) {
controller.fling(velocity: -1.0);
} else {
controller.fling(velocity: 1.0);
}
// Don't end the gesture until the transition completes.
final AnimationStatus status = controller.status;
handleStatusChanged(status);
controller?.addStatusListener(handleStatusChanged);
return (status == AnimationStatus.reverse || status == AnimationStatus.dismissed);
}
void handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
navigator.pop();
assert(controller == null);
} else if (status == AnimationStatus.completed) {
dispose();
assert(controller == null);
}
}
}
/// A modal route that replaces the entire screen with a material design transition.
///
/// The entrance transition for the page slides the page upwards and fades it
/// For Android, the entrance transition for the page slides the page upwards and fades it
/// in. The exit transition is the same, but in reverse.
///
/// The transition is adaptive to the platform and on iOS, the page slides in from the right and
/// exits in reverse. The page also shifts to the left in parallax when another page enters to
/// cover it.
///
/// 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.
......@@ -226,7 +92,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
super.dispose();
}
_CupertinoBackGestureController _backGestureController;
CupertinoBackGestureController _backGestureController;
/// Support for dismissing this route with a horizontal swipe is enabled
/// for [TargetPlatform.iOS]. If attempts to dismiss this route might be
......@@ -246,14 +112,23 @@ class MaterialPageRoute<T> extends PageRoute<T> {
if (controller.status != AnimationStatus.completed)
return null;
assert(_backGestureController == null);
_backGestureController = new _CupertinoBackGestureController(
_backGestureController = new CupertinoBackGestureController(
navigator: navigator,
controller: controller,
onDisposed: () { _backGestureController = null; }
);
controller.addStatusListener(_handleBackGestureEnded);
return _backGestureController;
}
void _handleBackGestureEnded(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_backGestureController?.dispose();
_backGestureController = null;
controller.removeStatusListener(_handleBackGestureEnded);
}
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
final Widget result = builder(context);
......@@ -273,7 +148,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
if (Theme.of(context).platform == TargetPlatform.iOS &&
Navigator.of(context).userGestureInProgress) {
return new _CupertinoPageTransition(
return new CupertinoPageTransition(
animation: new AnimationMean(left: animation, right: forwardAnimation),
child: child
);
......
......@@ -38,4 +38,31 @@ void main() {
// Animation starts with page 2 being near transparent.
expect(widget2Opacity.opacity < 0.01, true);
});
testWidgets('test iOS page transition', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Material(child: new Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return new Material(child: new Text('Page 2'));
},
}
)
);
final Point widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 250));
final Point widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// This is currently an incorrect behaviour and we want right to left transition instead.
// See https://github.com/flutter/flutter/issues/8726.
expect(widget1TopLeft.x == widget2TopLeft.x, true);
expect(widget1TopLeft.y - widget2TopLeft.y < 0, 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