routes.dart 14.8 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
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 6
import 'dart:async';

Adam Barth's avatar
Adam Barth committed
7
import 'basic.dart';
8
import 'focus.dart';
Adam Barth's avatar
Adam Barth committed
9
import 'framework.dart';
10
import 'modal_barrier.dart';
Adam Barth's avatar
Adam Barth committed
11 12
import 'navigator.dart';
import 'overlay.dart';
13
import 'page_storage.dart';
14
import 'pages.dart';
Adam Barth's avatar
Adam Barth committed
15

Hixie's avatar
Hixie committed
16 17
const _kTransparent = const Color(0x00000000);

18
/// A route that displays widgets in the [Navigator]'s [Overlay].
Hixie's avatar
Hixie committed
19
abstract class OverlayRoute<T> extends Route<T> {
20
  /// Subclasses should override this getter to return the builders for the overlay.
21
  List<WidgetBuilder> get builders;
Adam Barth's avatar
Adam Barth committed
22

23
  /// The entries this route has placed in the overlay.
Adam Barth's avatar
Adam Barth committed
24
  List<OverlayEntry> get overlayEntries => _overlayEntries;
25
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
Adam Barth's avatar
Adam Barth committed
26

Hixie's avatar
Hixie committed
27
  void install(OverlayEntry insertionPoint) {
28
    assert(_overlayEntries.isEmpty);
29
    for (WidgetBuilder builder in builders)
30
      _overlayEntries.add(new OverlayEntry(builder: builder));
Hixie's avatar
Hixie committed
31
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
Adam Barth's avatar
Adam Barth committed
32 33
  }

34 35 36 37 38 39 40 41
  /// A request was made to pop this route. If the route can handle it
  /// internally (e.g. because it has its own stack of internal state) then
  /// return false, otherwise return true. Returning false will prevent the
  /// default behavior of NavigatorState.pop().
  ///
  /// If this is called, the Navigator will not call dispose(). It is the
  /// responsibility of the Route to later call dispose().
  ///
42
  /// Subclasses shouldn't call this if they want to delay the finished() call.
Hixie's avatar
Hixie committed
43
  bool didPop(T result) {
Hixie's avatar
Hixie committed
44
    finished();
Hixie's avatar
Hixie committed
45
    return true;
Hixie's avatar
Hixie committed
46 47 48 49 50 51 52 53 54 55
  }

  /// Clears out the overlay entries.
  ///
  /// This method is intended to be used by subclasses who don't call
  /// super.didPop() because they want to have control over the timing of the
  /// overlay removal.
  ///
  /// Do not call this method outside of this context.
  void finished() {
Adam Barth's avatar
Adam Barth committed
56 57 58 59
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }
60 61 62 63

  void dispose() {
    finished();
  }
Adam Barth's avatar
Adam Barth committed
64 65
}

Hixie's avatar
Hixie committed
66 67 68 69 70 71
abstract class TransitionRoute<T> extends OverlayRoute<T> {
  TransitionRoute({
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  }) : _popCompleter = popCompleter,
       _transitionCompleter = transitionCompleter;
72

73 74 75 76 77
  TransitionRoute.explicit(
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  ) : this(popCompleter: popCompleter, transitionCompleter: transitionCompleter);

Hixie's avatar
Hixie committed
78 79 80 81 82
  /// This future completes once the animation has been dismissed. For
  /// ModalRoutes, this will be after the completer that's passed in, since that
  /// one completes before the animation even starts, as soon as the route is
  /// popped.
  Future<T> get popped => _popCompleter?.future;
Hixie's avatar
Hixie committed
83
  final Completer<T> _popCompleter;
Hixie's avatar
Hixie committed
84 85 86

