Commit d4dd786b authored by Hixie's avatar Hixie

Allow route transitions to be more flexible

- Fix AnimationTiming to have defaults for 'interval' and 'curve' since
  that seems to be how we use it.

- Merge RouteBase.build and RouteBase.buildTransition

- Get rid of HistoryEntry, since it added nothing

- Broke out RouteBase.createPerformance() so subclasses can change what
  is created.

- Build the routes backwards so that we more efficiently avoid building
  hidden routes.

- Introduce an explicit way (!hasContent) for RouteState to avoid
  building, rather than the implicit "build returns null" we had before.
parent 68211652
...@@ -30,9 +30,9 @@ abstract class AnimatedVariable { ...@@ -30,9 +30,9 @@ abstract class AnimatedVariable {
/// can be made to take longer in one direction that the other. /// can be made to take longer in one direction that the other.
class AnimationTiming { class AnimationTiming {
AnimationTiming({ AnimationTiming({
this.interval, this.interval: const Interval(0.0, 1.0),
this.reverseInterval, this.reverseInterval,
this.curve, this.curve: linear,
this.reverseCurve this.reverseCurve
}); });
......
...@@ -27,14 +27,9 @@ class Linear implements Curve { ...@@ -27,14 +27,9 @@ class Linear implements Curve {
double transform(double t) => t; double transform(double t) => t;
} }
/// A curve that is initially 0.0, then linear, then 1.0 /// A curve that is 0.0 until start, then linear from 0.0 to 1.0 at end, then 1.0
class Interval implements Curve { class Interval implements Curve {
Interval(this.start, this.end) { const Interval(this.start, this.end);
assert(start >= 0.0);
assert(start <= 1.0);
assert(end >= 0.0);
assert(end <= 1.0);
}
/// The smallest value for which this interval is 0.0 /// The smallest value for which this interval is 0.0
final double start; final double start;
...@@ -43,6 +38,10 @@ class Interval implements Curve { ...@@ -43,6 +38,10 @@ class Interval implements Curve {
final double end; final double end;
double transform(double t) { double transform(double t) {
assert(start >= 0.0);
assert(start <= 1.0);
assert(end >= 0.0);
assert(end <= 1.0);
return ((t - start) / (end - start)).clamp(0.0, 1.0); return ((t - start) / (end - start)).clamp(0.0, 1.0);
} }
} }
......
...@@ -139,21 +139,19 @@ class DialogRoute extends RouteBase { ...@@ -139,21 +139,19 @@ class DialogRoute extends RouteBase {
final Completer completer; final Completer completer;
final RouteBuilder builder; final RouteBuilder builder;
Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
bool get isOpaque => false;
void popState([dynamic result]) {
completer.complete(result);
}
Duration get transitionDuration => _kTransitionDuration; Duration get transitionDuration => _kTransitionDuration;
TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }) { bool get isOpaque => false;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) {
return new FadeTransition( return new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: child child: builder(navigator, route)
); );
} }
void popState([dynamic result]) {
completer.complete(result);
}
} }
Future showDialog(Navigator navigator, DialogBuilder builder) { Future showDialog(Navigator navigator, DialogBuilder builder) {
......
...@@ -13,18 +13,12 @@ typedef Widget RouteBuilder(Navigator navigator, RouteBase route); ...@@ -13,18 +13,12 @@ typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
typedef void NotificationCallback(); typedef void NotificationCallback();
abstract class RouteBase { abstract class RouteBase {
Widget build(Navigator navigator, RouteBase route);
bool get isOpaque;
void popState([dynamic result]) { assert(result == null); }
AnimationPerformance _performance; AnimationPerformance _performance;
NotificationCallback onDismissed; NotificationCallback onDismissed;
NotificationCallback onCompleted; NotificationCallback onCompleted;
WatchableAnimationPerformance ensurePerformance({ Direction direction }) { AnimationPerformance createPerformance() {
assert(direction != null); AnimationPerformance result = new AnimationPerformance(duration: transitionDuration);
if (_performance == null) { result.addStatusListener((AnimationStatus status) {
_performance = new AnimationPerformance(duration: transitionDuration);
_performance.addStatusListener((AnimationStatus status) {
switch (status) { switch (status) {
case AnimationStatus.dismissed: case AnimationStatus.dismissed:
if (onDismissed != null) if (onDismissed != null)
...@@ -38,15 +32,25 @@ abstract class RouteBase { ...@@ -38,15 +32,25 @@ abstract class RouteBase {
; ;
} }
}); });
return result;
} }
WatchableAnimationPerformance ensurePerformance({ Direction direction }) {
assert(direction != null);
if (_performance == null)
_performance = createPerformance();
AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse; AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse;
if (_performance.status != desiredStatus) if (_performance.status != desiredStatus)
_performance.play(direction); _performance.play(direction);
return _performance.view; 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; Duration get transitionDuration;
TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }); bool get isOpaque;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance);
void popState([dynamic result]) { assert(result == null); }
String toString() => '$runtimeType()'; String toString() => '$runtimeType()';
} }
...@@ -59,11 +63,11 @@ class Route extends RouteBase { ...@@ -59,11 +63,11 @@ class Route extends RouteBase {
final String name; final String name;
final RouteBuilder builder; final RouteBuilder builder;
Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
bool get isOpaque => true; bool get isOpaque => true;
Duration get transitionDuration => _kTransitionDuration; Duration get transitionDuration => _kTransitionDuration;
TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }) {
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) {
// TODO(jackson): Hit testing should ignore transform // TODO(jackson): Hit testing should ignore transform
// TODO(jackson): Block input unless content is interactive // TODO(jackson): Block input unless content is interactive
return new SlideTransition( return new SlideTransition(
...@@ -73,7 +77,7 @@ class Route extends RouteBase { ...@@ -73,7 +77,7 @@ class Route extends RouteBase {
child: new FadeTransition( child: new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: child child: builder(navigator, route)
) )
); );
} }
...@@ -88,7 +92,6 @@ class RouteState extends RouteBase { ...@@ -88,7 +92,6 @@ class RouteState extends RouteBase {
RouteBase route; RouteBase route;
StatefulComponent owner; StatefulComponent owner;
Widget build(Navigator navigator, RouteBase route) => null;
bool get isOpaque => false; bool get isOpaque => false;
void popState([dynamic result]) { void popState([dynamic result]) {
...@@ -97,23 +100,9 @@ class RouteState extends RouteBase { ...@@ -97,23 +100,9 @@ class RouteState extends RouteBase {
callback(this); callback(this);
} }
// Custom state routes shouldn't be asked to construct a transition bool get hasContent => false;
Duration get transitionDuration { Duration get transitionDuration => const Duration();
assert(false); Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) => null;
return const Duration();
}
TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }) {
assert(false);
return null;
}
}
class HistoryEntry {
HistoryEntry({ this.route });
final RouteBase route;
bool fullyOpaque = false;
// TODO(jackson): Keep track of the requested transition
String toString() => "HistoryEntry($route, hashCode=$hashCode)";
} }
class NavigationState { class NavigationState {
...@@ -123,14 +112,14 @@ class NavigationState { ...@@ -123,14 +112,14 @@ class NavigationState {
if (route.name != null) if (route.name != null)
namedRoutes[route.name] = route; namedRoutes[route.name] = route;
} }
history.add(new HistoryEntry(route: routes[0])); history.add(routes[0]);
} }
List<HistoryEntry> history = new List<HistoryEntry>(); List<RouteBase> history = new List<RouteBase>();
int historyIndex = 0; int historyIndex = 0;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>(); Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
RouteBase get currentRoute => history[historyIndex].route; RouteBase get currentRoute => history[historyIndex];
bool hasPrevious() => historyIndex > 0; bool hasPrevious() => historyIndex > 0;
void pushNamed(String name) { void pushNamed(String name) {
...@@ -141,22 +130,20 @@ class NavigationState { ...@@ -141,22 +130,20 @@ class NavigationState {
void push(RouteBase route) { void push(RouteBase route) {
assert(!_debugCurrentlyHaveRoute(route)); assert(!_debugCurrentlyHaveRoute(route));
HistoryEntry historyEntry = new HistoryEntry(route: route); history.insert(historyIndex + 1, route);
history.insert(historyIndex + 1, historyEntry);
historyIndex++; historyIndex++;
} }
void pop([dynamic result]) { void pop([dynamic result]) {
if (historyIndex > 0) { if (historyIndex > 0) {
HistoryEntry entry = history[historyIndex]; RouteBase route = history[historyIndex];
entry.route.popState(result); route.popState(result);
entry.fullyOpaque = false;
historyIndex--; historyIndex--;
} }
} }
bool _debugCurrentlyHaveRoute(RouteBase route) { bool _debugCurrentlyHaveRoute(RouteBase route) {
return history.any((entry) => entry.route == route); return history.any((candidate) => candidate == route);
} }
} }
...@@ -201,39 +188,25 @@ class Navigator extends StatefulComponent { ...@@ -201,39 +188,25 @@ class Navigator extends StatefulComponent {
Widget build() { Widget build() {
List<Widget> visibleRoutes = new List<Widget>(); List<Widget> visibleRoutes = new List<Widget>();
for (int i = 0; i < state.history.length; i++) { for (int i = state.history.length-1; i >= 0; i -= 1) {
// Avoid building routes that are not visible RouteBase route = state.history[i];
if (i + 1 < state.history.length && state.history[i + 1].fullyOpaque) if (!route.hasContent)
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; continue;
WatchableAnimationPerformance performance = historyEntry.route.ensurePerformance( WatchableAnimationPerformance performance = route.ensurePerformance(
direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse
); );
historyEntry.route.onDismissed = () { route.onDismissed = () {
setState(() { setState(() {
assert(state.history.contains(historyEntry)); assert(state.history.contains(route));
state.history.remove(historyEntry); state.history.remove(route);
}); });
}; };
historyEntry.route.onCompleted = () { Key key = new ObjectKey(route);
setState(() { Widget widget = route.build(key, this, route, performance);
historyEntry.fullyOpaque = historyEntry.route.isOpaque; visibleRoutes.add(widget);
}); if (route.isActuallyOpaque)
}; break;
TransitionBase transition = historyEntry.route.buildTransition(
key: new ObjectKey(historyEntry),
child: child,
performance: performance
);
visibleRoutes.add(transition);
} }
return new Focus(child: new Stack(visibleRoutes)); return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
} }
} }
...@@ -15,7 +15,7 @@ import 'package:sky/src/widgets/scrollable.dart'; ...@@ -15,7 +15,7 @@ import 'package:sky/src/widgets/scrollable.dart';
import 'package:sky/src/widgets/transitions.dart'; import 'package:sky/src/widgets/transitions.dart';
const Duration _kMenuDuration = const Duration(milliseconds: 300); const Duration _kMenuDuration = const Duration(milliseconds: 300);
double _kMenuCloseIntervalEnd = 2.0 / 3.0; const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
const double _kMenuWidthStep = 56.0; const double _kMenuWidthStep = 56.0;
const double _kMenuMargin = 16.0; // 24.0 on tablet const double _kMenuMargin = 16.0; // 24.0 on tablet
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment