From f676d29b8597caced1d44c24a4b6d01f11b0a49a Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 28 Jul 2015 15:23:12 -0400 Subject: [PATCH] Add an AnimationStatus to AnimationPerformance, and use that in Drawer, SnackBar, and PopupMenu instead of custom statuses. --- .../sky/example/stocks/lib/stock_home.dart | 22 +++--- .../lib/animation/animation_performance.dart | 68 +++++++++++++++++-- sky/packages/sky/lib/widgets/drawer.dart | 35 ++++------ sky/packages/sky/lib/widgets/popup_menu.dart | 34 ++++------ sky/packages/sky/lib/widgets/snack_bar.dart | 52 +++++--------- 5 files changed, 115 insertions(+), 96 deletions(-) diff --git a/sky/packages/sky/example/stocks/lib/stock_home.dart b/sky/packages/sky/example/stocks/lib/stock_home.dart index 4d0bbaed3124e..f286e9d73aa56 100644 --- a/sky/packages/sky/example/stocks/lib/stock_home.dart +++ b/sky/packages/sky/example/stocks/lib/stock_home.dart @@ -27,7 +27,7 @@ class StockHome extends StatefulComponent { bool _isSearching = false; String _searchQuery; - SnackBarStatus _snackBarStatus = SnackBarStatus.inactive; + AnimationStatus _snackBarStatus = AnimationStatus.dismissed; bool _isSnackBarShowing = false; void _handleSearchBegin() { @@ -59,28 +59,28 @@ class StockHome extends StatefulComponent { } bool _drawerShowing = false; - DrawerStatus _drawerStatus = DrawerStatus.inactive; + AnimationStatus _drawerStatus = AnimationStatus.dismissed; void _handleOpenDrawer() { setState(() { _drawerShowing = true; - _drawerStatus = DrawerStatus.active; + _drawerStatus = AnimationStatus.forward; }); } - void _handleDrawerStatusChange(DrawerStatus status) { + void _handleDrawerStatusChange(AnimationStatus status) { setState(() { _drawerStatus = status; }); } bool _menuShowing = false; - PopupMenuStatus _menuStatus = PopupMenuStatus.inactive; + AnimationStatus _menuStatus = AnimationStatus.dismissed; void _handleMenuShow() { setState(() { _menuShowing = true; - _menuStatus = PopupMenuStatus.active; + _menuStatus = AnimationStatus.forward; }); } @@ -90,7 +90,7 @@ class StockHome extends StatefulComponent { }); } - void _handleMenuStatusChanged(PopupMenuStatus status) { + void _handleMenuStatusChanged(AnimationStatus status) { setState(() { _menuStatus = status; }); @@ -112,7 +112,7 @@ class StockHome extends StatefulComponent { } Drawer buildDrawer() { - if (_drawerStatus == DrawerStatus.inactive) + if (_drawerStatus == AnimationStatus.dismissed) return null; assert(_drawerShowing); // TODO(mpcomplete): this is always true return new Drawer( @@ -246,7 +246,7 @@ class StockHome extends StatefulComponent { } Widget buildSnackBar() { - if (_snackBarStatus == SnackBarStatus.inactive) + if (_snackBarStatus == AnimationStatus.dismissed) return null; return new SnackBar( showing: _isSnackBarShowing, @@ -259,7 +259,7 @@ class StockHome extends StatefulComponent { void _handleStockPurchased() { setState(() { _isSnackBarShowing = true; - _snackBarStatus = SnackBarStatus.active; + _snackBarStatus = AnimationStatus.forward; }); } @@ -272,7 +272,7 @@ class StockHome extends StatefulComponent { } void addMenuToOverlays(List overlays) { - if (_menuStatus == PopupMenuStatus.inactive) + if (_menuStatus == AnimationStatus.dismissed) return; overlays.add(new ModalOverlay( children: [new StockMenu( diff --git a/sky/packages/sky/lib/animation/animation_performance.dart b/sky/packages/sky/lib/animation/animation_performance.dart index 716165ae6eca4..109a69b744ee5 100644 --- a/sky/packages/sky/lib/animation/animation_performance.dart +++ b/sky/packages/sky/lib/animation/animation_performance.dart @@ -8,6 +8,18 @@ import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/forces.dart'; import 'package:sky/animation/timeline.dart'; +enum AnimationDirection { + forward, + reverse +} + +enum AnimationStatus { + dismissed, // stoped at 0 + forward, // animating from 0 => 1 + reverse, // animating from 1 => 0 + completed, // stopped at 1 +} + // This class manages a "performance" - a collection of values that change // based on a timeline. For example, a performance may handle an animation // of a menu opening by sliding and fading in (changing Y value and opacity) @@ -16,30 +28,49 @@ import 'package:sky/animation/timeline.dart'; // manipulating |progress|, or |fling| the timeline causing a physics-based // simulation to take over the progression. class AnimationPerformance { - AnimationPerformance() { + AnimationPerformance({this.variable, this.duration}) { _timeline = new Timeline(_tick); } AnimatedVariable variable; - // TODO(mpcomplete): duration should be on a director. Duration duration; // Advances from 0 to 1. On each tick, we'll update our variable's values. Timeline _timeline; Timeline get timeline => _timeline; + AnimationDirection _direction; + AnimationDirection get direction => _direction; + double get progress => timeline.value; void set progress(double t) { + // TODO(mpcomplete): should this affect |direction|? stop(); timeline.value = t.clamp(0.0, 1.0); + _checkStatusChanged(); } - bool get isDismissed => progress == 0.0; - bool get isCompleted => progress == 1.0; + bool get isDismissed => status == AnimationStatus.dismissed; + bool get isCompleted => status == AnimationStatus.completed; bool get isAnimating => timeline.isAnimating; - Future play() => _animateTo(1.0); - Future reverse() => _animateTo(0.0); + AnimationStatus get status { + if (!isAnimating && progress == 1.0) + return AnimationStatus.completed; + if (!isAnimating && progress == 0.0) + return AnimationStatus.dismissed; + return direction == AnimationDirection.forward ? + AnimationStatus.forward : + AnimationStatus.reverse; + } + + Future play([AnimationDirection direction = AnimationDirection.forward]) { + _direction = direction; + return resume(); + } + Future forward() => play(AnimationDirection.forward); + Future reverse() => play(AnimationDirection.reverse); + Future resume() => _animateTo(direction == AnimationDirection.forward ? 1.0 : 0.0); void stop() { timeline.stop(); @@ -51,6 +82,9 @@ class AnimationPerformance { Future fling({double velocity: 1.0, Force force}) { if (force == null) force = kDefaultSpringForce; + // This is an approximation - the force may not necessarily result in + // animating the same direction as the initial velocity. + _direction = velocity >= 0.0 ? AnimationDirection.forward : AnimationDirection.reverse; return timeline.fling(force.release(progress, velocity)); } @@ -70,6 +104,27 @@ class AnimationPerformance { listener(); } + final List _statusListeners = new List(); + + void addStatusListener(Function listener) { + _statusListeners.add(listener); + } + + void removeStatusListener(Function listener) { + _statusListeners.remove(listener); + } + + AnimationStatus _lastStatus = AnimationStatus.dismissed; + void _checkStatusChanged() { + AnimationStatus currentStatus = status; + if (currentStatus != _lastStatus) { + List localListeners = new List.from(_statusListeners); + for (Function listener in localListeners) + listener(currentStatus); + } + _lastStatus = currentStatus; + } + Future _animateTo(double target) { double remainingDistance = (target - timeline.value).abs(); timeline.stop(); @@ -81,5 +136,6 @@ class AnimationPerformance { void _tick(double t) { variable.setProgress(t); _notifyListeners(); + _checkStatusChanged(); } } diff --git a/sky/packages/sky/lib/widgets/drawer.dart b/sky/packages/sky/lib/widgets/drawer.dart index 8a34bdf529ce5..b6cd4dc89bad4 100644 --- a/sky/packages/sky/lib/widgets/drawer.dart +++ b/sky/packages/sky/lib/widgets/drawer.dart @@ -15,6 +15,8 @@ import 'package:sky/widgets/scrollable_viewport.dart'; import 'package:sky/widgets/theme.dart'; import 'package:vector_math/vector_math.dart'; +export 'package:sky/animation/animation_performance.dart' show AnimationStatus; + // TODO(eseidel): Draw width should vary based on device size: // http://www.google.com/design/spec/layout/structure.html#structure-side-nav @@ -35,14 +37,7 @@ const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); const Point _kOpenPosition = Point.origin; const Point _kClosedPosition = const Point(-_kWidth, 0.0); -typedef void DrawerStatusChangeHandler (bool showing); - -enum DrawerStatus { - active, - inactive, -} - -typedef void DrawerStatusChangedCallback(DrawerStatus status); +typedef void DrawerStatusChangedCallback(AnimationStatus status); class Drawer extends AnimatedComponent { Drawer({ @@ -70,7 +65,7 @@ class Drawer extends AnimatedComponent { _performance = new AnimationPerformance() ..duration = _kBaseSettleDuration ..variable = new AnimatedList([_position, _maskColor]) - ..addListener(_checkForStateChanged); + ..addStatusListener(_onStatusChanged); watch(_performance); if (showing) _show(); @@ -137,22 +132,16 @@ class Drawer extends AnimatedComponent { double get xPosition => _position.value.x; - DrawerStatus _lastStatus; - void _checkForStateChanged() { - DrawerStatus status = _status; - if (_lastStatus != null && status != _lastStatus) { - if (status == DrawerStatus.inactive && - navigator != null && - navigator.currentRoute is RouteState && - (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer - navigator.pop(); - if (onStatusChanged != null) - onStatusChanged(status); - } - _lastStatus = status; + void _onStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.dismissed && + navigator != null && + navigator.currentRoute is RouteState && + (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer + navigator.pop(); + if (onStatusChanged != null) + onStatusChanged(status); } - DrawerStatus get _status => _performance.isDismissed ? DrawerStatus.inactive : DrawerStatus.active; bool get _isMostlyClosed => xPosition <= -_kWidth/2; void _settle() => _fling(_isMostlyClosed ? -1.0 : 1.0); diff --git a/sky/packages/sky/lib/widgets/popup_menu.dart b/sky/packages/sky/lib/widgets/popup_menu.dart index db18386393461..6ebbd260117f5 100644 --- a/sky/packages/sky/lib/widgets/popup_menu.dart +++ b/sky/packages/sky/lib/widgets/popup_menu.dart @@ -16,6 +16,8 @@ import 'package:sky/widgets/navigator.dart'; import 'package:sky/widgets/popup_menu_item.dart'; import 'package:sky/widgets/scrollable_viewport.dart'; +export 'package:sky/animation/animation_performance.dart' show AnimationStatus; + const Duration _kMenuDuration = const Duration(milliseconds: 300); double _kMenuCloseIntervalEnd = 2.0 / 3.0; const double _kMenuWidthStep = 56.0; @@ -25,12 +27,7 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; const double _kMenuHorizontalPadding = 16.0; const double _kMenuVerticalPadding = 8.0; -enum PopupMenuStatus { - active, - inactive, -} - -typedef void PopupMenuStatusChangedCallback(PopupMenuStatus status); +typedef void PopupMenuStatusChangedCallback(AnimationStatus status); class PopupMenu extends AnimatedComponent { @@ -59,7 +56,7 @@ class PopupMenu extends AnimatedComponent { void initState() { _performance = new AnimationPerformance() ..duration = _kMenuDuration - ..addListener(_checkForStateChanged); + ..addStatusListener(_onStatusChanged); _updateAnimationVariables(); watch(_performance); _updateBoxPainter(); @@ -115,21 +112,14 @@ class PopupMenu extends AnimatedComponent { boxShadow: shadows[level])); } - PopupMenuStatus get _status => _opacity.value != 0.0 ? PopupMenuStatus.active : PopupMenuStatus.inactive; - - PopupMenuStatus _lastStatus; - void _checkForStateChanged() { - PopupMenuStatus status = _status; - if (_lastStatus != null && status != _lastStatus) { - if (status == PopupMenuStatus.inactive && - navigator != null && - navigator.currentRoute is RouteState && - (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer - navigator.pop(); - if (onStatusChanged != null) - onStatusChanged(status); - } - _lastStatus = status; + void _onStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.dismissed && + navigator != null && + navigator.currentRoute is RouteState && + (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer + navigator.pop(); + if (onStatusChanged != null) + onStatusChanged(status); } diff --git a/sky/packages/sky/lib/widgets/snack_bar.dart b/sky/packages/sky/lib/widgets/snack_bar.dart index 57f37e9786d8a..cb6d3b5012bc5 100644 --- a/sky/packages/sky/lib/widgets/snack_bar.dart +++ b/sky/packages/sky/lib/widgets/snack_bar.dart @@ -14,15 +14,11 @@ import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/default_text_style.dart'; import 'package:sky/widgets/material.dart'; import 'package:sky/widgets/theme.dart'; - import 'package:vector_math/vector_math.dart'; -enum SnackBarStatus { - active, - inactive, -} +export 'package:sky/animation/animation_performance.dart' show AnimationStatus; -typedef void SnackBarStatusChangedCallback(SnackBarStatus status); +typedef void SnackBarStatusChangedCallback(AnimationStatus status); const Duration _kSlideInDuration = const Duration(milliseconds: 200); @@ -48,53 +44,35 @@ class SnackBarAction extends Component { // TODO(mpcomplete): generalize this to a SlideIn class. class SnackBarSlideInIntention extends AnimationIntention { - SnackBarSlideInIntention(this.duration, this.onStatusChanged); + SnackBarSlideInIntention(Duration duration) { + _position = new AnimatedValue(const Point(0.0, 50.0), end: Point.origin); + performance = new AnimationPerformance(duration: duration, variable: _position); + } - Duration duration; SnackBarStatusChangedCallback onStatusChanged; AnimatedValue _position; - AnimationPerformance _performance; + AnimationPerformance performance; void initFields(AnimatedContainer container) { - _position = new AnimatedValue(new Point(0.0, 50.0), end: Point.origin); - _performance = new AnimationPerformance() - ..duration = _kSlideInDuration - ..variable = _position - ..addListener(() { _updateProgress(container); }); - _performance.progress = 0.0; + performance.addListener(() { _updateProgress(container); }); + performance.progress = 0.0; if (container.tag) - _show(); + performance.play(); } void syncFields(AnimatedContainer original, AnimatedContainer updated) { if (original.tag != updated.tag) { original.tag = updated.tag; - original.tag ? _show() : _hide(); + performance.play(original.tag ? AnimationDirection.forward : AnimationDirection.reverse); } } - void _show() { - _performance.play(); - } - - void _hide() { - _performance.reverse(); - } - - SnackBarStatus _lastStatus; void _updateProgress(AnimatedContainer container) { container.setState(() { container.transform = new Matrix4.identity() ..translate(_position.value.x, _position.value.y); }); - - SnackBarStatus status = _status; - if (_lastStatus != null && status != _lastStatus && onStatusChanged != null) - scheduleMicrotask(() { onStatusChanged(status); }); - _lastStatus = status; } - - SnackBarStatus get _status => _performance.isDismissed ? SnackBarStatus.inactive : SnackBarStatus.active; } class SnackBar extends StatefulComponent { @@ -117,7 +95,8 @@ class SnackBar extends StatefulComponent { SnackBarSlideInIntention _intention; void initState() { - _intention = new SnackBarSlideInIntention(_kSlideInDuration, onStatusChanged); + _intention = new SnackBarSlideInIntention(_kSlideInDuration); + _intention.performance.addStatusListener(_onStatusChanged); } void syncFields(SnackBar source) { @@ -127,6 +106,11 @@ class SnackBar extends StatefulComponent { showing = source.showing; } + void _onStatusChanged(AnimationStatus status) { + if (onStatusChanged != null) + scheduleMicrotask(() { onStatusChanged(status); }); + } + Widget build() { List children = [ new Flexible(