  /// This future completes only once the transition itself has finished, after
  /// the overlay entries have been removed from the navigator's overlay.
Hixie's avatar
Hixie committed
87
  Future<T> get completed => _transitionCompleter?.future;
Hixie's avatar
Hixie committed
88
  final Completer<T> _transitionCompleter;
89

Adam Barth's avatar
Adam Barth committed
90 91 92
  Duration get transitionDuration;
  bool get opaque;

93 94
  Animation<double> get animation => _animation;
  Animation<double> _animation;
95
  AnimationController _controller;
96

97
  /// Called to create the animation controller that will drive the transitions to
98 99
  /// this route from the previous one, and back to the previous route from this
  /// one.
100
  AnimationController createAnimationController() {
Adam Barth's avatar
Adam Barth committed
101 102
    Duration duration = transitionDuration;
    assert(duration != null && duration >= Duration.ZERO);
103
    return new AnimationController(duration: duration, debugLabel: debugLabel);
Adam Barth's avatar
Adam Barth committed
104 105
  }

106 107
  /// Called to create the animation that exposes the current progress of
  /// the transition controlled by the animation controller created by
108
  /// [createAnimationController()].
109
  Animation<double> createAnimation() {
110 111
    assert(_controller != null);
    return _controller.view;
112 113
  }

Hixie's avatar
Hixie committed
114
  T _result;
Adam Barth's avatar
Adam Barth committed
115

116
  void handleStatusChanged(AnimationStatus status) {
Adam Barth's avatar
Adam Barth committed
117
    switch (status) {
118
      case AnimationStatus.completed:
Adam Barth's avatar
Adam Barth committed
119 120 121
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
122 123
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
124 125 126
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
127
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
128 129 130
        assert(!overlayEntries.first.opaque);
        finished(); // clear the overlays
        assert(overlayEntries.isEmpty);
Adam Barth's avatar
Adam Barth committed
131 132 133 134
        break;
    }
  }

135
  Animation<double> get forwardAnimation => _forwardAnimation;
136
  final ProxyAnimation _forwardAnimation = new ProxyAnimation(kAlwaysDismissedAnimation);
Hixie's avatar
Hixie committed
137

Hixie's avatar
Hixie committed
138
  void install(OverlayEntry insertionPoint) {
139 140 141 142
    _controller = createAnimationController();
    assert(_controller != null);
    _animation = createAnimation();
    assert(_animation != null);
Hixie's avatar
Hixie committed
143
    super.install(insertionPoint);
144 145 146
  }

  void didPush() {
147 148
    _animation.addStatusListener(handleStatusChanged);
    _controller.forward();
149
    super.didPush();
Adam Barth's avatar
Adam Barth committed
150 151
  }

152 153
  void didReplace(Route oldRoute) {
    if (oldRoute is TransitionRoute)
154 155
      _controller.value = oldRoute._controller.value;
    _animation.addStatusListener(handleStatusChanged);
156 157 158
    super.didReplace(oldRoute);
  }

Hixie's avatar
Hixie committed
159
  bool didPop(T result) {
Adam Barth's avatar
Adam Barth committed
160
    _result = result;
161
    _controller.reverse();
Hixie's avatar
Hixie committed
162
    _popCompleter?.complete(_result);
Hixie's avatar
Hixie committed
163
    return true;
Hixie's avatar
Hixie committed
164 165
  }

Hixie's avatar
Hixie committed
166
  void didPopNext(Route nextRoute) {
167
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
168
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
169 170
  }

Hixie's avatar
Hixie committed
171
  void didChangeNext(Route nextRoute) {
172
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
173
    super.didChangeNext(nextRoute);
174 175
  }

176
  void _updateForwardAnimation(Route nextRoute) {
177
    if (nextRoute is TransitionRoute && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
178
      Animation<double> current = _forwardAnimation.parent;
Hixie's avatar
Hixie committed
179
      if (current != null) {
180
        if (current is TrainHoppingAnimation) {
181 182
          TrainHoppingAnimation newAnimation;
          newAnimation = new TrainHoppingAnimation(
Hixie's avatar
Hixie committed
183
            current.currentTrain,
184
            nextRoute.animation,
Hixie's avatar
Hixie committed
185
            onSwitchedTrain: () {
186
              assert(_forwardAnimation.parent == newAnimation);
187
              assert(newAnimation.currentTrain == nextRoute.animation);
188
              _forwardAnimation.parent = newAnimation.currentTrain;
189
              newAnimation.dispose();
Hixie's avatar
Hixie committed
190 191
            }
          );
192
          _forwardAnimation.parent = newAnimation;
Hixie's avatar
Hixie committed
193 194
          current.dispose();
        } else {
195
          _forwardAnimation.parent = new TrainHoppingAnimation(current, nextRoute.animation);
Hixie's avatar
Hixie committed
196 197
        }
      } else {
198
        _forwardAnimation.parent = nextRoute.animation;
Hixie's avatar
Hixie committed
199
      }
Hixie's avatar
Hixie committed
200
    } else {
201
      _forwardAnimation.parent = kAlwaysDismissedAnimation;
Hixie's avatar
Hixie committed
202 203 204
    }
  }

205 206 207
  bool canTransitionTo(TransitionRoute nextRoute) => true;
  bool canTransitionFrom(TransitionRoute nextRoute) => true;

Hixie's avatar
Hixie committed
208 209 210 211 212 213
  void finished() {
    super.finished();
    _transitionCompleter?.complete(_result);
  }

