From e73bbd949e6b8fd64805e24e6e9409def44965e7 Mon Sep 17 00:00:00 2001 From: Hixie Date: Mon, 14 Sep 2015 15:02:45 -0700 Subject: [PATCH] Require that you pass transitions a performance. This fixes #1103. --- examples/stocks/lib/stock_home.dart | 12 +- examples/stocks/lib/stock_list.dart | 2 +- examples/widgets/progress_indicator.dart | 42 ++-- .../lib/src/animation/animated_value.dart | 5 +- .../src/animation/animation_performance.dart | 47 +++- .../lib/src/widgets/animated_component.dart | 74 +++--- packages/flutter/lib/src/widgets/dialog.dart | 24 +- .../flutter/lib/src/widgets/dismissable.dart | 13 +- packages/flutter/lib/src/widgets/drawer.dart | 27 +- .../lib/src/widgets/global_key_watcher.dart | 21 +- .../flutter/lib/src/widgets/navigator.dart | 137 ++++++---- .../flutter/lib/src/widgets/popup_menu.dart | 25 +- .../lib/src/widgets/progress_indicator.dart | 27 +- .../flutter/lib/src/widgets/snack_bar.dart | 42 ++-- packages/flutter/lib/src/widgets/tabs.dart | 3 +- .../flutter/lib/src/widgets/transitions.dart | 237 +++++++----------- 16 files changed, 384 insertions(+), 354 deletions(-) diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 57914dbb774..0cecb05a018 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -251,13 +251,13 @@ class StockHome extends StatefulComponent { }); } - Anchor _snackBarAnchor = new Anchor(); + GlobalKey snackBarKey = new GlobalKey(label: 'snackbar'); Widget buildSnackBar() { if (_snackBarStatus == AnimationStatus.dismissed) return null; return new SnackBar( + transitionKey: snackBarKey, showing: _isSnackBarShowing, - anchor: _snackBarAnchor, content: new Text("Stock purchased!"), actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); } @@ -272,12 +272,14 @@ class StockHome extends StatefulComponent { } Widget buildFloatingActionButton() { - return _snackBarAnchor.build( - new FloatingActionButton( + return new TransitionProxy( + transitionKey: snackBarKey, + child: new FloatingActionButton( child: new Icon(type: 'content/add', size: 24), backgroundColor: Colors.redAccent[200], onPressed: _handleStockPurchased - )); + ) + ); } void addMenuToOverlays(List overlays) { diff --git a/examples/stocks/lib/stock_list.dart b/examples/stocks/lib/stock_list.dart index 516564c4ef3..42ea3842b99 100644 --- a/examples/stocks/lib/stock_list.dart +++ b/examples/stocks/lib/stock_list.dart @@ -15,7 +15,7 @@ class Stocklist extends Component { child: new ScrollableList( items: stocks, itemExtent: StockRow.kHeight, - itemBuilder: (stock) => new StockRow(stock: stock) + itemBuilder: (Stock stock) => new StockRow(stock: stock) ) ); } diff --git a/examples/widgets/progress_indicator.dart b/examples/widgets/progress_indicator.dart index d3f42a760a8..e455a72b23b 100644 --- a/examples/widgets/progress_indicator.dart +++ b/examples/widgets/progress_indicator.dart @@ -22,21 +22,28 @@ class ProgressIndicatorApp extends App { reverseCurve: ease, interval: new Interval(0.0, 0.9) ); + valueAnimation.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed || status == AnimationStatus.completed) + reverseValueAnimationDirection(); + }); + valueAnimation.play(valueAnimationDirection); } void handleTap() { - if (valueAnimation.isAnimating) - valueAnimation.stop(); - else - valueAnimation.resume(); + setState(() { + // valueAnimation.isAnimating is part of our build state + if (valueAnimation.isAnimating) + valueAnimation.stop(); + else + valueAnimation.resume(); + }); } void reverseValueAnimationDirection() { - setState(() { - valueAnimationDirection = (valueAnimationDirection == Direction.forward) - ? Direction.reverse - : Direction.forward; - }); + valueAnimationDirection = (valueAnimationDirection == Direction.forward) + ? Direction.reverse + : Direction.forward; + valueAnimation.play(valueAnimationDirection); } Widget buildIndicators() { @@ -58,11 +65,12 @@ class ProgressIndicatorApp extends App { width: 50.0, height: 30.0, child: new CircularProgressIndicator(value: valueAnimation.value) - ) + ), + new Text("${(valueAnimation.value * 100.0).toStringAsFixed(1)}%" + (valueAnimation.isAnimating ? '' : ' (paused)')) ]; return new Column( indicators - .map((c) => new Container(child: c, margin: const EdgeDims.symmetric(vertical: 20.0))) + .map((c) => new Container(child: c, margin: const EdgeDims.symmetric(vertical: 15.0, horizontal: 20.0))) .toList(), justifyContent: FlexJustifyContent.center ); @@ -76,10 +84,7 @@ class ProgressIndicatorApp extends App { decoration: new BoxDecoration(backgroundColor: Theme.of(this).cardColor), child: new BuilderTransition( variables: [valueAnimation.variable], - direction: valueAnimationDirection, - performance: valueAnimation, - onDismissed: reverseValueAnimationDirection, - onCompleted: reverseValueAnimationDirection, + performance: valueAnimation.view, builder: buildIndicators ) ) @@ -94,10 +99,13 @@ class ProgressIndicatorApp extends App { accentColor: Colors.redAccent[200] ), child: new Title( - title: 'Cards', + title: 'Progress Indicators', child: new Scaffold( toolbar: new ToolBar(center: new Text('Progress Indicators')), - body: body + body: new DefaultTextStyle( + style: Theme.of(this).text.title, + child: body + ) ) ) ) diff --git a/packages/flutter/lib/src/animation/animated_value.dart b/packages/flutter/lib/src/animation/animated_value.dart index ff1fba65046..8d4ed9f57a8 100644 --- a/packages/flutter/lib/src/animation/animated_value.dart +++ b/packages/flutter/lib/src/animation/animated_value.dart @@ -15,7 +15,10 @@ enum Direction { reverse } -/// A variable that changes as an animation progresses +/// An interface describing a variable that changes as an animation progresses. +/// +/// AnimatedVariables, by convention, must be cheap to create. This allows them to be used in +/// build functions in Widgets. abstract class AnimatedVariable { /// Update the variable to a given time in an animation that is running in the given direction void setProgress(double t, Direction direction); diff --git a/packages/flutter/lib/src/animation/animation_performance.dart b/packages/flutter/lib/src/animation/animation_performance.dart index 69745e87279..9bb46b9c770 100644 --- a/packages/flutter/lib/src/animation/animation_performance.dart +++ b/packages/flutter/lib/src/animation/animation_performance.dart @@ -23,6 +23,26 @@ enum AnimationStatus { completed, } +typedef void AnimationPerformanceListener(); +typedef void AnimationPerformanceStatusListener(AnimationStatus status); + +/// An interface that is implemented by [AnimationPerformance] that exposes a +/// read-only view of the underlying performance. This is used by classes that +/// want to watch a performance but should not be able to change the +/// performance's state. +abstract class WatchableAnimationPerformance { + /// Update the given variable according to the current progress of the performance + void updateVariable(AnimatedVariable variable); + /// Calls the listener every time the progress of the performance changes + void addListener(AnimationPerformanceListener listener); + /// Stop calling the listener every time the progress of the performance changes + void removeListener(AnimationPerformanceListener listener); + /// Calls listener every time the status of the performance changes + void addStatusListener(AnimationPerformanceStatusListener listener); + /// Stops calling the listener every time the status of the performance changes + void removeStatusListener(AnimationPerformanceStatusListener listener); +} + /// A collection of values that animated based on a timeline /// /// For example, a performance may handle an animation of a menu opening by @@ -31,12 +51,17 @@ enum AnimationStatus { /// may also take direct control of the timeline by manipulating [progress], or /// [fling] the timeline causing a physics-based simulation to take over the /// progression. -class AnimationPerformance { +class AnimationPerformance implements WatchableAnimationPerformance { AnimationPerformance({ AnimatedVariable variable, this.duration }) : _variable = variable { _timeline = new Timeline(_tick); } + /// Returns a [WatchableAnimationPerformance] for this performance, + /// so that a pointer to this object can be passed around without + /// allowing users of that pointer to mutate the AnimationPerformance state. + WatchableAnimationPerformance get view => this; + /// The length of time this performance should last Duration duration; @@ -155,33 +180,33 @@ class AnimationPerformance { return _timeline.fling(force.release(progress, velocity)); } - final List _listeners = new List(); + final List _listeners = new List(); /// Calls the listener every time the progress of this performance changes - void addListener(Function listener) { + void addListener(AnimationPerformanceListener listener) { _listeners.add(listener); } /// Stop calling the listener every time the progress of this performance changes - void removeListener(Function listener) { + void removeListener(AnimationPerformanceListener listener) { _listeners.remove(listener); } void _notifyListeners() { - List localListeners = new List.from(_listeners); - for (Function listener in localListeners) + List localListeners = new List.from(_listeners); + for (AnimationPerformanceListener listener in localListeners) listener(); } - final List _statusListeners = new List(); + final List _statusListeners = new List(); /// Calls listener every time the status of this performance changes - void addStatusListener(Function listener) { + void addStatusListener(AnimationPerformanceStatusListener listener) { _statusListeners.add(listener); } /// Stops calling the listener every time the status of this performance changes - void removeStatusListener(Function listener) { + void removeStatusListener(AnimationPerformanceStatusListener listener) { _statusListeners.remove(listener); } @@ -189,8 +214,8 @@ class AnimationPerformance { void _checkStatusChanged() { AnimationStatus currentStatus = status; if (currentStatus != _lastStatus) { - List localListeners = new List.from(_statusListeners); - for (Function listener in localListeners) + List localListeners = new List.from(_statusListeners); + for (AnimationPerformanceStatusListener listener in localListeners) listener(currentStatus); } _lastStatus = currentStatus; diff --git a/packages/flutter/lib/src/widgets/animated_component.dart b/packages/flutter/lib/src/widgets/animated_component.dart index 0a84bd25de4..f44a50ec2e1 100644 --- a/packages/flutter/lib/src/widgets/animated_component.dart +++ b/packages/flutter/lib/src/widgets/animated_component.dart @@ -7,47 +7,49 @@ import 'package:sky/src/widgets/framework.dart'; abstract class AnimatedComponent extends StatefulComponent { - AnimatedComponent({ Key key }) : super(key: key); + AnimatedComponent({ Key key, this.direction, this.duration }) : super(key: key); - void syncConstructorArguments(AnimatedComponent source) { } + Duration duration; + Direction direction; - final List _watchedPerformances = new List(); + void syncConstructorArguments(AnimatedComponent source) { + bool resumePerformance = false; + if (duration != source.duration) { + duration = source.duration; + resumePerformance = true; + } + if (direction != source.direction) { + direction = source.direction; + resumePerformance = true; + } + if (resumePerformance) + performance.play(direction); + } - void _performanceChanged() { - setState(() { - // We don't actually have any state to change, per se, - // we just know that we have in fact changed state. + AnimationPerformance get performance => _performance; + AnimationPerformance _performance; + + void initState() { + _performance = new AnimationPerformance(duration: duration); + performance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) + handleCompleted(); + else if (status == AnimationStatus.dismissed) + handleDismissed(); }); + if (buildDependsOnPerformance) { + performance.addListener(() { + setState(() { + // We don't actually have any state to change, per se, + // we just know that we have in fact changed state. + }); + }); + } + performance.play(direction); } - bool isWatching(AnimationPerformance performance) { - return _watchedPerformances.contains(performance); - } - - void watch(AnimationPerformance performance) { - assert(!isWatching(performance)); - _watchedPerformances.add(performance); - if (mounted) - performance.addListener(_performanceChanged); - } - - void unwatch(AnimationPerformance performance) { - assert(isWatching(performance)); - _watchedPerformances.remove(performance); - if (mounted) - performance.removeListener(_performanceChanged); - } - - void didMount() { - for (AnimationPerformance performance in _watchedPerformances) - performance.addListener(_performanceChanged); - super.didMount(); - } - - void didUnmount() { - for (AnimationPerformance performance in _watchedPerformances) - performance.removeListener(_performanceChanged); - super.didUnmount(); - } + bool get buildDependsOnPerformance => false; + void handleCompleted() { } + void handleDismissed() { } } diff --git a/packages/flutter/lib/src/widgets/dialog.dart b/packages/flutter/lib/src/widgets/dialog.dart index 89150c4bbd2..7d16f54bb44 100644 --- a/packages/flutter/lib/src/widgets/dialog.dart +++ b/packages/flutter/lib/src/widgets/dialog.dart @@ -131,6 +131,8 @@ class Dialog extends Component { } } +const Duration _kTransitionDuration = const Duration(milliseconds: 150); + class DialogRoute extends RouteBase { DialogRoute({ this.completer, this.builder }); @@ -144,28 +146,10 @@ class DialogRoute extends RouteBase { completer.complete(result); } - TransitionBase buildTransition({ Key key }) => new DialogTransition(key: key); -} - -const Duration _kTransitionDuration = const Duration(milliseconds: 150); -class DialogTransition extends TransitionBase { - DialogTransition({ - 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) { + Duration get transitionDuration => _kTransitionDuration; + TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }) { return new FadeTransition( performance: performance, - direction: direction, opacity: new AnimatedValue(0.0, end: 1.0, curve: easeOut), child: child ); diff --git a/packages/flutter/lib/src/widgets/dismissable.dart b/packages/flutter/lib/src/widgets/dismissable.dart index 728c8c303f3..e7a0edbe33e 100644 --- a/packages/flutter/lib/src/widgets/dismissable.dart +++ b/packages/flutter/lib/src/widgets/dismissable.dart @@ -54,6 +54,10 @@ class Dismissable extends StatefulComponent { void initState() { _fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout); + _fadePerformance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) + _handleFadeCompleted(); + }); } void syncConstructorArguments(Dismissable source) { @@ -99,6 +103,7 @@ class Dismissable extends StatefulComponent { _resizePerformance = new AnimationPerformance() ..duration = _kCardDismissResize ..addListener(_handleResizeProgressChanged); + _resizePerformance.play(); }); } @@ -226,8 +231,7 @@ class Dismissable extends StatefulComponent { ); return new SquashTransition( - performance: _resizePerformance, - direction: Direction.forward, + performance: _resizePerformance.view, width: _directionIsYAxis ? squashAxisExtent : null, height: !_directionIsYAxis ? squashAxisExtent : null ); @@ -243,11 +247,10 @@ class Dismissable extends StatefulComponent { child: new SizeObserver( callback: _handleSizeChanged, child: new FadeTransition( - performance: _fadePerformance, - onCompleted: _handleFadeCompleted, + performance: _fadePerformance.view, opacity: new AnimatedValue(1.0, end: 0.0), child: new SlideTransition( - performance: _fadePerformance, + performance: _fadePerformance.view, position: new AnimatedValue(Point.origin, end: _activeCardDragEndPoint), child: child ) diff --git a/packages/flutter/lib/src/widgets/drawer.dart b/packages/flutter/lib/src/widgets/drawer.dart index b4715ecbf10..8801cd45f84 100644 --- a/packages/flutter/lib/src/widgets/drawer.dart +++ b/packages/flutter/lib/src/widgets/drawer.dart @@ -58,32 +58,41 @@ class Drawer extends StatefulComponent { void initState() { _performance = new AnimationPerformance(duration: _kBaseSettleDuration); - + _performance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) + _handleDismissed(); + }); // Use a spring force for animating the drawer. We can't use curves for // this because we need a linear curve in order to track the user's finger // while dragging. _performance.attachedForce = kDefaultSpringForce; - if (navigator != null) { + // TODO(ianh): This is crazy. We should convert drawer to use a pattern like openDialog(). + // https://github.com/domokit/sky_engine/pull/1186 scheduleMicrotask(() { navigator.pushState(this, (_) => _performance.reverse()); }); } + _performance.play(_direction); } + Direction get _direction => showing ? Direction.forward : Direction.reverse; + void syncConstructorArguments(Drawer source) { children = source.children; + if (showing != source.showing) { + showing = source.showing; + _performance.play(_direction); + } level = source.level; - navigator = source.navigator; - showing = source.showing; onDismissed = source.onDismissed; + navigator = source.navigator; } Widget build() { var mask = new GestureDetector( child: new ColorTransition( - performance: _performance, - direction: showing ? Direction.forward : Direction.reverse, + performance: _performance.view, color: new AnimatedColorValue(Colors.transparent, end: const Color(0x7F000000)), child: new Container() ), @@ -93,10 +102,8 @@ class Drawer extends StatefulComponent { ); Widget content = new SlideTransition( - performance: _performance, - direction: showing ? Direction.forward : Direction.reverse, + performance: _performance.view, position: new AnimatedValue(_kClosedPosition, end: _kOpenPosition), - onDismissed: _onDismissed, child: new AnimatedContainer( behavior: implicitlyAnimate(const Duration(milliseconds: 200)), decoration: new BoxDecoration( @@ -115,7 +122,7 @@ class Drawer extends StatefulComponent { ); } - void _onDismissed() { + void _handleDismissed() { if (navigator != null && navigator.currentRoute is RouteState && (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer diff --git a/packages/flutter/lib/src/widgets/global_key_watcher.dart b/packages/flutter/lib/src/widgets/global_key_watcher.dart index b3d06fe57f3..03c54035252 100644 --- a/packages/flutter/lib/src/widgets/global_key_watcher.dart +++ b/packages/flutter/lib/src/widgets/global_key_watcher.dart @@ -13,12 +13,14 @@ abstract class GlobalKeyWatcher extends StatefulComponent { GlobalKey watchedKey; void syncConstructorArguments(GlobalKeyWatcher source) { - if (source != source.watchedKey) { - _removeListeners(); + if (watchedKey != source.watchedKey) { + if (watchedKey != null) + _removeListeners(); watchedKey = source.watchedKey; - if (mounted) + if (mounted && watchedKey != null) { _setWatchedWidget(GlobalKey.getWidget(watchedKey)); - _addListeners(); + _addListeners(); + } } } @@ -29,7 +31,7 @@ abstract class GlobalKeyWatcher extends StatefulComponent { if (watchedWidget != value) { if (watchedWidget != null) stopWatching(); - assert(debugValidateWatchedWidget(value)); + assert(value == null || debugValidateWatchedWidget(value)); setState(() { _watchedWidget = value; }); @@ -42,13 +44,16 @@ abstract class GlobalKeyWatcher extends StatefulComponent { void didMount() { super.didMount(); - _setWatchedWidget(GlobalKey.getWidget(watchedKey)); - _addListeners(); + if (watchedKey != null) { + _setWatchedWidget(GlobalKey.getWidget(watchedKey)); + _addListeners(); + } } void didUnmount() { super.didUnmount(); - _removeListeners(); + if (watchedKey != null) + _removeListeners(); _setWatchedWidget(null); } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 07e97a7732c..d432b015b98 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -10,13 +10,49 @@ import 'package:sky/src/widgets/transitions.dart'; 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); } - TransitionBase buildTransition({ Key key }); + + AnimationPerformance _performance; + NotificationCallback onDismissed; + NotificationCallback onCompleted; + 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: + ; + } + }); + } + AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse; + if (_performance.status != desiredStatus) + _performance.play(direction); + return _performance.view; + } + + Duration get transitionDuration; + TransitionBase buildTransition({ Key key, Widget child, WatchableAnimationPerformance performance }); + + 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 }); @@ -25,7 +61,24 @@ class Route extends RouteBase { Widget build(Navigator navigator, RouteBase route) => builder(navigator, route); bool get isOpaque => true; - TransitionBase buildTransition({ Key key }) => new SlideUpFadeTransition(key: key); + + Duration get transitionDuration => _kTransitionDuration; + TransitionBase buildTransition({ Key key, Widget child, 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(_kTransitionStartPoint, end: Point.origin, curve: easeOut), + child: new FadeTransition( + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: easeOut), + child: child + ) + ); + } + + String toString() => '$runtimeType(name="$name")'; } class RouteState extends RouteBase { @@ -44,52 +97,23 @@ class RouteState extends RouteBase { callback(this); } - TransitionBase buildTransition({ Key key }) { - // Custom state routes shouldn't be asked to construct a transition + // 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; } } -// 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(_kTransitionStartPoint, end: Point.origin, curve: easeOut), - child: new FadeTransition( - performance: performance, - direction: direction, - opacity: new AnimatedValue(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 + String toString() => "HistoryEntry($route, hashCode=$hashCode)"; } class NavigationState { @@ -116,6 +140,7 @@ class NavigationState { } void push(RouteBase route) { + assert(!_debugCurrentlyHaveRoute(route)); HistoryEntry historyEntry = new HistoryEntry(route: route); history.insert(historyIndex + 1, historyEntry); historyIndex++; @@ -129,6 +154,10 @@ class NavigationState { historyIndex--; } } + + bool _debugCurrentlyHaveRoute(RouteBase route) { + return history.any((entry) => entry.route == route); + } } class Navigator extends StatefulComponent { @@ -184,19 +213,25 @@ class Navigator extends StatefulComponent { } if (child == null) continue; - TransitionBase transition = historyEntry.route.buildTransition(key: new ObjectKey(historyEntry)) - ..child = child - ..direction = (i <= state.historyIndex) ? Direction.forward : Direction.reverse - ..onDismissed = () { - setState(() { - state.history.remove(historyEntry); - }); - } - ..onCompleted = () { - setState(() { - historyEntry.fullyOpaque = historyEntry.route.isOpaque; - }); - }; + WatchableAnimationPerformance performance = historyEntry.route.ensurePerformance( + direction: (i <= state.historyIndex) ? Direction.forward : Direction.reverse + ); + historyEntry.route.onDismissed = () { + setState(() { + assert(state.history.contains(historyEntry)); + state.history.remove(historyEntry); + }); + }; + 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); } return new Focus(child: new Stack(visibleRoutes)); diff --git a/packages/flutter/lib/src/widgets/popup_menu.dart b/packages/flutter/lib/src/widgets/popup_menu.dart index eb45d2c5230..71186a7255a 100644 --- a/packages/flutter/lib/src/widgets/popup_menu.dart +++ b/packages/flutter/lib/src/widgets/popup_menu.dart @@ -45,10 +45,13 @@ class PopupMenu extends StatefulComponent { AnimationPerformance _performance; void initState() { - _performance = new AnimationPerformance() - ..duration = _kMenuDuration; + _performance = new AnimationPerformance(duration: _kMenuDuration); _performance.timing = new AnimationTiming() - ..reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd); + ..reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd); + _performance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) + _handleDismissed(); + }); _updateBoxPainter(); if (showing) @@ -69,6 +72,7 @@ class PopupMenu extends StatefulComponent { void _open() { navigator.pushState(this, (_) => _close()); + _performance.play(); } void _close() { @@ -82,7 +86,7 @@ class PopupMenu extends StatefulComponent { boxShadow: shadows[level])); } - void _onDismissed() { + void _handleDismissed() { if (navigator != null && navigator.currentRoute is RouteState && (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer @@ -100,24 +104,21 @@ class PopupMenu extends StatefulComponent { double start = (i + 1) * unit; double end = (start + 1.5 * unit).clamp(0.0, 1.0); children.add(new FadeTransition( - direction: showing ? Direction.forward : Direction.reverse, - performance: _performance, + performance: _performance.view, opacity: new AnimatedValue(0.0, end: 1.0, interval: new Interval(start, end)), - child: items[i])); + child: items[i]) + ); } final width = new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, unit)); final height = new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, unit * items.length)); return new FadeTransition( - direction: showing ? Direction.forward : Direction.reverse, - performance: _performance, - onDismissed: _onDismissed, + performance: _performance.view, opacity: new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)), child: new Container( margin: new EdgeDims.all(_kMenuMargin), child: new BuilderTransition( - direction: showing ? Direction.forward : Direction.reverse, - performance: _performance, + performance: _performance.view, variables: [width, height], builder: () { return new CustomPaint( diff --git a/packages/flutter/lib/src/widgets/progress_indicator.dart b/packages/flutter/lib/src/widgets/progress_indicator.dart index 6a7b683411f..9b225ac15e5 100644 --- a/packages/flutter/lib/src/widgets/progress_indicator.dart +++ b/packages/flutter/lib/src/widgets/progress_indicator.dart @@ -25,16 +25,21 @@ abstract class ProgressIndicator extends StatefulComponent { double value; // Null for non-determinate progress indicator. double bufferValue; // TODO(hansmuller) implement the support for this. - AnimationPerformance _animation; - double get _animationValue => (_animation.variable as AnimatedValue).value; + AnimationPerformance _performance; + double get _performanceValue => (_performance.variable as AnimatedValue).value; Color get _backgroundColor => Theme.of(this).primarySwatch[200]; Color get _valueColor => Theme.of(this).primaryColor; - Object get _customPaintToken => value != null ? value : _animationValue; + Object get _customPaintToken => value != null ? value : _performanceValue; void initState() { - _animation = new AnimationPerformance() + _performance = new AnimationPerformance() ..duration = const Duration(milliseconds: 1500) ..variable = new AnimatedValue(0.0, end: 1.0, curve: ease); + _performance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) + _restartAnimation(); + }); + _performance.play(); } void syncConstructorArguments(ProgressIndicator source) { @@ -43,8 +48,8 @@ abstract class ProgressIndicator extends StatefulComponent { } void _restartAnimation() { - _animation.progress = 0.0; - _animation.play(); + _performance.progress = 0.0; + _performance.play(); } Widget build() { @@ -52,10 +57,8 @@ abstract class ProgressIndicator extends StatefulComponent { return _buildIndicator(); return new BuilderTransition( - variables: [_animation.variable], - direction: Direction.forward, - performance: _animation, - onCompleted: _restartAnimation, + variables: [_performance.variable], + performance: _performance.view, builder: _buildIndicator ); } @@ -81,7 +84,7 @@ class LinearProgressIndicator extends ProgressIndicator { double width = value.clamp(0.0, 1.0) * size.width; canvas.drawRect(Point.origin & new Size(width, size.height), paint); } else { - double startX = size.width * (1.5 * _animationValue - 0.5); + double startX = size.width * (1.5 * _performanceValue - 0.5); double endX = startX + 0.5 * size.width; double x = startX.clamp(0.0, size.width); double width = endX.clamp(0.0, size.width) - x; @@ -125,7 +128,7 @@ class CircularProgressIndicator extends ProgressIndicator { ..arcTo(Point.origin & size, _kStartAngle, angle, false); canvas.drawPath(path, paint); } else { - double startAngle = _kTwoPI * (1.75 * _animationValue - 0.75); + double startAngle = _kTwoPI * (1.75 * _performanceValue - 0.75); double endAngle = startAngle + _kTwoPI * 0.75; double arcAngle = startAngle.clamp(0.0, _kTwoPI); double arcSweep = endAngle.clamp(0.0, _kTwoPI) - arcAngle; diff --git a/packages/flutter/lib/src/widgets/snack_bar.dart b/packages/flutter/lib/src/widgets/snack_bar.dart index 9e3478aadc2..361fdb3ee99 100644 --- a/packages/flutter/lib/src/widgets/snack_bar.dart +++ b/packages/flutter/lib/src/widgets/snack_bar.dart @@ -6,6 +6,7 @@ import 'package:sky/animation.dart'; import 'package:sky/painting.dart'; import 'package:sky/material.dart'; +import 'package:sky/src/widgets/animated_component.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/default_text_style.dart'; import 'package:sky/src/widgets/framework.dart'; @@ -17,6 +18,7 @@ import 'package:sky/src/widgets/transitions.dart'; typedef void SnackBarDismissedCallback(); const Duration _kSlideInDuration = const Duration(milliseconds: 200); +// TODO(ianh): factor out some of the constants below class SnackBarAction extends Component { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { @@ -38,25 +40,37 @@ class SnackBarAction extends Component { } } -class SnackBar extends Component { +class SnackBar extends AnimatedComponent { SnackBar({ Key key, - this.anchor, + this.transitionKey, this.content, this.actions, - this.showing, + bool showing, this.onDismissed - }) : super(key: key) { + }) : super(key: key, direction: showing ? Direction.forward : Direction.reverse, duration: _kSlideInDuration) { assert(content != null); } - Anchor anchor; + Key transitionKey; Widget content; List actions; - bool showing; SnackBarDismissedCallback onDismissed; + void syncConstructorArguments(SnackBar source) { + transitionKey = source.transitionKey; + content = source.content; + actions = source.actions; + onDismissed = source.onDismissed; + super.syncConstructorArguments(source); + } + + void handleDismissed() { + if (onDismissed != null) + onDismissed(); + } + Widget build() { List children = [ new Flexible( @@ -71,15 +85,15 @@ class SnackBar extends Component { ]; if (actions != null) children.addAll(actions); - return new SlideTransition( - duration: _kSlideInDuration, - direction: showing ? Direction.forward : Direction.reverse, - position: new AnimatedValue(Point.origin, - end: const Point(0.0, -52.0), - curve: easeIn, reverseCurve: easeOut), - onDismissed: onDismissed, - anchor: anchor, + key: transitionKey, + performance: performance.view, + position: new AnimatedValue( + Point.origin, + end: const Point(0.0, -52.0), + curve: easeIn, + reverseCurve: easeOut + ), child: new Material( level: 2, color: const Color(0xFF323232), diff --git a/packages/flutter/lib/src/widgets/tabs.dart b/packages/flutter/lib/src/widgets/tabs.dart index 59c6a369bf3..07c937ec9a9 100644 --- a/packages/flutter/lib/src/widgets/tabs.dart +++ b/packages/flutter/lib/src/widgets/tabs.dart @@ -542,8 +542,7 @@ class TabBar extends Scrollable { style: textStyle, child: new BuilderTransition( variables: [_indicatorRect], - direction: Direction.forward, - performance: _indicatorAnimation, + performance: _indicatorAnimation.view, builder: () { return new TabBarWrapper( children: tabs, diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index e2e8ff7699c..59e8e33a9b3 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -6,144 +6,125 @@ import 'package:sky/animation.dart'; import 'package:sky/src/widgets/animated_component.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; +import 'package:sky/src/widgets/global_key_watcher.dart'; import 'package:vector_math/vector_math.dart'; export 'package:sky/animation.dart' show Direction; -// A helper class to anchor widgets to one another. Pass an instance of this to -// a Transition, then use the build() method to create a child with the same -// transition applied. -class Anchor { - Anchor(); +class TransitionProxy extends GlobalKeyWatcher { - TransitionBase transition; - - Widget build(Widget child) { - return new _AnchorTransition(anchoredTo: this, child: child); - } -} - -// Used with the Anchor class to apply a transition to multiple children. -class _AnchorTransition extends AnimatedComponent { - _AnchorTransition({ + TransitionProxy({ Key key, - this.anchoredTo, + GlobalKey transitionKey, this.child - }) : super(key: key); + }) : super(key: key, watchedKey: transitionKey); - Anchor anchoredTo; Widget child; - TransitionBase get transition => anchoredTo.transition; - void initState() { - if (transition != null) - watch(transition.performance); - } - - void syncConstructorArguments(_AnchorTransition source) { - if (transition != null && isWatching(transition.performance)) - unwatch(transition.performance); - anchoredTo = source.anchoredTo; - if (transition != null) - watch(transition.performance); + void syncConstructorArguments(TransitionProxy source) { child = source.child; super.syncConstructorArguments(source); } + bool debugValidateWatchedWidget(Widget candidate) { + return candidate is TransitionBaseWithChild; + } + + TransitionBaseWithChild get transition => this.watchedWidget; + + void startWatching() { + transition.performance.addListener(_performanceChanged); + } + + void stopWatching() { + transition.performance.removeListener(_performanceChanged); + } + + void _performanceChanged() { + setState(() { + // The performance changed, so we probably need to ask the transition + // we're watching for a rebuild. + }); + } + Widget build() { - if (transition == null) - return child; - return transition.buildWithChild(child); + if (transition != null) + return transition.buildWithChild(child); + return child; } + } -abstract class TransitionBase extends AnimatedComponent { +abstract class TransitionBase extends StatefulComponent { + TransitionBase({ Key key, - this.child, - this.anchor, - this.direction, - this.duration, - this.performance, - this.onDismissed, - this.onCompleted - }) : super(key: key); - - Widget child; - Anchor anchor; - Direction direction; - Duration duration; - AnimationPerformance performance; - Function onDismissed; - Function onCompleted; - - void initState() { - if (anchor != null) - anchor.transition = this; - - if (performance == null) { - assert(duration != null); - performance = new AnimationPerformance(duration: duration); - if (direction == Direction.reverse) - performance.progress = 1.0; - } - performance.addStatusListener(_checkStatusChanged); - - watch(performance); - _start(); + this.performance + }) : super(key: key) { + assert(performance != null); } + WatchableAnimationPerformance performance; + void syncConstructorArguments(TransitionBase source) { + if (performance != source.performance) { + if (mounted) + performance.removeListener(_performanceChanged); + performance = source.performance; + if (mounted) + performance.addListener(_performanceChanged); + } + } + + void _performanceChanged() { + setState(() { + // The performance's state is our build state, and it changed already. + }); + } + + void didMount() { + performance.addListener(_performanceChanged); + super.didMount(); + } + + void didUnmount() { + performance.removeListener(_performanceChanged); + super.didUnmount(); + } + +} + +abstract class TransitionBaseWithChild extends TransitionBase { + + TransitionBaseWithChild({ + Key key, + this.child, + WatchableAnimationPerformance performance + }) : super(key: key, performance: performance); + + Widget child; + + void syncConstructorArguments(TransitionBaseWithChild source) { child = source.child; - onCompleted = source.onCompleted; - onDismissed = source.onDismissed; - duration = source.duration; - if (direction != source.direction) { - direction = source.direction; - _start(); - } super.syncConstructorArguments(source); } - void _start() { - performance.play(direction); - } - - void _checkStatusChanged(AnimationStatus status) { - if (performance.isDismissed) { - if (onDismissed != null) - onDismissed(); - } else if (performance.isCompleted) { - if (onCompleted != null) - onCompleted(); - } - } - Widget build() { return buildWithChild(child); } Widget buildWithChild(Widget child); + } -class SlideTransition extends TransitionBase { +class SlideTransition extends TransitionBaseWithChild { SlideTransition({ Key key, - Anchor anchor, this.position, - Duration duration, - AnimationPerformance performance, - Direction direction, - Function onDismissed, - Function onCompleted, + WatchableAnimationPerformance performance, Widget child }) : super(key: key, - anchor: anchor, - duration: duration, performance: performance, - direction: direction, - onDismissed: onDismissed, - onCompleted: onCompleted, child: child); AnimatedValue position; @@ -161,24 +142,14 @@ class SlideTransition extends TransitionBase { } } -class FadeTransition extends TransitionBase { +class FadeTransition extends TransitionBaseWithChild { FadeTransition({ Key key, - Anchor anchor, this.opacity, - Duration duration, - AnimationPerformance performance, - Direction direction, - Function onDismissed, - Function onCompleted, + WatchableAnimationPerformance performance, Widget child }) : super(key: key, - anchor: anchor, - duration: duration, performance: performance, - direction: direction, - onDismissed: onDismissed, - onCompleted: onCompleted, child: child); AnimatedValue opacity; @@ -194,24 +165,14 @@ class FadeTransition extends TransitionBase { } } -class ColorTransition extends TransitionBase { +class ColorTransition extends TransitionBaseWithChild { ColorTransition({ Key key, - Anchor anchor, this.color, - Duration duration, - AnimationPerformance performance, - Direction direction, - Function onDismissed, - Function onCompleted, + WatchableAnimationPerformance performance, Widget child }) : super(key: key, - anchor: anchor, - duration: duration, performance: performance, - direction: direction, - onDismissed: onDismissed, - onCompleted: onCompleted, child: child); AnimatedColorValue color; @@ -230,25 +191,15 @@ class ColorTransition extends TransitionBase { } } -class SquashTransition extends TransitionBase { +class SquashTransition extends TransitionBaseWithChild { SquashTransition({ Key key, - Anchor anchor, this.width, this.height, - Duration duration, - AnimationPerformance performance, - Direction direction, - Function onDismissed, - Function onCompleted, + WatchableAnimationPerformance performance, Widget child }) : super(key: key, - anchor: anchor, - duration: duration, performance: performance, - direction: direction, - onDismissed: onDismissed, - onCompleted: onCompleted, child: child); AnimatedValue width; @@ -274,23 +225,11 @@ typedef Widget BuilderFunction(); class BuilderTransition extends TransitionBase { BuilderTransition({ Key key, - Anchor anchor, this.variables, this.builder, - Duration duration, - AnimationPerformance performance, - Direction direction, - Function onDismissed, - Function onCompleted, - Widget child + WatchableAnimationPerformance performance }) : super(key: key, - anchor: anchor, - duration: duration, - performance: performance, - direction: direction, - onDismissed: onDismissed, - onCompleted: onCompleted, - child: child); + performance: performance); List variables; BuilderFunction builder; @@ -301,7 +240,7 @@ class BuilderTransition extends TransitionBase { super.syncConstructorArguments(source); } - Widget buildWithChild(Widget child) { + Widget build() { for (int i = 0; i < variables.length; ++i) performance.updateVariable(variables[i]); return builder();