Simple version of the iOS back gesture. (#5512)
Doesn't do any of the fancy effects. Just lets the user control the back transition by sliding from the left, like a drawer. Hero transitions are disabled during the gesture. BUG=https://github.com/flutter/flutter/issues/4817
This commit is contained in:
parent
7d0b92897a
commit
f1d5fd8c0d
@ -533,3 +533,75 @@ class TrainHoppingAnimation extends Animation<double>
|
||||
return '$currentTrain\u27A9$runtimeType(no next)';
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface for combining multiple Animations. Subclasses need only
|
||||
/// implement the `value` getter to control how the child animations are
|
||||
/// combined. Can be chained to combine more than 2 animations.
|
||||
///
|
||||
/// For example, to create an animation that is the sum of two others, subclass
|
||||
/// this class and define `T get value = first.value + second.value;`
|
||||
abstract class CompoundAnimation<T> extends Animation<T>
|
||||
with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
|
||||
/// Creates a CompoundAnimation. Both arguments must be non-null. Either can
|
||||
/// be a CompoundAnimation itself to combine multiple animations.
|
||||
CompoundAnimation({
|
||||
this.first,
|
||||
this.next,
|
||||
}) {
|
||||
assert(first != null);
|
||||
assert(next != null);
|
||||
}
|
||||
|
||||
/// The first sub-animation. Its status takes precedence if neither are
|
||||
/// animating.
|
||||
final Animation<T> first;
|
||||
|
||||
/// The second sub-animation.
|
||||
final Animation<T> next;
|
||||
|
||||
@override
|
||||
void didStartListening() {
|
||||
first.addListener(_maybeNotifyListeners);
|
||||
first.addStatusListener(_maybeNotifyStatusListeners);
|
||||
next.addListener(_maybeNotifyListeners);
|
||||
next.addStatusListener(_maybeNotifyStatusListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
void didStopListening() {
|
||||
first.removeListener(_maybeNotifyListeners);
|
||||
first.removeStatusListener(_maybeNotifyStatusListeners);
|
||||
next.removeListener(_maybeNotifyListeners);
|
||||
next.removeStatusListener(_maybeNotifyStatusListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
AnimationStatus get status {
|
||||
// If one of the sub-animations is moving, use that status. Otherwise,
|
||||
// default to `first`.
|
||||
if (next.status == AnimationStatus.forward || next.status == AnimationStatus.reverse)
|
||||
return next.status;
|
||||
return first.status;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($first, $next)';
|
||||
}
|
||||
|
||||
AnimationStatus _lastStatus;
|
||||
void _maybeNotifyStatusListeners(AnimationStatus _) {
|
||||
if (this.status != _lastStatus) {
|
||||
_lastStatus = this.status;
|
||||
notifyStatusListeners(this.status);
|
||||
}
|
||||
}
|
||||
|
||||
T _lastValue;
|
||||
void _maybeNotifyListeners() {
|
||||
if (this.value != _lastValue) {
|
||||
_lastValue = this.value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,23 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'material.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
final FractionalOffsetTween _kMaterialPageTransitionTween = new FractionalOffsetTween(
|
||||
// Used for Android and Fuchsia.
|
||||
class _MountainViewPageTransition extends AnimatedWidget {
|
||||
static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
|
||||
begin: FractionalOffset.bottomLeft,
|
||||
end: FractionalOffset.topLeft
|
||||
);
|
||||
);
|
||||
|
||||
class _MaterialPageTransition extends AnimatedWidget {
|
||||
_MaterialPageTransition({
|
||||
_MountainViewPageTransition({
|
||||
Key key,
|
||||
Animation<double> animation,
|
||||
this.child
|
||||
}) : super(
|
||||
key: key,
|
||||
animation: _kMaterialPageTransitionTween.animate(new CurvedAnimation(
|
||||
animation: _kTween.animate(new CurvedAnimation(
|
||||
parent: animation, // The route's linear 0.0 - 1.0 animation.
|
||||
curve: Curves.fastOutSlowIn
|
||||
)
|
||||
@ -36,6 +39,108 @@ class _MaterialPageTransition 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,
|
||||
animation: _kTween.animate(new CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: new _CupertinoTransitionCurve()
|
||||
)
|
||||
));
|
||||
|
||||
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: animation,
|
||||
child: new Material(
|
||||
elevation: 6,
|
||||
child: child
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationMean extends CompoundAnimation<double> {
|
||||
AnimationMean({
|
||||
Animation<double> left,
|
||||
Animation<double> right,
|
||||
}) : super(first: left, next: right);
|
||||
|
||||
@override
|
||||
double get value => (first.value + next.value) / 2.0;
|
||||
}
|
||||
|
||||
// Custom curve for iOS page transitions. The halfway point is when the page
|
||||
// is fully on-screen. 0.0 is fully off-screen to the right. 1.0 is off-screen
|
||||
// to the left.
|
||||
class _CupertinoTransitionCurve extends Curve {
|
||||
_CupertinoTransitionCurve();
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
if (t > 0.5)
|
||||
return (t - 0.5) / 3.0 + 0.5;
|
||||
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({
|
||||
NavigatorState navigator,
|
||||
this.controller,
|
||||
this.onDisposed,
|
||||
}) : super(navigator);
|
||||
|
||||
AnimationController controller;
|
||||
VoidCallback onDisposed;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
onDisposed();
|
||||
controller.removeStatusListener(handleStatusChanged);
|
||||
controller = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dragUpdate(double delta) {
|
||||
controller.value -= delta;
|
||||
}
|
||||
|
||||
@override
|
||||
void dragEnd() {
|
||||
if (controller.value <= 0.5) {
|
||||
navigator.pop();
|
||||
} else {
|
||||
controller.forward();
|
||||
}
|
||||
// Don't end the gesture until the transition completes.
|
||||
handleStatusChanged(controller.status);
|
||||
controller?.addStatusListener(handleStatusChanged);
|
||||
}
|
||||
|
||||
void handleStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed || status == AnimationStatus.completed)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@ -64,7 +169,28 @@ class MaterialPageRoute<T> extends PageRoute<T> {
|
||||
Color get barrierColor => null;
|
||||
|
||||
@override
|
||||
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) => false;
|
||||
bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
|
||||
return nextRoute is MaterialPageRoute<dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
backGestureController?.dispose();
|
||||
}
|
||||
|
||||
_CupertinoBackGestureController backGestureController;
|
||||
|
||||
@override
|
||||
NavigationGestureController startPopGesture(NavigatorState navigator) {
|
||||
assert(backGestureController == null);
|
||||
backGestureController = new _CupertinoBackGestureController(
|
||||
navigator: navigator,
|
||||
controller: controller,
|
||||
onDisposed: () { backGestureController = null; }
|
||||
);
|
||||
return backGestureController;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
|
||||
@ -83,10 +209,28 @@ class MaterialPageRoute<T> extends PageRoute<T> {
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
|
||||
return new _MaterialPageTransition(
|
||||
// TODO(mpcomplete): This hack prevents the previousRoute from animating
|
||||
// when we pop(). Remove once we fix this bug:
|
||||
// https://github.com/flutter/flutter/issues/5577
|
||||
if (!Navigator.of(context).userGestureInProgress)
|
||||
forwardAnimation = kAlwaysDismissedAnimation;
|
||||
|
||||
ThemeData theme = Theme.of(context);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.android:
|
||||
return new _MountainViewPageTransition(
|
||||
animation: animation,
|
||||
child: child
|
||||
);
|
||||
case TargetPlatform.iOS:
|
||||
return new _CupertinoPageTransition(
|
||||
animation: new AnimationMean(left: animation, right: forwardAnimation),
|
||||
child: child
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -22,6 +22,8 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de
|
||||
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
|
||||
final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0);
|
||||
|
||||
const double _kBackGestureWidth = 20.0;
|
||||
|
||||
/// The Scaffold's appbar is the toolbar, bottom, and the "flexible space"
|
||||
/// that's stacked behind them. The Scaffold's appBarBehavior defines how
|
||||
/// its layout responds to scrolling the application's body.
|
||||
@ -689,6 +691,34 @@ class ScaffoldState extends State<Scaffold> {
|
||||
);
|
||||
}
|
||||
|
||||
// IOS-specific back gesture.
|
||||
|
||||
final GlobalKey _backGestureKey = new GlobalKey();
|
||||
NavigationGestureController _backGestureController;
|
||||
|
||||
bool _shouldHandleBackGesture() {
|
||||
return Theme.of(context).platform == TargetPlatform.iOS && Navigator.canPop(context);
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
_backGestureController = Navigator.of(context).startPopGesture();
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
final RenderBox box = context.findRenderObject();
|
||||
_backGestureController?.dragUpdate(details.primaryDelta / box.size.width);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
_backGestureController?.dragEnd();
|
||||
_backGestureController = null;
|
||||
}
|
||||
|
||||
void _handleDragCancel() {
|
||||
_backGestureController?.dragEnd();
|
||||
_backGestureController = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EdgeInsets padding = MediaQuery.of(context).padding;
|
||||
@ -772,6 +802,24 @@ class ScaffoldState extends State<Scaffold> {
|
||||
child: config.drawer
|
||||
)
|
||||
));
|
||||
} else if (_shouldHandleBackGesture()) {
|
||||
// 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)
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
EdgeInsets appPadding = (config.appBarBehavior != AppBarBehavior.anchor) ? EdgeInsets.zero : padding;
|
||||
|
@ -467,8 +467,21 @@ class HeroController extends NavigatorObserver {
|
||||
}
|
||||
}
|
||||
|
||||
// Disable Hero animations while a user gesture is controlling the navigation.
|
||||
bool _questsEnabled = true;
|
||||
|
||||
@override
|
||||
void didStartUserGesture() {
|
||||
_questsEnabled = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void didStopUserGesture() {
|
||||
_questsEnabled = true;
|
||||
}
|
||||
|
||||
void _checkForHeroQuest() {
|
||||
if (_from != null && _to != null && _from != _to) {
|
||||
if (_from != null && _to != null && _from != _to && _questsEnabled) {
|
||||
_to.offstage = _to.animation.status != AnimationStatus.completed;
|
||||
WidgetsBinding.instance.addPostFrameCallback(_updateQuest);
|
||||
}
|
||||
|
@ -80,6 +80,13 @@ abstract class Route<T> {
|
||||
/// is replaced, or if the navigator itself is disposed).
|
||||
void dispose() { }
|
||||
|
||||
// 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.
|
||||
NavigationGestureController startPopGesture(NavigatorState navigator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Whether this route is the top-most route on the navigator.
|
||||
///
|
||||
/// If this is true, then [isActive] is also true.
|
||||
@ -142,6 +149,39 @@ class NavigatorObserver {
|
||||
|
||||
/// The [Navigator] popped the given route.
|
||||
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }
|
||||
|
||||
/// The [Navigator] is being controlled by a user gesture. Used for the
|
||||
/// iOS back gesture.
|
||||
void didStartUserGesture() { }
|
||||
|
||||
/// User gesture is no longer controlling the [Navigator].
|
||||
void didStopUserGesture() { }
|
||||
}
|
||||
|
||||
// An interface to be implemented by the Route, allowing its transition
|
||||
// animation to be controlled by a drag.
|
||||
abstract class NavigationGestureController {
|
||||
NavigationGestureController(this._navigator) {
|
||||
// Disable Hero transitions until the gesture is complete.
|
||||
_navigator.didStartUserGesture();
|
||||
}
|
||||
|
||||
// Must be called when the gesture is done.
|
||||
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.
|
||||
void dragEnd();
|
||||
|
||||
@protected
|
||||
NavigatorState get navigator => _navigator;
|
||||
NavigatorState _navigator;
|
||||
}
|
||||
|
||||
/// Signature for the [Navigator.popUntil] predicate argument.
|
||||
@ -460,6 +500,27 @@ class NavigatorState extends State<Navigator> {
|
||||
return _history.length > 1 || _history[0].willHandlePopInternally;
|
||||
}
|
||||
|
||||
NavigationGestureController startPopGesture() {
|
||||
if (canPop())
|
||||
return _history.last.startPopGesture(this);
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO(mpcomplete): remove this bool when we fix
|
||||
// https://github.com/flutter/flutter/issues/5577
|
||||
bool _userGestureInProgress = false;
|
||||
bool get userGestureInProgress => _userGestureInProgress;
|
||||
|
||||
void didStartUserGesture() {
|
||||
_userGestureInProgress = true;
|
||||
config.observer?.didStartUserGesture();
|
||||
}
|
||||
|
||||
void didStopUserGesture() {
|
||||
_userGestureInProgress = false;
|
||||
config.observer?.didStopUserGesture();
|
||||
}
|
||||
|
||||
final Set<int> _activePointers = new Set<int>();
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
|
@ -109,6 +109,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
||||
/// forward transition.
|
||||
Animation<double> get animation => _animation;
|
||||
Animation<double> _animation;
|
||||
|
||||
@protected
|
||||
AnimationController get controller => _controller;
|
||||
AnimationController _controller;
|
||||
|
||||
/// Called to create the animation controller that will drive the transitions to
|
||||
|
Loading…
x
Reference in New Issue
Block a user