| // 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)); |
| } |
| } |