page.dart 6.25 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/cupertino.dart';
6
import 'package:flutter/foundation.dart';  
7
import 'package:flutter/widgets.dart';
8

9
import 'theme.dart';
10

11 12 13 14 15 16
// Fractional offset from 1/4 screen below the top to fully on screen. 
final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween(
  begin: const FractionalOffset(0.0, 0.25),
  end: FractionalOffset.topLeft
);

17
// Used for Android and Fuchsia.
18
class _MountainViewPageTransition extends StatelessWidget {
19
  _MountainViewPageTransition({
20
    Key key,
21 22 23 24 25 26 27 28 29 30 31
    @required Animation<double> routeAnimation,
    @required this.child,
  }) : _positionAnimation = _kBottomUpTween.animate(new CurvedAnimation(
         parent: routeAnimation, // The route's linear 0.0 - 1.0 animation.
         curve: Curves.fastOutSlowIn,
       )),
       _opacityAnimation = new CurvedAnimation(
         parent: routeAnimation,
         curve: Curves.easeIn, // Eyeballed from other Material apps.
       ),
       super(key: key);
32

33 34
  final Animation<FractionalOffset> _positionAnimation;
  final Animation<double> _opacityAnimation;
35
  final Widget child;
36

37
  @override
38
  Widget build(BuildContext context) {
39 40
    // TODO(ianh): tell the transform to be un-transformed for hit testing
    return new SlideTransition(
41
      position: _positionAnimation,
42
      child: new FadeTransition(
43
        opacity: _opacityAnimation,
44 45
        child: child,
      ),
46 47 48 49
    );
  }
}

50 51
/// A modal route that replaces the entire screen with a material design transition.
///
52
/// For Android, the entrance transition for the page slides the page upwards and fades it
53 54
/// in. The exit transition is the same, but in reverse.
///
55 56 57 58
/// 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.
///
59 60 61
/// 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.
62 63 64
///
/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those
/// pages animate bottom->up rather than right->left.
Hixie's avatar
Hixie committed
65
class MaterialPageRoute<T> extends PageRoute<T> {
66
  /// Creates a page route for use in a material design app.
67
  MaterialPageRoute({
68
    @required this.builder,
69 70
    RouteSettings settings: const RouteSettings(),
    this.maintainState: true,
71
    this.fullscreenDialog: false,
72
  }) : super(settings: settings) {
73 74 75 76
    assert(builder != null);
    assert(opaque);
  }

77
  /// Builds the primary contents of the route.
78
  final WidgetBuilder builder;
Adam Barth's avatar
Adam Barth committed
79 80 81 82 83

  /// Whether this route is a full-screen dialog.
  ///
  /// Prevents [startPopGesture] from poping the route using an edge swipe on
  /// iOS.
84
  final bool fullscreenDialog;
85

86 87 88
  @override
  final bool maintainState;

89
  @override
90
  Duration get transitionDuration => const Duration(milliseconds: 300);
91 92

  @override
93
  Color get barrierColor => null;
94 95

  @override
96 97 98 99
  bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) {
    return nextRoute is MaterialPageRoute<dynamic>;
  }

100 101 102 103 104 105
  @override
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
    // Don't perform outgoing animation if the next route is a fullscreen dialog.
    return nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog;
  }

106 107
  @override
  void dispose() {
108
    _backGestureController?.dispose();
109 110 111
    super.dispose();
  }

112
  CupertinoBackGestureController _backGestureController;
113

114 115 116 117 118 119 120 121 122
  /// 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.
123
  @override
124
  NavigationGestureController startPopGesture() {
125 126 127 128
    // 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;
129 130 131
    // Fullscreen dialogs aren't dismissable by back swipe.
    if (fullscreenDialog)
      return null;
132 133
    if (controller.status != AnimationStatus.completed)
      return null;
134
    assert(_backGestureController == null);
135
    _backGestureController = new CupertinoBackGestureController(
136 137 138
      navigator: navigator,
      controller: controller,
    );
139 140

    controller.addStatusListener(_handleBackGestureEnded);
141
    return _backGestureController;
142
  }
143

144 145 146 147 148 149 150 151
  void _handleBackGestureEnded(AnimationStatus status) {
    if (status == AnimationStatus.completed) {
      _backGestureController?.dispose();
      _backGestureController = null;
      controller.removeStatusListener(_handleBackGestureEnded);
    }
  }

152
  @override
153
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
154
    final Widget result = builder(context);
155
    assert(() {
156 157 158 159 160 161
      if (result == null) {
        throw new FlutterError(
          'The builder for route "${settings.name}" returned null.\n'
          'Route builders must never return null.'
        );
      }
162 163 164 165 166
      return true;
    });
    return result;
  }

167
  @override
168
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
169 170 171 172 173 174 175 176
    if (Theme.of(context).platform == TargetPlatform.iOS) {
      if (fullscreenDialog)
        return new CupertinoFullscreenDialogTransition(
          animation: animation,
          child: child,
        );
      else
        return new CupertinoPageTransition(
177 178
          primaryRouteAnimation: animation,
          secondaryRouteAnimation: secondaryAnimation,
179
          child: child,
180 181 182
          // In the middle of a back gesture drag, let the transition be linear to match finger
          // motions.
          linearTransition: _backGestureController != null,
183
        );
184 185
    } else {
      return new _MountainViewPageTransition(
186
        routeAnimation: animation,
187 188
        child: child
      );
189
    }
190 191
  }

192
  @override
193 194
  String get debugLabel => '${super.debugLabel}(${settings.name})';
}