routes.dart 15 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 8 9
import 'package:flutter/animation.dart';

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

Hixie's avatar
Hixie committed
18 19
const _kTransparent = const Color(0x00000000);

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

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

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

36 37 38 39 40 41 42 43
  /// 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().
  ///
44
  /// Subclasses shouldn't call this if they want to delay the finished() call.
Hixie's avatar
Hixie committed
45
  bool didPop(T result) {
Hixie's avatar
Hixie committed
46
    finished();
Hixie's avatar
Hixie committed
47
    return true;
Hixie's avatar
Hixie committed
48 49 50 51 52 53 54 55 56 57
  }

  /// 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
58 59 60 61
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }
62 63 64 65

  void dispose() {
    finished();
  }
Adam Barth's avatar
Adam Barth committed
66 67
}

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

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

Hixie's avatar
Hixie committed
80 81 82 83 84
  /// 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
85
  final Completer<T> _popCompleter;
Hixie's avatar
Hixie committed
86 87 88

  /// 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
89
  Future<T> get completed => _transitionCompleter?.future;
Hixie's avatar
Hixie committed
90
  final Completer<T> _transitionCompleter;
91

Adam Barth's avatar
Adam Barth committed
92 93 94
  Duration get transitionDuration;
  bool get opaque;

95 96 97 98 99 100 101 102
  PerformanceView get performance => _performance;
  Performance _performanceController;
  PerformanceView _performance;

  /// Called to create the Performance object that will drive the transitions to
  /// this route from the previous one, and back to the previous route from this
  /// one.
  Performance createPerformanceController() {
Adam Barth's avatar
Adam Barth committed
103 104 105 106 107
    Duration duration = transitionDuration;
    assert(duration != null && duration >= Duration.ZERO);
    return new Performance(duration: duration, debugLabel: debugLabel);
  }

108 109 110 111 112 113 114 115
  /// Called to create the PerformanceView that exposes the current progress of
  /// the transition controlled by the Performance object created by
  /// [createPerformanceController()].
  PerformanceView createPerformance() {
    assert(_performanceController != null);
    return _performanceController.view;
  }

Hixie's avatar
Hixie committed
116
  T _result;
Adam Barth's avatar
Adam Barth committed
117

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

Hixie's avatar
Hixie committed
137 138 139
  PerformanceView get forwardPerformance => _forwardPerformance;
  final ProxyPerformance _forwardPerformance = new ProxyPerformance(alwaysDismissedPerformance);

Hixie's avatar
Hixie committed
140
  void install(OverlayEntry insertionPoint) {
141 142
    _performanceController = createPerformanceController();
    assert(_performanceController != null);
143
    _performance = createPerformance();
144
    assert(_performance != null);
Hixie's avatar
Hixie committed
145
    super.install(insertionPoint);
146 147 148 149
  }

  void didPush() {
    _performance.addStatusListener(handleStatusChanged);
150
    _performanceController.forward();
151
    super.didPush();
Adam Barth's avatar
Adam Barth committed
152 153
  }

154 155
  void didReplace(Route oldRoute) {
    if (oldRoute is TransitionRoute)
156
      _performanceController.progress = oldRoute._performanceController.progress;
157 158 159 160
    _performance.addStatusListener(handleStatusChanged);
    super.didReplace(oldRoute);
  }

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

Hixie's avatar
Hixie committed
168 169 170
  void didPopNext(Route nextRoute) {
    _updateForwardPerformance(nextRoute);
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
171 172
  }

Hixie's avatar
Hixie committed
173 174 175
  void didChangeNext(Route nextRoute) {
    _updateForwardPerformance(nextRoute);
    super.didChangeNext(nextRoute);
176 177
  }

Hixie's avatar
Hixie committed
178
  void _updateForwardPerformance(Route nextRoute) {
179
    if (nextRoute is TransitionRoute && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
180
      PerformanceView current = _forwardPerformance.masterPerformance;
Hixie's avatar
Hixie committed
181 182 183 184 185 186 187
      if (current != null) {
        if (current is TrainHoppingPerformance) {
          TrainHoppingPerformance newPerformance;
          newPerformance = new TrainHoppingPerformance(
            current.currentTrain,
            nextRoute.performance,
            onSwitchedTrain: () {
188
              assert(_forwardPerformance.masterPerformance == newPerformance);
Hixie's avatar
Hixie committed
189
              assert(newPerformance.currentTrain == nextRoute.performance);
190
              _forwardPerformance.masterPerformance = newPerformance.currentTrain;
Hixie's avatar
Hixie committed
191 192 193
              newPerformance.dispose();
            }
          );
194
          _forwardPerformance.masterPerformance = newPerformance;
Hixie's avatar
Hixie committed
195 196
          current.dispose();
        } else {
197
          _forwardPerformance.masterPerformance = new TrainHoppingPerformance(current, nextRoute.performance);
Hixie's avatar
Hixie committed
198 199
        }
      } else {
200
        _forwardPerformance.masterPerformance = nextRoute.performance;
Hixie's avatar
Hixie committed
201
      }
Hixie's avatar
Hixie committed
202 203
    } else {
      _forwardPerformance.masterPerformance = alwaysDismissedPerformance;
Hixie's avatar
Hixie committed
204 205 206
    }
  }

