navigator.dart 6.09 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:sky/animation.dart';
6 7 8 9
import 'package:sky/src/widgets/basic.dart';
import 'package:sky/src/widgets/focus.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/transitions.dart';
10

11
typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
12

13 14
typedef void NotificationCallback();

15
abstract class RouteBase {
16 17 18
  AnimationPerformance _performance;
  NotificationCallback onDismissed;
  NotificationCallback onCompleted;
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
  AnimationPerformance createPerformance() {
    AnimationPerformance result = new AnimationPerformance(duration: transitionDuration);
    result.addStatusListener((AnimationStatus status) {
      switch (status) {
        case AnimationStatus.dismissed:
          if (onDismissed != null)
            onDismissed();
          break;
        case AnimationStatus.completed:
          if (onCompleted != null)
            onCompleted();
          break;
        default:
          ;
      }
    });
    return result;
  }
37 38
  WatchableAnimationPerformance ensurePerformance({ Direction direction }) {
    assert(direction != null);
39 40
    if (_performance == null)
      _performance = createPerformance();
41 42 43 44 45
    AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse;
    if (_performance.status != desiredStatus)
      _performance.play(direction);
    return _performance.view;
  }
46 47 48
  bool get isActuallyOpaque => _performance != null && _performance.isCompleted && isOpaque;

  bool get hasContent => true; // set to false if you have nothing useful to return from build()
49 50

  Duration get transitionDuration;
51
  bool get isOpaque;
52
  Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance);
53
  void popState([dynamic result]) { assert(result == null); }
54 55

  String toString() => '$runtimeType()';
56 57
}

58 59
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
60
class Route extends RouteBase {
61
  Route({ this.name, this.builder });
62

63
  final String name;
64 65 66
  final RouteBuilder builder;

  bool get isOpaque => true;
67 68

  Duration get transitionDuration => _kTransitionDuration;
69

70
  Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
71 72 73 74 75 76 77 78 79
    // TODO(jackson): Hit testing should ignore transform
    // TODO(jackson): Block input unless content is interactive
    return new SlideTransition(
      key: key,
      performance: performance,
      position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
      child: new FadeTransition(
        performance: performance,
        opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
80
        child: builder(navigator, this)
81 82 83 84 85
      )
    );
  }

  String toString() => '$runtimeType(name="$name")';
86 87 88
}

class RouteState extends RouteBase {
89
  RouteState({ this.callback, this.route, this.owner });
90 91

  Function callback;
92 93
  RouteBase route;
  StatefulComponent owner;
94

95
  bool get isOpaque => false;
96

97 98
  void popState([dynamic result]) {
    assert(result == null);
99 100 101
    if (callback != null)
      callback(this);
  }
102

103 104
  bool get hasContent => false;
  Duration get transitionDuration => const Duration();
105
  Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) => null;
106 107
}

108 109 110 111 112 113 114
class NavigationState {

  NavigationState(List<Route> routes) {
    for (Route route in routes) {
      if (route.name != null)
        namedRoutes[route.name] = route;
    }
115
    history.add(routes[0]);
116 117
  }

118
  List<RouteBase> history = new List<RouteBase>();
119 120 121
  int historyIndex = 0;
  Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();

122
  RouteBase get currentRoute => history[historyIndex];
123 124 125 126 127 128 129 130 131
  bool hasPrevious() => historyIndex > 0;

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

  void push(RouteBase route) {
132
    assert(!_debugCurrentlyHaveRoute(route));
133
    history.insert(historyIndex + 1, route);
134
    historyIndex++;
135 136
  }

137
  void pop([dynamic result]) {
138
    if (historyIndex > 0) {
139 140
      RouteBase route = history[historyIndex];
      route.popState(result);
141 142 143
      historyIndex--;
    }
  }
144 145

  bool _debugCurrentlyHaveRoute(RouteBase route) {
146
    return history.any((candidate) => candidate == route);
147
  }
148 149 150 151
}

class Navigator extends StatefulComponent {

152
  Navigator(this.state, { Key key }) : super(key: key);
153 154 155

  NavigationState state;

156
  void syncConstructorArguments(Navigator source) {
157 158 159 160 161
    state = source.state;
  }

  RouteBase get currentRoute => state.currentRoute;

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

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

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

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

  Widget build() {
190
    List<Widget> visibleRoutes = new List<Widget>();
191 192 193
    for (int i = state.history.length-1; i >= 0; i -= 1) {
      RouteBase route = state.history[i];
      if (!route.hasContent)
194
        continue;
195
      WatchableAnimationPerformance performance = route.ensurePerformance(
196 197
        direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse
      );
198
      route.onDismissed = () {
199
        setState(() {
200 201
          assert(state.history.contains(route));
          state.history.remove(route);
202 203
        });
      };
204
      Key key = new ObjectKey(route);
205
      Widget widget = route.build(key, this, performance);
206 207 208
      visibleRoutes.add(widget);
      if (route.isActuallyOpaque)
        break;
209
    }
210 211
    if (visibleRoutes.length > 1) {
      visibleRoutes.insert(1, new Listener(
Adam Barth's avatar
Adam Barth committed
212
        onPointerDown: (_) { pop(); },
213 214 215
        child: new Container()
      ));
    }
216
    return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
217 218
  }
}