blob: c1c48faaead80a188b155675a9cb00e1ec7b3770 [file] [log] [blame]
// 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.
import 'package:sky/animation/animated_value.dart';
import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/curves.dart';
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/focus.dart';
import 'package:sky/widgets/transitions.dart';
typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
abstract class RouteBase {
Widget build(Navigator navigator, RouteBase route);
bool get isOpaque;
void popState([dynamic result]) { assert(result == null); }
TransitionBase buildTransition({ Key key });
}
class Route extends RouteBase {
Route({ this.name, this.builder });
final String name;
final RouteBuilder builder;
Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
bool get isOpaque => true;
TransitionBase buildTransition({ Key key }) => new SlideUpFadeTransition(key: key);
}
class RouteState extends RouteBase {
RouteState({ this.callback, this.route, this.owner });
Function callback;
RouteBase route;
StatefulComponent owner;
Widget build(Navigator navigator, RouteBase route) => null;
bool get isOpaque => false;
void popState([dynamic result]) {
assert(result == null);
if (callback != null)
callback(this);
}
TransitionBase buildTransition({ Key key }) {
// Custom state routes shouldn't be asked to construct a transition
assert(false);
return null;
}
}
// TODO(jackson): Refactor this into its own file
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
class SlideUpFadeTransition extends TransitionBase {
SlideUpFadeTransition({
Key key,
Widget child,
Direction direction,
Function onDismissed,
Function onCompleted
}): super(key: key,
child: child,
duration: _kTransitionDuration,
direction: direction,
onDismissed: onDismissed,
onCompleted: onCompleted);
Widget buildWithChild(Widget child) {
// TODO(jackson): Hit testing should ignore transform
// TODO(jackson): Block input unless content is interactive
return new SlideTransition(
performance: performance,
direction: direction,
position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
child: new FadeTransition(
performance: performance,
direction: direction,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: child
)
);
}
}
class HistoryEntry {
HistoryEntry({ this.route });
final RouteBase route;
bool fullyOpaque = false;
// TODO(jackson): Keep track of the requested transition
}
class NavigationState {
NavigationState(List<Route> routes) {
for (Route route in routes) {
if (route.name != null)
namedRoutes[route.name] = route;
}
history.add(new HistoryEntry(route: routes[0]));
}
List<HistoryEntry> history = new List<HistoryEntry>();
int historyIndex = 0;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
RouteBase get currentRoute => history[historyIndex].route;
bool hasPrevious() => historyIndex > 0;
void pushNamed(String name) {
Route route = namedRoutes[name];
assert(route != null);
push(route);
}
void push(RouteBase route) {
HistoryEntry historyEntry = new HistoryEntry(route: route);
history.insert(historyIndex + 1, historyEntry);
historyIndex++;
}
void pop([dynamic result]) {
if (historyIndex > 0) {
HistoryEntry entry = history[historyIndex];
entry.route.popState(result);
entry.fullyOpaque = false;
historyIndex--;
}
}
}
class Navigator extends StatefulComponent {
Navigator(this.state, { Key key }) : super(key: key);
NavigationState state;
void syncConstructorArguments(Navigator source) {
state = source.state;
}
RouteBase get currentRoute => state.currentRoute;
void pushState(StatefulComponent owner, Function callback) {
RouteBase route = new RouteState(
owner: owner,
callback: callback,
route: state.currentRoute
);
push(route);
}
void pushNamed(String name) {
setState(() {
state.pushNamed(name);
});
}
void push(RouteBase route) {
setState(() {
state.push(route);
});
}
void pop([dynamic result]) {
setState(() {
state.pop(result);
});
}
Widget build() {
List<Widget> visibleRoutes = new List<Widget>();
for (int i = 0; i < state.history.length; i++) {
// Avoid building routes that are not visible
if (i + 1 < state.history.length && state.history[i + 1].fullyOpaque)
continue;
HistoryEntry historyEntry = state.history[i];
Widget child = historyEntry.route.build(this, historyEntry.route);
if (i == 0) {
visibleRoutes.add(child);
continue;
}
if (child == null)
continue;
TransitionBase transition = historyEntry.route.buildTransition(key: new Key.fromObjectIdentity(historyEntry))
..child = child
..direction = (i <= state.historyIndex) ? Direction.forward : Direction.reverse
..onDismissed = () {
setState(() {
state.history.remove(historyEntry);
});
}
..onCompleted = () {
setState(() {
historyEntry.fullyOpaque = historyEntry.route.isOpaque;
});
};
visibleRoutes.add(transition);
}
return new Focus(child: new Stack(visibleRoutes));
}
}