提交 e56f8322 编写于 作者: H 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.
上级 ca9a7d7d
......@@ -30,9 +30,9 @@ abstract class AnimatedVariable {
/// can be made to take longer in one direction that the other.
class AnimationTiming {
AnimationTiming({
this.interval,
this.interval: const Interval(0.0, 1.0),
this.reverseInterval,
this.curve,
this.curve: linear,
this.reverseCurve
});
......
......@@ -27,14 +27,9 @@ class Linear implements Curve {
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 {
Interval(this.start, this.end) {
assert(start >= 0.0);
assert(start <= 1.0);
assert(end >= 0.0);
assert(end <= 1.0);
}
const Interval(this.start, this.end);
/// The smallest value for which this interval is 0.0
final double start;
......@@ -43,6 +38,10 @@ class Interval implements Curve {
final double end;
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);
}
}
......
......@@ -139,21 +139,19 @@ class DialogRoute extends RouteBase {
final Completer completer;
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;
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(
performance: performance,
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) {
......
......@@ -13,40 +13,44 @@ typedef Widget RouteBuilder(Navigator navigator, RouteBase route);
typedef void NotificationCallback();
abstract class RouteBase {
Widget build(Navigator navigator, RouteBase route);
bool get isOpaque;
void popState([dynamic result]) { assert(result == null); }
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 = new AnimationPerformance(duration: transitionDuration);
_performance.addStatusListener((AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
if (onDismissed != null)
onDismissed();
break;
case AnimationStatus.completed:
if (onCompleted != null)
onCompleted();
break;
default:
;
}
});
}
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;
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()';
}
......@@ -59,11 +63,11 @@ class Route extends RouteBase {
final String name;
final RouteBuilder builder;
Widget build(Navigator navigator, RouteBase route) => builder(navigator, route);
bool get isOpaque => true;
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): Block input unless content is interactive
return new SlideTransition(
......@@ -73,7 +77,7 @@ class Route extends RouteBase {
child: new FadeTransition(
performance: performance,
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 {
RouteBase route;
StatefulComponent owner;
Widget build(Navigator navigator, RouteBase route) => null;
bool get isOpaque => false;
void popState([dynamic result]) {
......@@ -97,23 +100,9 @@ class RouteState extends RouteBase {
callback(this);
}
// Custom state routes shouldn't be asked to construct a transition
Duration get transitionDuration {
assert(false);
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)";
bool get hasContent => false;
Duration get transitionDuration => const Duration();
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) => null;
}
class NavigationState {
......@@ -123,14 +112,14 @@ class NavigationState {
if (route.name != null)
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;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
RouteBase get currentRoute => history[historyIndex].route;
RouteBase get currentRoute => history[historyIndex];
bool hasPrevious() => historyIndex > 0;
void pushNamed(String name) {
......@@ -141,22 +130,20 @@ class NavigationState {
void push(RouteBase route) {
assert(!_debugCurrentlyHaveRoute(route));
HistoryEntry historyEntry = new HistoryEntry(route: route);
history.insert(historyIndex + 1, historyEntry);
history.insert(historyIndex + 1, route);
historyIndex++;
}
void pop([dynamic result]) {
if (historyIndex > 0) {
HistoryEntry entry = history[historyIndex];
entry.route.popState(result);
entry.fullyOpaque = false;
RouteBase route = history[historyIndex];
route.popState(result);
historyIndex--;
}
}
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 {
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);
for (int i = state.history.length-1; i >= 0; i -= 1) {
RouteBase route = state.history[i];
if (!route.hasContent)
continue;
}
if (child == null)
continue;
WatchableAnimationPerformance performance = historyEntry.route.ensurePerformance(
WatchableAnimationPerformance performance = route.ensurePerformance(
direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse
);
historyEntry.route.onDismissed = () {
route.onDismissed = () {
setState(() {
assert(state.history.contains(historyEntry));
state.history.remove(historyEntry);
assert(state.history.contains(route));
state.history.remove(route);
});
};
historyEntry.route.onCompleted = () {
setState(() {
historyEntry.fullyOpaque = historyEntry.route.isOpaque;
});
};
TransitionBase transition = historyEntry.route.buildTransition(
key: new ObjectKey(historyEntry),
child: child,
performance: performance
);
visibleRoutes.add(transition);
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));
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
}
}
......@@ -15,7 +15,7 @@ import 'package:sky/src/widgets/scrollable.dart';
import 'package:sky/src/widgets/transitions.dart';
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 _kMenuMargin = 16.0; // 24.0 on tablet
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册