From a079f3adf22939124b9697f5c331b5fa84dbbfb2 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 23 Jul 2015 18:28:45 -0400 Subject: [PATCH] Refactor AnimationContainer to support drop-in Intentions. This lets us be flexible in how to animate the properties of the container. Currently used by SnackBar with a SlideIn intention. --- .../sky/lib/widgets/animated_container.dart | 239 +++++++++++------- sky/packages/sky/lib/widgets/material.dart | 2 +- sky/packages/sky/lib/widgets/snack_bar.dart | 108 +++++--- 3 files changed, 209 insertions(+), 140 deletions(-) diff --git a/sky/packages/sky/lib/widgets/animated_container.dart b/sky/packages/sky/lib/widgets/animated_container.dart index e55ba7dc4d9..a48535a478b 100644 --- a/sky/packages/sky/lib/widgets/animated_container.dart +++ b/sky/packages/sky/lib/widgets/animated_container.dart @@ -53,6 +53,24 @@ class AnimatedEdgeDimsValue extends AnimatedValue { } } +class AnimatedMatrix4Value extends AnimatedValue { + AnimatedMatrix4Value(Matrix4 begin, { Matrix4 end, Curve curve: linear }) + : super(begin, end: end, curve: curve); + + void setProgress(double t) { + if (t == 1.0) { + value = end; + return; + } + // TODO(mpcomplete): Animate the full matrix. Will animating the cells + // separately work? + Vector3 beginT = begin.getTranslation(); + Vector3 endT = end.getTranslation(); + Vector3 lerpT = beginT*(1.0-t) + endT*t; + value = new Matrix4.identity()..translate(lerpT); + } +} + class ImplicitlyAnimatedValue { final AnimationPerformance performance = new AnimationPerformance(); final AnimatedValue _variable; @@ -64,7 +82,7 @@ class ImplicitlyAnimatedValue { } T get value => _variable.value; - void set value(T newValue) { + void animateTo(T newValue) { _variable.begin = _variable.value; _variable.end = newValue; if (_variable.value != _variable.end) { @@ -75,11 +93,118 @@ class ImplicitlyAnimatedValue { } } +abstract class AnimationIntention { + void initFields(AnimatedContainer original); + void syncFields(AnimatedContainer original, AnimatedContainer updated); +} + +abstract class ImplicitlySyncFieldIntention extends AnimationIntention { + ImplicitlySyncFieldIntention(this.duration); + + Duration duration; + ImplicitlyAnimatedValue field; + + // Overrides. + T getter(AnimatedContainer container); + void setter(AnimatedContainer container, T value); + AnimatedValue initField(T value); + + void initFields(AnimatedContainer original) { + _updateField(original, getter(original)); + } + + void syncFields(AnimatedContainer original, AnimatedContainer updated) { + _updateField(original, getter(updated)); + } + + void _updateField(AnimatedContainer original, T newValue) { + if (field != null) { + // Animate to newValue (possibly null). + field.animateTo(newValue); + } else if (newValue != null) { + // Set the value and prepare it for future animations. + field = new ImplicitlyAnimatedValue(initField(newValue), duration); + field.performance.addListener(() { + original.setState(() { setter(original, field.value); }); + }); + } + } +} + +class ImplicitlySyncConstraintsIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncConstraintsIntention(Duration duration) : super(duration); + + BoxConstraints getter(AnimatedContainer container) => container.constraints; + void setter(AnimatedContainer container, BoxConstraints val) { container.constraints = val; } + AnimatedValue initField(BoxConstraints val) => new AnimatedBoxConstraintsValue(val); +} + +class ImplicitlySyncDecorationIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncDecorationIntention(Duration duration) : super(duration); + + BoxDecoration getter(AnimatedContainer container) => container.decoration; + void setter(AnimatedContainer container, BoxDecoration val) { container.decoration = val; } + AnimatedValue initField(BoxDecoration val) => new AnimatedBoxDecorationValue(val); +} + +class ImplicitlySyncMarginIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncMarginIntention(Duration duration) : super(duration); + + EdgeDims getter(AnimatedContainer container) => container.margin; + void setter(AnimatedContainer container, EdgeDims val) { container.margin = val; } + AnimatedValue initField(EdgeDims val) => new AnimatedEdgeDimsValue(val); +} + +class ImplicitlySyncPaddingIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncPaddingIntention(Duration duration) : super(duration); + + EdgeDims getter(AnimatedContainer container) => container.padding; + void setter(AnimatedContainer container, EdgeDims val) { container.padding = val; } + AnimatedValue initField(EdgeDims val) => new AnimatedEdgeDimsValue(val); +} + +class ImplicitlySyncTransformIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncTransformIntention(Duration duration) : super(duration); + + Matrix4 getter(AnimatedContainer container) => container.transform; + void setter(AnimatedContainer container, Matrix4 val) { container.transform = val; } + AnimatedValue initField(Matrix4 val) => new AnimatedMatrix4Value(val); +} + +class ImplicitlySyncWidthIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncWidthIntention(Duration duration) : super(duration); + + double getter(AnimatedContainer container) => container.width; + void setter(AnimatedContainer container, double val) { container.width = val; } + AnimatedValue initField(double val) => new AnimatedValue(val); +} + +class ImplicitlySyncHeightIntention extends ImplicitlySyncFieldIntention { + ImplicitlySyncHeightIntention(Duration duration) : super(duration); + + double getter(AnimatedContainer container) => container.height; + void setter(AnimatedContainer container, double val) { container.height = val; } + AnimatedValue initField(double val) => new AnimatedValue(val); +} + +List implicitlySyncFieldsIntention(Duration duration) { + return [ + new ImplicitlySyncConstraintsIntention(duration), + new ImplicitlySyncDecorationIntention(duration), + new ImplicitlySyncMarginIntention(duration), + new ImplicitlySyncPaddingIntention(duration), + new ImplicitlySyncTransformIntention(duration), + new ImplicitlySyncWidthIntention(duration), + new ImplicitlySyncHeightIntention(duration) + ]; +} + class AnimatedContainer extends AnimatedComponent { AnimatedContainer({ Key key, this.child, - this.duration, + this.intentions, + this.tag, this.constraints, this.decoration, this.width, @@ -90,7 +215,6 @@ class AnimatedContainer extends AnimatedComponent { }) : super(key: key); Widget child; - Duration duration; // TODO(abarth): Support separate durations for each value. BoxConstraints constraints; BoxDecoration decoration; EdgeDims margin; @@ -99,109 +223,30 @@ class AnimatedContainer extends AnimatedComponent { double width; double height; - ImplicitlyAnimatedValue _constraints; - ImplicitlyAnimatedValue _decoration; - ImplicitlyAnimatedValue _margin; - ImplicitlyAnimatedValue _padding; - ImplicitlyAnimatedValue _transform; - ImplicitlyAnimatedValue _width; - ImplicitlyAnimatedValue _height; + List intentions; + dynamic tag; // Used by intentions to determine desired state. void initState() { - _updateFields(); + for (AnimationIntention i in intentions) + i.initFields(this); } - void syncFields(AnimatedContainer source) { - child = source.child; - constraints = source.constraints; - decoration = source.decoration; - margin = source.margin; - padding = source.padding; - width = source.width; - height = source.height; - _updateFields(); - } - - void _updateFields() { - _updateConstraints(); - _updateDecoration(); - _updateMargin(); - _updatePadding(); - _updateTransform(); - _updateWidth(); - _updateHeight(); - } - - void _updateField(dynamic value, ImplicitlyAnimatedValue animatedValue, Function initField) { - if (animatedValue != null) - animatedValue.value = value; - else if (value != null) - initField(); - } - - void _updateConstraints() { - _updateField(constraints, _constraints, () { - _constraints = new ImplicitlyAnimatedValue(new AnimatedBoxConstraintsValue(constraints), duration); - watch(_constraints.performance); - }); - } - - void _updateDecoration() { - _updateField(decoration, _decoration, () { - _decoration = new ImplicitlyAnimatedValue(new AnimatedBoxDecorationValue(decoration), duration); - watch(_decoration.performance); - }); - } - - void _updateMargin() { - _updateField(margin, _margin, () { - _margin = new ImplicitlyAnimatedValue(new AnimatedEdgeDimsValue(margin), duration); - watch(_margin.performance); - }); - } - - void _updatePadding() { - _updateField(padding, _padding, () { - _padding = new ImplicitlyAnimatedValue(new AnimatedEdgeDimsValue(padding), duration); - watch(_padding.performance); - }); - } - - void _updateTransform() { - _updateField(transform, _transform, () { - _transform = new ImplicitlyAnimatedValue(new AnimatedValue(transform), duration); - watch(_transform.performance); - }); - } - - void _updateWidth() { - _updateField(width, _width, () { - _width = new ImplicitlyAnimatedValue(new AnimatedValue(width), duration); - watch(_width.performance); - }); - } - - void _updateHeight() { - _updateField(height, _height, () { - _height = new ImplicitlyAnimatedValue( new AnimatedValue(height), duration); - watch(_height.performance); - }); - } - - dynamic _getValue(dynamic value, ImplicitlyAnimatedValue animatedValue) { - return animatedValue == null ? value : animatedValue.value; + void syncFields(AnimatedContainer updated) { + child = updated.child; + for (AnimationIntention i in intentions) + i.syncFields(this, updated); } Widget build() { return new Container( child: child, - constraints: _getValue(constraints, _constraints), - decoration: _getValue(decoration, _decoration), - margin: _getValue(margin, _margin), - padding: _getValue(padding, _padding), - transform: _getValue(transform, _transform), - width: _getValue(width, _width), - height: _getValue(height, _height) + constraints: constraints, + decoration: decoration, + margin: margin, + padding: padding, + transform: transform, + width: width, + height: height ); } } diff --git a/sky/packages/sky/lib/widgets/material.dart b/sky/packages/sky/lib/widgets/material.dart index 0a28dcd940e..ccf2a787a07 100644 --- a/sky/packages/sky/lib/widgets/material.dart +++ b/sky/packages/sky/lib/widgets/material.dart @@ -49,7 +49,7 @@ class Material extends Component { Widget build() { return new AnimatedContainer( - duration: const Duration(milliseconds: 200), + intentions: implicitlySyncFieldsIntention(const Duration(milliseconds: 200)), decoration: new BoxDecoration( backgroundColor: _backgroundColor, borderRadius: edges[type], diff --git a/sky/packages/sky/lib/widgets/snack_bar.dart b/sky/packages/sky/lib/widgets/snack_bar.dart index 42ffc703ddb..57f37e9786d 100644 --- a/sky/packages/sky/lib/widgets/snack_bar.dart +++ b/sky/packages/sky/lib/widgets/snack_bar.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +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/animated_component.dart'; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/default_text_style.dart'; @@ -43,7 +46,58 @@ class SnackBarAction extends Component { } } -class SnackBar extends AnimatedComponent { +// TODO(mpcomplete): generalize this to a SlideIn class. +class SnackBarSlideInIntention extends AnimationIntention { + SnackBarSlideInIntention(this.duration, this.onStatusChanged); + + Duration duration; + SnackBarStatusChangedCallback onStatusChanged; + AnimatedValue _position; + 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; + if (container.tag) + _show(); + } + + void syncFields(AnimatedContainer original, AnimatedContainer updated) { + if (original.tag != updated.tag) { + original.tag = updated.tag; + original.tag ? _show() : _hide(); + } + } + + 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 { SnackBar({ Key key, @@ -60,48 +114,19 @@ class SnackBar extends AnimatedComponent { bool showing; SnackBarStatusChangedCallback onStatusChanged; + SnackBarSlideInIntention _intention; + + void initState() { + _intention = new SnackBarSlideInIntention(_kSlideInDuration, onStatusChanged); + } + void syncFields(SnackBar source) { content = source.content; actions = source.actions; onStatusChanged = source.onStatusChanged; - if (showing != source.showing) { - showing = source.showing; - showing ? _show() : _hide(); - } + showing = source.showing; } - AnimatedValue _position; - AnimationPerformance _performance; - - void initState() { - _position = new AnimatedValue(new Point(0.0, 50.0), end: Point.origin); - _performance = new AnimationPerformance() - ..duration = _kSlideInDuration - ..variable = _position - ..addListener(_checkStatusChanged); - watch(_performance); - if (showing) - _show(); - } - - void _show() { - _performance.play(); - } - - void _hide() { - _performance.reverse(); - } - - SnackBarStatus _lastStatus; - void _checkStatusChanged() { - SnackBarStatus status = _status; - if (_lastStatus != null && status != _lastStatus && onStatusChanged != null) - onStatusChanged(status); - _lastStatus = status; - } - - SnackBarStatus get _status => _performance.isDismissed ? SnackBarStatus.inactive : SnackBarStatus.active; - Widget build() { List children = [ new Flexible( @@ -115,11 +140,10 @@ class SnackBar extends AnimatedComponent { ) ]..addAll(actions); - Matrix4 transform = new Matrix4.identity(); - transform.translate(_position.value.x, _position.value.y); - return new Transform( - transform: transform, - child: new Material( + return new AnimatedContainer( + intentions: [_intention], + tag: showing, + child: new Material( level: 2, color: const Color(0xFF323232), type: MaterialType.canvas,