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
This commit is contained in:
parent
9095d76299
commit
c7f98efb63
@ -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';
|
||||
|
143
packages/flutter/lib/src/cupertino/page.dart
Normal file
143
packages/flutter/lib/src/cupertino/page.dart
Normal file
@ -0,0 +1,143 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user