  void dispose() {
214
    _controller.stop();
Hixie's avatar
Hixie committed
215 216 217
    super.dispose();
  }

Adam Barth's avatar
Adam Barth committed
218
  String get debugLabel => '$runtimeType';
219
  String toString() => '$runtimeType(animation: $_controller)';
Adam Barth's avatar
Adam Barth committed
220
}
221

Hixie's avatar
Hixie committed
222 223 224 225 226 227
class LocalHistoryEntry {
  LocalHistoryEntry({ this.onRemove });
  final VoidCallback onRemove;
  LocalHistoryRoute _owner;
  void remove() {
    _owner.removeLocalHistoryEntry(this);
228
    assert(_owner == null);
Hixie's avatar
Hixie committed
229 230 231 232 233 234 235
  }
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

236
abstract class LocalHistoryRoute<T> extends Route<T> {
Hixie's avatar
Hixie committed
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
  List<LocalHistoryEntry> _localHistory;
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
    _localHistory.add(entry);
  }
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
  }
  bool didPop(T result) {
    if (_localHistory != null && _localHistory.length > 0) {
      LocalHistoryEntry entry = _localHistory.removeLast();
      assert(entry._owner == this);
      entry._owner = null;
      entry._notifyRemoved();
      return false;
    }
    return super.didPop(result);
  }
Hixie's avatar
Hixie committed
262 263 264
  bool get willHandlePopInternally {
    return _localHistory != null && _localHistory.length > 0;
  }
Hixie's avatar
Hixie committed
265 266
}

Hixie's avatar
Hixie committed
267 268 269
class _ModalScopeStatus extends InheritedWidget {
  _ModalScopeStatus({
    Key key,
270
    this.isCurrent,
Hixie's avatar
Hixie committed
271 272 273
    this.route,
    Widget child
  }) : super(key: key, child: child) {
274
    assert(isCurrent != null);
Hixie's avatar
Hixie committed
275 276 277 278
    assert(route != null);
    assert(child != null);
  }

279
  final bool isCurrent;
Hixie's avatar
Hixie committed
280 281 282
  final Route route;

  bool updateShouldNotify(_ModalScopeStatus old) {
283
    return isCurrent != old.isCurrent ||
Hixie's avatar
Hixie committed
284 285 286 287 288
           route != old.route;
  }

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
289
    description.add('${isCurrent ? "active" : "inactive"}');
Hixie's avatar
Hixie committed
290 291 292
  }
}

293
class _ModalScope extends StatefulComponent {
294 295 296
  _ModalScope({
    Key key,
    this.route
297
  }) : super(key: key);
298 299 300

  final ModalRoute route;

301 302 303 304 305 306
  _ModalScopeState createState() => new _ModalScopeState();
}

class _ModalScopeState extends State<_ModalScope> {
  void initState() {
    super.initState();
307 308
    config.route.animation?.addStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.addStatusListener(_animationStatusChanged);
309 310 311
  }

  void didUpdateConfig(_ModalScope oldConfig) {
312
    assert(config.route == oldConfig.route);
313 314 315
  }

  void dispose() {
316 317
    config.route.animation?.removeStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
318 319 320
    super.dispose();
  }

321
  void _animationStatusChanged(AnimationStatus status) {
322
    setState(() {
323
      // The animation's states are our build state, and they changed already.
324 325 326
    });
  }

327 328
  Widget build(BuildContext context) {
    Widget contents = new PageStorage(
329 330
      key: config.route._subtreeKey,
      bucket: config.route._storageBucket,
Hixie's avatar
Hixie committed
331
      child: new _ModalScopeStatus(
332
        route: config.route,
333
        isCurrent: config.route.isCurrent,
334
        child: config.route.buildPage(context, config.route.animation, config.route.forwardAnimation)
Hixie's avatar
Hixie committed
335
      )
336
    );
337
    if (config.route.offstage) {
338 339
      contents = new OffStage(child: contents);
    } else {
340 341 342 343 344 345 346
      contents = new IgnorePointer(
        ignoring: config.route.animation?.status == AnimationStatus.reverse,
        child: config.route.buildTransitions(
          context,
          config.route.animation,
          config.route.forwardAnimation,
          contents
347 348 349
        )
      );
    }
350 351 352 353
    contents = new Focus(
      key: new GlobalObjectKey(config.route),
      child: new RepaintBoundary(child: contents)
    );
354
    ModalPosition position = config.route.getPosition(context);
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
    if (position == null)
      return contents;
    return new Positioned(
      top: position.top,
      right: position.right,
      bottom: position.bottom,
      left: position.left,
      child: contents
    );
  }
}

