navigator.dart 5.89 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 6
import 'dart:async';

7
import 'package:sky/animation/animated_value.dart';
8 9
import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/curves.dart';
10
import 'package:sky/animation/forces.dart';
11
import 'package:sky/widgets/basic.dart';
12
import 'package:sky/widgets/focus.dart';
13
import 'package:sky/widgets/transitions.dart';
14

15
typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
16 17 18

abstract class RouteBase {
  Widget build(Navigator navigator, RouteBase route);
19
  bool get isOpaque;
20
  void popState([dynamic result]) { assert(result == null); }
21 22 23
}

class Route extends RouteBase {
24
  Route({ this.name, this.builder });
25

26
  final String name;
27 28
  final RouteBuilder builder;

29
  Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
30
  bool get isOpaque => true;
31 32
}

33
class DialogRoute extends RouteBase {
34
  DialogRoute({ this.completer, this.builder });
35

36
  final Completer completer;
37 38 39 40
  final RouteBuilder builder;

  Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
  bool get isOpaque => false;
41

42 43
  void popState([dynamic result]) {
    completer.complete(result);
44 45 46 47
  }
}

class RouteState extends RouteBase {
48
  RouteState({ this.callback, this.route, this.owner });
49 50

  Function callback;
51 52
  RouteBase route;
  StatefulComponent owner;
53

54
  Widget build(Navigator navigator, RouteBase route) => null;
55
  bool get isOpaque => false;
56

57 58
  void popState([dynamic result]) {
    assert(result == null);
59 60 61 62 63
    if (callback != null)
      callback(this);
  }
}

64 65
// TODO(jackson): Refactor this into its own file
// and support multiple transition types
66 67
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
68
class Transition extends TransitionBase {
69
  Transition({
70
    Key key,
71 72 73 74
    Widget child,
    Direction direction,
    Function onDismissed,
    Function onCompleted,
75
    this.interactive
76 77 78 79 80 81
  }): super(key: key,
            child: child,
            duration: _kTransitionDuration,
            direction: direction,
            onDismissed: onDismissed,
            onCompleted: onCompleted);
82 83 84
  bool interactive;

  void syncFields(Transition source) {
85
    interactive = source.interactive;
86 87 88 89 90 91
    super.syncFields(source);
  }

  Widget build() {
    // TODO(jackson): Hit testing should ignore transform
    // TODO(jackson): Block input unless content is interactive
92
    return new SlideTransition(
93 94 95
      performance: performance,
      direction: direction,
      position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
96
      child: new FadeTransition(
97 98 99 100
        performance: performance,
        direction: direction,
        opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
        child: child
101 102 103 104 105 106
      )
    );
  }
}

class HistoryEntry {
107
  HistoryEntry({ this.route });
108
  final RouteBase route;
109
  bool fullyOpaque = false;
110 111 112
  // TODO(jackson): Keep track of the requested transition
}

113 114 115 116 117 118 119
class NavigationState {

  NavigationState(List<Route> routes) {
    for (Route route in routes) {
      if (route.name != null)
        namedRoutes[route.name] = route;
    }
120
    history.add(new HistoryEntry(route: routes[0]));
121 122
  }

123
  List<HistoryEntry> history = new List<HistoryEntry>();
124 125 126
  int historyIndex = 0;
  Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();

127
  RouteBase get currentRoute => history[historyIndex].route;
128 129 130 131 132 133 134 135 136
  bool hasPrevious() => historyIndex > 0;

  void pushNamed(String name) {
    Route route = namedRoutes[name];
    assert(route != null);
    push(route);
  }

  void push(RouteBase route) {
137
    HistoryEntry historyEntry = new HistoryEntry(route: route);
138 139
    history.insert(historyIndex + 1, historyEntry);
    historyIndex++;
140 141
  }

142
  void pop([dynamic result]) {
143
    if (historyIndex > 0) {
144
      HistoryEntry entry = history[historyIndex];
145
      entry.route.popState(result);
146
      entry.fullyOpaque = false;
147 148 149 150 151 152 153
      historyIndex--;
    }
  }
}

class Navigator extends StatefulComponent {

154
  Navigator(this.state, { Key key }) : super(key: key);
155 156 157 158 159 160 161 162 163

  NavigationState state;

  void syncFields(Navigator source) {
    state = source.state;
  }

  RouteBase get currentRoute => state.currentRoute;

164
  void pushState(StatefulComponent owner, Function callback) {
165
    RouteBase route = new RouteState(
166
      owner: owner,
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
      callback: callback,
      route: state.currentRoute
    );
    push(route);
  }

  void pushNamed(String name) {
    setState(() {
      state.pushNamed(name);
    });
  }

  void push(RouteBase route) {
    setState(() {
      state.push(route);
    });
  }

185
  void pop([dynamic result]) {
186
    setState(() {
187
      state.pop(result);
188 189 190 191
    });
  }

  Widget build() {
192 193
    List<Widget> visibleRoutes = new List<Widget>();
    for (int i = 0; i < state.history.length; i++) {
194
      // Avoid building routes that are not visible
195
      if (i + 1 < state.history.length && state.history[i + 1].fullyOpaque)
196
        continue;
197
      HistoryEntry historyEntry = state.history[i];
198
      Widget child = historyEntry.route.build(this, historyEntry.route);
199
      if (i == 0) {
200
        visibleRoutes.add(child);
201 202
        continue;
      }
203
      if (child == null)
204
        continue;
Collin Jackson's avatar
Collin Jackson committed
205
      Transition transition = new Transition(
206
        key: new Key.fromObjectIdentity(historyEntry),
207
        child: child,
208
        direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse,
Collin Jackson's avatar
Collin Jackson committed
209 210 211 212 213
        interactive: (i == state.historyIndex),
        onDismissed: () {
          setState(() {
            state.history.remove(historyEntry);
          });
214 215 216
        },
        onCompleted: () {
          setState(() {
217
            historyEntry.fullyOpaque = historyEntry.route.isOpaque;
218
          });
Collin Jackson's avatar
Collin Jackson committed
219 220
        }
      );
221 222
      visibleRoutes.add(transition);
    }
223
    return new Focus(child: new Stack(visibleRoutes));
224 225
  }
}