page.dart 8.51 KB
Newer Older
1 2 3 4 5
// Copyright 2015 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/widgets.dart';
6 7
import 'package:meta/meta.dart';

8 9
import 'material.dart';
import 'theme.dart';
10

11 12
const double _kMinFlingVelocity = 1.0;  // screen width per second

13 14 15 16 17 18
// Used for Android and Fuchsia.
class _MountainViewPageTransition extends AnimatedWidget {
  static final FractionalOffsetTween _kTween = new FractionalOffsetTween(
    begin: FractionalOffset.bottomLeft,
    end: FractionalOffset.topLeft
  );
19

20
  _MountainViewPageTransition({
21
    Key key,
22
    Animation<double> animation,
23 24 25
    this.child
  }) : super(
    key: key,
26
    animation: _kTween.animate(new CurvedAnimation(
27 28 29 30
      parent: animation, // The route's linear 0.0 - 1.0 animation.
      curve: Curves.fastOutSlowIn
    )
  ));
31

32
  final Widget child;
33

34
  @override
35
  Widget build(BuildContext context) {
36 37 38 39
    // TODO(ianh): tell the transform to be un-transformed for hit testing
    return new SlideTransition(
      position: animation,
      child: child
40 41 42 43
    );
  }
}

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
// 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,
59
      curve: new _CupertinoTransitionCurve(null)
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
    )
  ));

  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
      )
    );
  }
}

79
// Custom curve for iOS page transitions.
80
class _CupertinoTransitionCurve extends Curve {
81 82 83
  _CupertinoTransitionCurve(this.curve);

  Curve curve;
84 85 86

  @override
  double transform(double t) {
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    // 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;
    }
106 107 108 109 110 111 112 113
    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({
114 115 116 117 118 119 120
    @required NavigatorState navigator,
    @required this.controller,
    @required this.onDisposed,
  }) : super(navigator) {
    assert(controller != null);
    assert(onDisposed != null);
  }
121 122

  AnimationController controller;
123
  final VoidCallback onDisposed;
124 125 126 127 128

  @override
  void dispose() {
    controller.removeStatusListener(handleStatusChanged);
    controller = null;
129 130
    onDisposed();
    super.dispose();
131 132 133 134
  }

  @override
  void dragUpdate(double delta) {
135 136 137 138 139 140
    // 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);
141 142 143 144
    controller.value -= delta;
  }

  @override
145
  bool dragEnd(double velocity) {
146 147 148 149 150 151 152
    // 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);

153 154 155 156
    if (velocity.abs() >= _kMinFlingVelocity) {
      controller.fling(velocity: -velocity);
    } else if (controller.value <= 0.5) {
      controller.fling(velocity: -1.0);
157
    } else {
158
      controller.fling(velocity: 1.0);
159
    }
160

161
    // Don't end the gesture until the transition completes.
162
    final AnimationStatus status = controller.status;
163
    handleStatusChanged(status);
164
    controller?.addStatusListener(handleStatusChanged);
165 166

    return (status == AnimationStatus.reverse || status == AnimationStatus.dismissed);
167 168 169
  }

  void handleStatusChanged(AnimationStatus status) {
170
    if (status == AnimationStatus.dismissed) {
171
      navigator.pop();
172 173
      assert(controller == null);
    } else if (status == AnimationStatus.completed) {
174
      dispose();
175 176
      assert(controller == null);
    }
177 178 179
  }
}

180 181 182 183 184
/// 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
/// in. The exit transition is the same, but in reverse.
///
185 186 187
/// 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.
Hixie's avatar
Hixie committed
188
class MaterialPageRoute<T> extends PageRoute<T> {
189
  /// Creates a page route for use in a material design app.
190 191
  MaterialPageRoute({
    this.builder,
192 193
    RouteSettings settings: const RouteSettings(),
    this.maintainState: true,
194
  }) : super(settings: settings) {
195 196 197 198
    assert(builder != null);
    assert(opaque);
  }

199
  /// Builds the primary contents of the route.
200 201
  final WidgetBuilder builder;

202 203 204
  @override
  final bool maintainState;

205
  @override
206
  Duration get transitionDuration => const Duration(milliseconds: 300);
207 208

  @override
209
  Color get barrierColor => null;
210 211

  @override
212 213 214 215 216 217
  bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
    return nextRoute is MaterialPageRoute<dynamic>;
  }

  @override
  void dispose() {
218
    _backGestureController?.dispose();
219 220 221
    super.dispose();
  }

222
  _CupertinoBackGestureController _backGestureController;
223

224 225 226 227 228 229 230 231 232
  /// 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:
  ///
  ///  * [hasScopedWillPopCallback], which is true if a `willPop` callback
  ///    is defined for this route.
233
  @override
234
  NavigationGestureController startPopGesture() {
235 236 237 238
    // If attempts to dismiss this route might be vetoed, then do not
    // allow the user to dismiss the route with a swipe.
    if (hasScopedWillPopCallback)
      return null;
239 240
    if (controller.status != AnimationStatus.completed)
      return null;
241 242
    assert(_backGestureController == null);
    _backGestureController = new _CupertinoBackGestureController(
243 244
      navigator: navigator,
      controller: controller,
245
      onDisposed: () { _backGestureController = null; }
246
    );
247
    return _backGestureController;
248
  }
249

250
  @override
251
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
252 253
    Widget result = builder(context);
    assert(() {
254 255 256 257 258 259
      if (result == null) {
        throw new FlutterError(
          'The builder for route "${settings.name}" returned null.\n'
          'Route builders must never return null.'
        );
      }
260 261 262 263 264
      return true;
    });
    return result;
  }

265
  @override
266
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
267 268 269 270 271 272 273 274 275 276 277
    if (Theme.of(context).platform == TargetPlatform.iOS &&
        Navigator.of(context).userGestureInProgress) {
      return new _CupertinoPageTransition(
        animation: new AnimationMean(left: animation, right: forwardAnimation),
        child: child
      );
    } else {
      return new _MountainViewPageTransition(
        animation: animation,
        child: child
      );
278
    }
279 280
  }

281
  @override
282 283
  String get debugLabel => '${super.debugLabel}(${settings.name})';
}