207 208 209
  bool canTransitionTo(TransitionRoute nextRoute) => true;
  bool canTransitionFrom(TransitionRoute nextRoute) => true;

Hixie's avatar
Hixie committed
210 211 212 213 214 215 216 217 218 219
  void finished() {
    super.finished();
    _transitionCompleter?.complete(_result);
  }

  void dispose() {
    _performanceController.stop();
    super.dispose();
  }

Adam Barth's avatar
Adam Barth committed
220
  String get debugLabel => '$runtimeType';
221
  String toString() => '$runtimeType(performance: $_performanceController)';
Adam Barth's avatar
Adam Barth committed
222
}
223

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

238
abstract class LocalHistoryRoute<T> extends Route<T> {
Hixie's avatar
Hixie committed
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
  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
264 265 266
  bool get willHandlePopInternally {
    return _localHistory != null && _localHistory.length > 0;
  }
Hixie's avatar
Hixie committed
267 268
}

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

281
  final bool isCurrent;
Hixie's avatar
Hixie committed
282 283 284
  final Route route;

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

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

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

  final ModalRoute route;

303 304 305 306 307 308
  _ModalScopeState createState() => new _ModalScopeState();
}

class _ModalScopeState extends State<_ModalScope> {
  void initState() {
    super.initState();
309 310
    config.route.performance?.addStatusListener(_performanceStatusChanged);
    config.route.forwardPerformance?.addStatusListener(_performanceStatusChanged);
311 312 313
  }

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

  void dispose() {
318 319
    config.route.performance?.removeStatusListener(_performanceStatusChanged);
    config.route.forwardPerformance?.removeStatusListener(_performanceStatusChanged);
320 321 322 323 324 325 326 327 328
    super.dispose();
  }

  void _performanceStatusChanged(PerformanceStatus status) {
    setState(() {
      // The performances' states are our build state, and they changed already.
    });
  }

329 330
  Widget build(BuildContext context) {
    Widget contents = new PageStorage(
331 332
      key: config.route._subtreeKey,
      bucket: config.route._storageBucket,
Hixie's avatar
Hixie committed
333
      child: new _ModalScopeStatus(
334
        route: config.route,
335 336
        isCurrent: config.route.isCurrent,
        child: config.route.buildPage(context, config.route.performance, config.route.forwardPerformance)
Hixie's avatar
Hixie committed
337
      )
338
    );
339
    if (config.route.offstage) {
340 341 342
      contents = new OffStage(child: contents);
    } else {
      contents = new Focus(
343
        key: new GlobalObjectKey(config.route),
344
        child: new IgnorePointer(
345
          ignoring: config.route.performance?.status == PerformanceStatus.reverse,
346 347
          child: config.route.buildTransitions(
            context,
348 349
            config.route.performance,
            config.route.forwardPerformance,
350 351
            contents
          )
352 353 354
        )
      );
    }
355
    contents = new RepaintBoundary(child: contents);
356
    ModalPosition position = config.route.getPosition(context);
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
    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;
}

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

Hixie's avatar
Hixie committed
383 384
  // The API for general users of this class

385
  final RouteSettings settings;
386

387 388 389
  /// 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
390 391 392 393 394
  static ModalRoute of(BuildContext context) {
    _ModalScopeStatus widget = context.inheritFromWidgetOfType(_ModalScopeStatus);
    return widget?.route;
  }

395 396 397

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

398
  ModalPosition getPosition(BuildContext context) => null;
399 400
  Widget buildPage(BuildContext context, PerformanceView performance, PerformanceView forwardPerformance);
  Widget buildTransitions(BuildContext context, PerformanceView performance, PerformanceView forwardPerformance, Widget child) {
401 402 403
    return child;
  }

Hixie's avatar
Hixie committed
404

405 406
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
407 408 409 410 411
  /// 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;
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436


  // 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

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

  Widget _buildModalBarrier(BuildContext context) {
442
    Widget barrier;
Hixie's avatar
Hixie committed
443 444
    if (barrierColor != null) {
      assert(barrierColor != _kTransparent);
445
      barrier = new AnimatedModalBarrier(
Hixie's avatar
Hixie committed
446 447 448 449 450
        color: new AnimatedColorValue(_kTransparent, end: barrierColor, curve: Curves.ease),
        performance: performance,
        dismissable: barrierDismissable
      );
    } else {
451
      barrier = new ModalBarrier(dismissable: barrierDismissable);
Hixie's avatar
Hixie committed
452
    }
453 454 455 456 457
    assert(performance.status != PerformanceStatus.dismissed);
    return new IgnorePointer(
      ignoring: performance.status == PerformanceStatus.reverse,
      child: barrier
    );
458 459 460 461 462 463
  }

  Widget _buildModalScope(BuildContext context) {
    return new _ModalScope(
      key: _scopeKey,
      route: this
464
      // calls buildTransitions() and buildPage(), defined above
465 466 467 468 469 470 471 472
    );
  }

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

473
  String toString() => '$runtimeType($settings, performance: $_performance)';
474
}
Hixie's avatar
Hixie committed
475 476 477 478 479

/// 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
480
  void didChangeNext(Route nextRoute) {
481
    assert(nextRoute is! PageRoute);
Hixie's avatar
Hixie committed
482
    super.didChangeNext(nextRoute);
483
  }
Hixie's avatar
Hixie committed
484
}