blob: 7eeac86647de07b852106f151f8d4ae5c06eb672 [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.dart';
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';
typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
typedef void NotificationCallback();
abstract class RouteBase {
AnimationPerformance _performance;
NotificationCallback onDismissed;
NotificationCallback onCompleted;
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;
}
WatchableAnimationPerformance ensurePerformance({ Direction direction }) {
assert(direction != null);
if (_performance == null)
_performance = createPerformance();
AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse;
if (_performance.status != desiredStatus)
_performance.play(direction);
return _performance.view;
}
bool get isActuallyOpaque => _performance != null && _performance.isCompleted && isOpaque;
bool get hasContent => true; // set to false if you have nothing useful to return from build()
Duration get transitionDuration;
bool get isOpaque;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance);
void popState([dynamic result]) { assert(result == null); }
String toString() => '$runtimeType()';
}
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
class Route extends RouteBase {
Route({ this.name, this.builder });
final String name;
final RouteBuilder builder;
bool get isOpaque => true;
Duration get transitionDuration => _kTransitionDuration;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) {
// 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),
child: builder(navigator, route)
)
);
}
String toString() => '$runtimeType(name="$name")';
}
class RouteState extends RouteBase {
RouteState({ this.callback, this.route, this.owner });
Function callback;
RouteBase route;
StatefulComponent owner;
bool get isOpaque => false;
void popState([dynamic result]) {
assert(result == null);
if (callback != null)
callback(this);
}
bool get hasContent => false;
Duration get transitionDuration => const Duration();
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) => null;
}
class NavigationState {
NavigationState(List<Route> routes) {
for (Route route in routes) {
if (route.name != null)
namedRoutes[route.name] = route;
}
history.add(routes[0]);
}
List<RouteBase> history = new List<RouteBase>();
int historyIndex = 0;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
RouteBase get currentRoute => history[historyIndex];
bool hasPrevious() => historyIndex > 0;
void pushNamed(String name) {
Route route = namedRoutes[name];
assert(route != null);
push(route);
}
void push(RouteBase route) {
assert(!_debugCurrentlyHaveRoute(route));
history.insert(historyIndex + 1, route);
historyIndex++;
}
void pop([dynamic result]) {
if (historyIndex > 0) {
RouteBase route = history[historyIndex];
route.popState(result);
historyIndex--;
}
}
bool _debugCurrentlyHaveRoute(RouteBase route) {
return history.any((candidate) => candidate == route);
}
}
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 = state.history.length-1; i >= 0; i -= 1) {
RouteBase route = state.history[i];
if (!route.hasContent)
continue;
WatchableAnimationPerformance performance = route.ensurePerformance(
direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse
);
route.onDismissed = () {
setState(() {
assert(state.history.contains(route));
state.history.remove(route);
});
};
Key key = new ObjectKey(route);
Widget widget = route.build(key, this, route, performance);
visibleRoutes.add(widget);
if (route.isActuallyOpaque)
break;
}
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
}
}