class ModalPosition {
  const ModalPosition({ this.top, this.right, this.bottom, this.left });
  final double top;
  final double right;
  final double bottom;
  final double left;
}

375
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
376
  ModalRoute({
Hixie's avatar
Hixie committed
377
    Completer<T> completer,
378
    this.settings: const RouteSettings()
379
  }) : super.explicit(completer, null);
380

Hixie's avatar
Hixie committed
381 382
  // The API for general users of this class

383
  final RouteSettings settings;
384

385 386 387
  /// Returns the modal route most closely associated with the given context.
  ///
  /// Returns null if the given context is not associated with a modal route.
Hixie's avatar
Hixie committed
388
  static ModalRoute of(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
389
    _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus);
Hixie's avatar
Hixie committed
390 391 392
    return widget?.route;
  }

393 394 395

  // The API for subclasses to override - used by _ModalScope

396
  ModalPosition getPosition(BuildContext context) => null;
397 398
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation);
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
399 400 401
    return child;
  }

402 403 404 405
  void didPush() {
    Focus.moveScopeTo(new GlobalObjectKey(this), context: navigator.context);
    super.didPush();
  }
Hixie's avatar
Hixie committed
406

407 408
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
409 410 411 412 413
  /// Whether you can dismiss this route by tapping the modal barrier.
  bool get barrierDismissable;
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
  Color get barrierColor;
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438


  // The API for _ModalScope and HeroController

  bool get offstage => _offstage;
  bool _offstage = false;
  void set offstage (bool value) {
    if (_offstage == value)
      return;
    _offstage = value;
    _scopeKey.currentState?.setState(() {
      // _offstage is the value we're setting, but since there might not be a
      // state, we set it outside of this callback (which will only be called if
      // there's a state currently built).
      // _scopeKey is the key for the _ModalScope built in _buildModalScope().
      // When we mark that state dirty, it'll rebuild itself, and use our
      // offstage (via their config.route.offstage) when building.
    });
  }

  BuildContext get subtreeContext => _subtreeKey.currentContext;


  // Internals

439
  final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
440 441 442 443
  final GlobalKey _subtreeKey = new GlobalKey();
  final PageStorageBucket _storageBucket = new PageStorageBucket();

  Widget _buildModalBarrier(BuildContext context) {
444
    Widget barrier;
Hixie's avatar
Hixie committed
445 446
    if (barrierColor != null) {
      assert(barrierColor != _kTransparent);
447
      Animation<Color> color = new ColorTween(
448 449 450 451 452 453
        begin: _kTransparent,
        end: barrierColor
      ).animate(new CurvedAnimation(
        parent: animation,
        curve: Curves.ease
      ));
454
      barrier = new AnimatedModalBarrier(
455
        color: color,
Hixie's avatar
Hixie committed
456 457 458
        dismissable: barrierDismissable
      );
    } else {
459
      barrier = new ModalBarrier(dismissable: barrierDismissable);
Hixie's avatar
Hixie committed
460
    }
461
    assert(animation.status != AnimationStatus.dismissed);
462
    return new IgnorePointer(
463
      ignoring: animation.status == AnimationStatus.reverse,
464 465
      child: barrier
    );
466 467 468 469 470 471
  }

  Widget _buildModalScope(BuildContext context) {
    return new _ModalScope(
      key: _scopeKey,
      route: this
472
      // calls buildTransitions() and buildPage(), defined above
473 474 475 476 477 478 479 480
    );
  }

  List<WidgetBuilder> get builders => <WidgetBuilder>[
    _buildModalBarrier,
    _buildModalScope
  ];

481
  String toString() => '$runtimeType($settings, animation: $_animation)';
482
}
Hixie's avatar
Hixie committed
483 484 485 486 487

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
  PopupRoute({ Completer<T> completer }) : super(completer: completer);
  bool get opaque => false;
Hixie's avatar
Hixie committed
488
  void didChangeNext(Route nextRoute) {
489
    assert(nextRoute is! PageRoute);
Hixie's avatar
Hixie committed
490
    super.didChangeNext(nextRoute);
491
  }
Hixie's avatar
Hixie committed
492
}