From f7d97d4cd870db1bc643d934016efb9cae95c6d7 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 30 Jul 2015 13:11:32 -0400 Subject: [PATCH] Use SnackBar's SlideInIntention for Drawer animation. This required some changes to AnimationPerformance to better understand animating with forces. --- .../sky/lib/animation/animated_value.dart | 4 +- .../lib/animation/animation_performance.dart | 49 ++++--- sky/packages/sky/lib/animation/forces.dart | 15 ++- .../sky/lib/animation/scroll_behavior.dart | 3 +- .../sky/lib/widgets/animation_intentions.dart | 87 ++++++++++++ sky/packages/sky/lib/widgets/drawer.dart | 125 +++++++++--------- sky/packages/sky/lib/widgets/snack_bar.dart | 42 +----- 7 files changed, 194 insertions(+), 131 deletions(-) create mode 100644 sky/packages/sky/lib/widgets/animation_intentions.dart diff --git a/sky/packages/sky/lib/animation/animated_value.dart b/sky/packages/sky/lib/animation/animated_value.dart index 89c053a34a3..a27878d7e67 100644 --- a/sky/packages/sky/lib/animation/animated_value.dart +++ b/sky/packages/sky/lib/animation/animated_value.dart @@ -69,8 +69,8 @@ class AnimatedList extends AnimatedVariable { String toString() => 'AnimatedList([$variables])'; } -class AnimatedColor extends AnimatedValue { - AnimatedColor(Color begin, { Color end, Curve curve: linear }) +class AnimatedColorValue extends AnimatedValue { + AnimatedColorValue(Color begin, { Color end, Curve curve: linear }) : super(begin, end: end, curve: curve); void setProgress(double t) { diff --git a/sky/packages/sky/lib/animation/animation_performance.dart b/sky/packages/sky/lib/animation/animation_performance.dart index 109a69b744e..d95c11e8bee 100644 --- a/sky/packages/sky/lib/animation/animation_performance.dart +++ b/sky/packages/sky/lib/animation/animation_performance.dart @@ -8,10 +8,7 @@ import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/forces.dart'; import 'package:sky/animation/timeline.dart'; -enum AnimationDirection { - forward, - reverse -} +export 'package:sky/animation/forces.dart' show Direction; enum AnimationStatus { dismissed, // stoped at 0 @@ -39,8 +36,21 @@ class AnimationPerformance { Timeline _timeline; Timeline get timeline => _timeline; - AnimationDirection _direction; - AnimationDirection get direction => _direction; + Direction _direction; + Direction get direction => _direction; + + // If non-null, animate with this force instead of a tween animation. + Force attachedForce; + + void addVariable(AnimatedVariable newVariable) { + if (variable == null) { + variable = newVariable; + } else if (variable is AnimatedList) { + (variable as AnimatedList).variables.add(newVariable); + } else { + variable = new AnimatedList([variable, newVariable]); + } + } double get progress => timeline.value; void set progress(double t) { @@ -59,33 +69,34 @@ class AnimationPerformance { return AnimationStatus.completed; if (!isAnimating && progress == 0.0) return AnimationStatus.dismissed; - return direction == AnimationDirection.forward ? + return direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse; } - Future play([AnimationDirection direction = AnimationDirection.forward]) { + Future play([Direction direction = Direction.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); + Future forward() => play(Direction.forward); + Future reverse() => play(Direction.reverse); + Future resume() { + if (attachedForce != null) + return fling(_direction, force: attachedForce); + return _animateTo(direction == Direction.forward ? 1.0 : 0.0); + } void stop() { timeline.stop(); } - // Flings the timeline with an optional force (defaults to a critically damped - // spring) and initial velocity. Negative velocity causes the timeline to go - // in reverse. - Future fling({double velocity: 1.0, Force force}) { + // Flings the timeline in the given direction with an optional force + // (defaults to a critically damped spring) and initial velocity. + Future fling(Direction direction, {double velocity: 0.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)); + _direction = direction; + return timeline.fling(force.release(progress, velocity, _direction)); } final List _listeners = new List(); diff --git a/sky/packages/sky/lib/animation/forces.dart b/sky/packages/sky/lib/animation/forces.dart index d1dae9840f2..e309f0ba5d6 100644 --- a/sky/packages/sky/lib/animation/forces.dart +++ b/sky/packages/sky/lib/animation/forces.dart @@ -4,9 +4,15 @@ import 'package:newton/newton.dart'; +// TODO(mpcomplete): This doesn't belong here. +enum Direction { + forward, + reverse +} + // Base class for creating Simulations for the animation Timeline. abstract class Force { - Simulation release(double position, double velocity); + Simulation release(double position, double velocity, Direction direction); } class SpringForce extends Force { @@ -17,18 +23,17 @@ class SpringForce extends Force { // respectively. final double left, right; - Simulation release(double position, double velocity) { + Simulation release(double position, double velocity, Direction direction) { // Target just past the endpoint, because the animation will stop once the // Spring gets within the epsilon, and we want to stop at the endpoint. - double target = velocity < 0.0 ? + double target = direction == Direction.reverse ? this.left - _kEpsilon : this.right + _kEpsilon; return new SpringSimulation(spring, position, target, velocity); } } final SpringDescription _kDefaultSpringDesc = - new SpringDescription.withDampingRatio( - mass: 1.0, springConstant: 500.0, ratio: 1.0); + new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 500.0, ratio: 1.0); final SpringForce kDefaultSpringForce = new SpringForce(_kDefaultSpringDesc); const double _kEpsilon = 0.001; diff --git a/sky/packages/sky/lib/animation/scroll_behavior.dart b/sky/packages/sky/lib/animation/scroll_behavior.dart index 7bd713c9dc8..08b2cbed2a2 100644 --- a/sky/packages/sky/lib/animation/scroll_behavior.dart +++ b/sky/packages/sky/lib/animation/scroll_behavior.dart @@ -5,11 +5,10 @@ import 'dart:math' as math; import 'package:newton/newton.dart'; -import 'package:sky/animation/forces.dart'; const double _kSecondsPerMillisecond = 1000.0; -abstract class ScrollBehavior extends Force { +abstract class ScrollBehavior { Simulation release(double position, double velocity) => null; // Returns the new scroll offset. diff --git a/sky/packages/sky/lib/widgets/animation_intentions.dart b/sky/packages/sky/lib/widgets/animation_intentions.dart new file mode 100644 index 00000000000..d14756a8005 --- /dev/null +++ b/sky/packages/sky/lib/widgets/animation_intentions.dart @@ -0,0 +1,87 @@ +// 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. + +// This file contains a set of common intentions to use with AnimationContainer +// for describing how to animate certain properties. + +import 'dart:sky'; + +import 'package:sky/animation/animated_value.dart'; +import 'package:sky/animation/animation_performance.dart'; +import 'package:sky/widgets/animated_container.dart'; +import 'package:sky/widgets/basic.dart'; +import 'package:vector_math/vector_math.dart'; + +// Slides a container in from |start| to |end| when the container's |tag| is +// true (reverses if false). +class SlideInIntention extends AnimationIntention { + SlideInIntention({Duration duration, this.performance, Point start, Point end}) { + if (performance == null) { + assert(duration != null); + performance = new AnimationPerformance(duration: duration); + } + _position = new AnimatedValue(start, end: end); + performance.addVariable(_position); + } + + AnimatedValue _position; + AnimationPerformance performance; + + void initFields(AnimatedContainer container) { + performance.addListener(() { _updateProgress(container); }); + performance.progress = 0.0; + if (container.tag) + performance.play(); + } + + void syncFields(AnimatedContainer original, AnimatedContainer updated) { + if (original.tag != updated.tag) { + original.tag = updated.tag; + performance.play(original.tag ? Direction.forward : Direction.reverse); + } + } + + void _updateProgress(AnimatedContainer container) { + container.setState(() { + container.transform = new Matrix4.identity() + ..translate(_position.value.x, _position.value.y); + }); + } +} + +// Changes color from |start| to |end| when the container's |tag| is true +// (reverses if false). +class ColorTransitionIntention extends AnimationIntention { + ColorTransitionIntention({Duration duration, this.performance, Color start, Color end}) { + if (performance == null) { + assert(duration != null); + performance = new AnimationPerformance(duration: duration); + } + _color = new AnimatedColorValue(start, end: end); + performance.addVariable(_color); + } + + AnimatedColorValue _color; + AnimationPerformance performance; + + void initFields(AnimatedContainer container) { + performance.addListener(() { _updateProgress(container); }); + performance.progress = 0.0; + if (container.tag) + performance.play(); + } + + void syncFields(AnimatedContainer original, AnimatedContainer updated) { + if (original.tag != updated.tag) { + original.tag = updated.tag; + performance.play(original.tag ? Direction.forward : Direction.reverse); + } + } + + void _updateProgress(AnimatedContainer container) { + container.setState(() { + container.decoration = new BoxDecoration(backgroundColor: _color.value); + }); + } +} diff --git a/sky/packages/sky/lib/widgets/drawer.dart b/sky/packages/sky/lib/widgets/drawer.dart index b6cd4dc89ba..d0e17e1f76c 100644 --- a/sky/packages/sky/lib/widgets/drawer.dart +++ b/sky/packages/sky/lib/widgets/drawer.dart @@ -2,18 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:sky' as sky; -import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/animation_performance.dart'; +import 'package:sky/animation/forces.dart'; import 'package:sky/theme/shadows.dart'; import 'package:sky/theme/colors.dart' as colors; -import 'package:sky/widgets/animated_component.dart'; +import 'package:sky/widgets/animated_container.dart'; +import 'package:sky/widgets/animation_intentions.dart'; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/navigator.dart'; 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; @@ -34,12 +35,13 @@ const double _kWidth = 304.0; const double _kMinFlingVelocity = 365.0; const double _kFlingVelocityScale = 1.0 / 300.0; const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); +const Duration _kThemeChangeDuration = const Duration(milliseconds: 200); const Point _kOpenPosition = Point.origin; const Point _kClosedPosition = const Point(-_kWidth, 0.0); typedef void DrawerStatusChangedCallback(AnimationStatus status); -class Drawer extends AnimatedComponent { +class Drawer extends StatefulComponent { Drawer({ Key key, this.children, @@ -55,70 +57,58 @@ class Drawer extends AnimatedComponent { DrawerStatusChangedCallback onStatusChanged; Navigator navigator; - AnimatedValue _position; - AnimatedColor _maskColor; - AnimationPerformance _performance; + SlideInIntention _intention; + ColorTransitionIntention _maskColorIntention; + AnimationPerformance get _performance => _intention.performance; void initState() { - _position = new AnimatedValue(_kClosedPosition, end: _kOpenPosition); - _maskColor = new AnimatedColor(colors.transparent, end: const Color(0x7F000000)); - _performance = new AnimationPerformance() - ..duration = _kBaseSettleDuration - ..variable = new AnimatedList([_position, _maskColor]) - ..addStatusListener(_onStatusChanged); - watch(_performance); - if (showing) - _show(); + _intention = new SlideInIntention( + duration: _kBaseSettleDuration, start: _kClosedPosition, end: _kOpenPosition); + _maskColorIntention = new ColorTransitionIntention( + performance: _intention.performance, start: colors.transparent, end: const Color(0x7F000000)); + + _performance.addStatusListener(_onStatusChanged); + // 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) + navigator.pushState(this, (_) => _performance.reverse()); } void syncFields(Drawer source) { children = source.children; level = source.level; navigator = source.navigator; - if (showing != source.showing) { - showing = source.showing; - showing ? _show() : _hide(); - } + showing = source.showing; onStatusChanged = source.onStatusChanged; - super.syncFields(source); - } - - void _show() { - if (navigator != null) - navigator.pushState(this, (_) => _performance.reverse()); - _fling(1.0); - } - - void _hide() { - _fling(-1.0); - } - - // We fling the performance timeline instead of animating it to give it a - // nice spring effect. We can't use curves for this because we need a linear - // curve in order to track the user's finger while dragging. - void _fling(double direction) { - _performance.fling(velocity: direction.sign); } Widget build() { var mask = new Listener( - child: new Container( - decoration: new BoxDecoration(backgroundColor: _maskColor.value) + child: new AnimatedContainer( + intentions: [_maskColorIntention], + tag: showing ), onGestureTap: handleMaskTap ); - Matrix4 transform = new Matrix4.identity(); - transform.translate(_position.value.x, _position.value.y); - Widget content = new Transform( - transform: transform, - child: new Container( - decoration: new BoxDecoration( - backgroundColor: Theme.of(this).canvasColor, - boxShadow: shadows[level]), - width: _kWidth, - child: new ScrollableBlock(children) - )); + Widget content = new AnimatedContainer( + intentions: [ + _intention, + // TODO(mpcomplete): it should be easier to override some intentions, + // and have those you don't care about revert to a sensible default. + new ImplicitlySyncDecorationIntention(_kThemeChangeDuration), + new ImplicitlySyncWidthIntention(_kThemeChangeDuration), + ], + tag: showing, + decoration: new BoxDecoration( + backgroundColor: Theme.of(this).canvasColor, + boxShadow: shadows[level]), + width: _kWidth, + child: new ScrollableBlock(children) + ); return new Listener( child: new Stack([ mask, content ]), @@ -130,27 +120,27 @@ class Drawer extends AnimatedComponent { ); } - double get xPosition => _position.value.x; - 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); + scheduleMicrotask(() { + 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); + }); } - bool get _isMostlyClosed => xPosition <= -_kWidth/2; + bool get _isMostlyClosed => _performance.progress < 0.5; - void _settle() => _fling(_isMostlyClosed ? -1.0 : 1.0); + void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); } - void handleMaskTap(_) => _fling(-1.0); + void handleMaskTap(_) { _performance.reverse(); } // TODO(mpcomplete): Figure out how to generalize these handlers on a // "PannableThingy" interface. - void handlePointerDown(_) => _performance.stop(); + void handlePointerDown(_) { _performance.stop(); } void handlePointerMove(sky.PointerEvent event) { if (_performance.isAnimating) @@ -169,7 +159,10 @@ class Drawer extends AnimatedComponent { } void handleFlingStart(event) { - if (event.velocityX.abs() >= _kMinFlingVelocity) - _performance.fling(velocity: event.velocityX * _kFlingVelocityScale); + if (event.velocityX.abs() >= _kMinFlingVelocity) { + _performance.fling( + event.velocityX < 0.0 ? Direction.reverse : Direction.forward, + velocity: event.velocityX.abs() * _kFlingVelocityScale); + } } } diff --git a/sky/packages/sky/lib/widgets/snack_bar.dart b/sky/packages/sky/lib/widgets/snack_bar.dart index 1591187412e..9496ead5de3 100644 --- a/sky/packages/sky/lib/widgets/snack_bar.dart +++ b/sky/packages/sky/lib/widgets/snack_bar.dart @@ -4,16 +4,15 @@ import 'dart:async'; -import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/animation_performance.dart'; import 'package:sky/painting/text_style.dart'; import 'package:sky/theme/typography.dart' as typography; import 'package:sky/widgets/animated_container.dart'; +import 'package:sky/widgets/animation_intentions.dart'; 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'; export 'package:sky/animation/animation_performance.dart' show AnimationStatus; @@ -41,39 +40,6 @@ class SnackBarAction extends Component { } } -// TODO(mpcomplete): generalize this to a SlideIn class. -class SnackBarSlideInIntention extends AnimationIntention { - SnackBarSlideInIntention(Duration duration) { - _position = new AnimatedValue(const Point(0.0, 50.0), end: Point.origin); - performance = new AnimationPerformance(duration: duration, variable: _position); - } - - SnackBarStatusChangedCallback onStatusChanged; - AnimatedValue _position; - AnimationPerformance performance; - - void initFields(AnimatedContainer container) { - performance.addListener(() { _updateProgress(container); }); - performance.progress = 0.0; - if (container.tag) - performance.play(); - } - - void syncFields(AnimatedContainer original, AnimatedContainer updated) { - if (original.tag != updated.tag) { - original.tag = updated.tag; - performance.play(original.tag ? AnimationDirection.forward : AnimationDirection.reverse); - } - } - - void _updateProgress(AnimatedContainer container) { - container.setState(() { - container.transform = new Matrix4.identity() - ..translate(_position.value.x, _position.value.y); - }); - } -} - class SnackBar extends StatefulComponent { SnackBar({ @@ -91,10 +57,12 @@ class SnackBar extends StatefulComponent { bool showing; SnackBarStatusChangedCallback onStatusChanged; - SnackBarSlideInIntention _intention; + SlideInIntention _intention; void initState() { - _intention = new SnackBarSlideInIntention(_kSlideInDuration); + _intention = new SlideInIntention(duration: _kSlideInDuration, + start: const Point(0.0, 50.0), + end: Point.origin); _intention.performance.addStatusListener(_onStatusChanged); }