Refactor Material animation to use AnimatedContainer
The idea is that AnimatedContainer is a drop-in replacement for Container that
provides implicit animations when its properties change.
R=mpcomplete@google.com
diff --git a/sky/engine/core/painting/Color.dart b/sky/engine/core/painting/Color.dart
index 266145e..d78fc53 100644
--- a/sky/engine/core/painting/Color.dart
+++ b/sky/engine/core/painting/Color.dart
@@ -22,6 +22,22 @@
bool operator ==(other) => other is Color && _value == other._value;
+ Color withAlpha(int a) {
+ return new Color.fromARGB(a, red, green, blue);
+ }
+
+ Color withRed(int r) {
+ return new Color.fromARGB(alpha, r, green, blue);
+ }
+
+ Color withGreen(int g) {
+ return new Color.fromARGB(alpha, red, g, blue);
+ }
+
+ Color withBlue(int b) {
+ return new Color.fromARGB(alpha, red, green, b);
+ }
+
int get hashCode => _value.hashCode;
String toString() => "Color(0x${_value.toRadixString(16).padLeft(8, '0')})";
}
diff --git a/sky/sdk/BUILD.gn b/sky/sdk/BUILD.gn
index b78bbc2..e13e38d 100644
--- a/sky/sdk/BUILD.gn
+++ b/sky/sdk/BUILD.gn
@@ -50,6 +50,7 @@
"lib/theme/typography.dart",
"lib/theme/view_configuration.dart",
"lib/widgets/animated_component.dart",
+ "lib/widgets/animated_container.dart",
"lib/widgets/animation_builder.dart",
"lib/widgets/basic.dart",
"lib/widgets/block_viewport.dart",
diff --git a/sky/sdk/lib/base/lerp.dart b/sky/sdk/lib/base/lerp.dart
index c40934c..db4b8d9 100644
--- a/sky/sdk/lib/base/lerp.dart
+++ b/sky/sdk/lib/base/lerp.dart
@@ -4,9 +4,27 @@
import 'dart:sky';
-num lerpNum(num a, num b, double t) => a + (b - a) * t;
+num lerpNum(num a, num b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ a = 0.0;
+ if (b == null)
+ b = 0.0;
+ return a + (b - a) * t;
+}
+
+Color _scaleAlpha(Color a, double factor) {
+ return a.withAlpha((a.alpha * factor).round());
+}
Color lerpColor(Color a, Color b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ return _scaleAlpha(b, t);
+ if (b == null)
+ return _scaleAlpha(b, 1.0 - t);
return new Color.fromARGB(
lerpNum(a.alpha, b.alpha, t).toInt(),
lerpNum(a.red, b.red, t).toInt(),
@@ -15,5 +33,11 @@
}
Offset lerpOffset(Offset a, Offset b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ return b * t;
+ if (b == null)
+ return a * (1.0 - t);
return new Offset(lerpNum(a.dx, b.dx, t), lerpNum(a.dy, b.dy, t));
}
diff --git a/sky/sdk/lib/painting/box_painter.dart b/sky/sdk/lib/painting/box_painter.dart
index c7c7e4c..33039ca 100644
--- a/sky/sdk/lib/painting/box_painter.dart
+++ b/sky/sdk/lib/painting/box_painter.dart
@@ -70,16 +70,48 @@
final Offset offset;
final double blur;
+ BoxShadow scale(double factor) {
+ return new BoxShadow(
+ color: color,
+ offset: offset * factor,
+ blur: blur * factor
+ );
+ }
+
String toString() => 'BoxShadow($color, $offset, $blur)';
}
BoxShadow lerpBoxShadow(BoxShadow a, BoxShadow b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ return b.scale(t);
+ if (b == null)
+ return a.scale(1.0 - t);
return new BoxShadow(
color: lerpColor(a.color, b.color, t),
offset: lerpOffset(a.offset, b.offset, t),
blur: lerpNum(a.blur, b.blur, t));
}
+List<BoxShadow> lerpListBoxShadow(List<BoxShadow> a, List<BoxShadow> b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ a = new List<BoxShadow>();
+ if (b == null)
+ b = new List<BoxShadow>();
+ List<BoxShadow> result = new List<BoxShadow>();
+ int commonLength = math.min(a.length, b.length);
+ for (int i = 0; i < commonLength; ++i)
+ result.add(lerpBoxShadow(a[i], b[i], t));
+ for (int i = commonLength; i < a.length; ++i)
+ result.add(a[i].scale(1.0 - t));
+ for (int i = commonLength; i < b.length; ++i)
+ result.add(b[i].scale(t));
+ return result;
+}
+
abstract class Gradient {
sky.Shader createShader();
}
@@ -198,6 +230,19 @@
final Gradient gradient;
final Shape shape;
+ BoxDecoration scale(double factor) {
+ // TODO(abarth): Scale ALL the things.
+ return new BoxDecoration(
+ backgroundColor: lerpColor(null, backgroundColor, factor),
+ backgroundImage: backgroundImage,
+ border: border,
+ borderRadius: lerpNum(null, borderRadius, factor),
+ boxShadow: lerpListBoxShadow(null, boxShadow, factor),
+ gradient: gradient,
+ shape: shape
+ );
+ }
+
String toString([String prefix = '']) {
List<String> result = [];
if (backgroundColor != null)
@@ -220,6 +265,25 @@
}
}
+BoxDecoration lerpBoxDecoration(BoxDecoration a, BoxDecoration b, double t) {
+ if (a == null && b == null)
+ return null;
+ if (a == null)
+ return b.scale(t);
+ if (b == null)
+ return a.scale(1.0 - t);
+ // TODO(abarth): lerp ALL the fields.
+ return new BoxDecoration(
+ backgroundColor: lerpColor(a.backgroundColor, b.backgroundColor, t),
+ backgroundImage: b.backgroundImage,
+ border: b.border,
+ borderRadius: lerpNum(a.borderRadius, b.borderRadius, t),
+ boxShadow: lerpListBoxShadow(a.boxShadow, b.boxShadow, t),
+ gradient: b.gradient,
+ shape: b.shape
+ );
+}
+
class BoxPainter {
BoxPainter(BoxDecoration decoration) : _decoration = decoration {
assert(decoration != null);
diff --git a/sky/sdk/lib/widgets/animated_container.dart b/sky/sdk/lib/widgets/animated_container.dart
new file mode 100644
index 0000000..1898664
--- /dev/null
+++ b/sky/sdk/lib/widgets/animated_container.dart
@@ -0,0 +1,206 @@
+// 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.
+
+import 'package:vector_math/vector_math.dart';
+
+import 'package:sky/animation/animation_performance.dart';
+import 'package:sky/animation/curves.dart';
+import 'package:sky/base/lerp.dart';
+import 'package:sky/painting/box_painter.dart';
+import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets/animated_component.dart';
+
+class AnimatedBoxConstraintsValue extends AnimatedType<BoxConstraints> {
+ AnimatedBoxConstraintsValue(BoxConstraints begin, { BoxConstraints end, Curve curve: linear })
+ : super(begin, end: end, curve: curve);
+
+ void setFraction(double t) {
+ // TODO(abarth): We should lerp the BoxConstraints.
+ value = end;
+ }
+}
+
+class AnimatedBoxDecorationValue extends AnimatedType<BoxDecoration> {
+ AnimatedBoxDecorationValue(BoxDecoration begin, { BoxDecoration end, Curve curve: linear })
+ : super(begin, end: end, curve: curve);
+
+ void setFraction(double t) {
+ if (t == 1.0) {
+ value = end;
+ return;
+ }
+ value = lerpBoxDecoration(begin, end, t);
+ }
+}
+
+class AnimatedEdgeDimsValue extends AnimatedType<EdgeDims> {
+ AnimatedEdgeDimsValue(EdgeDims begin, { EdgeDims end, Curve curve: linear })
+ : super(begin, end: end, curve: curve);
+
+ void setFraction(double t) {
+ if (t == 1.0) {
+ value = end;
+ return;
+ }
+ value = new EdgeDims(
+ lerpNum(begin.top, end.top, t),
+ lerpNum(begin.right, end.right, t),
+ lerpNum(begin.bottom, end.bottom, t),
+ lerpNum(begin.bottom, end.left, t)
+ );
+ }
+}
+
+class ImplicitlyAnimatedValue<T> {
+ final AnimationPerformance performance = new AnimationPerformance();
+ final AnimatedType<T> _variable;
+
+ ImplicitlyAnimatedValue(this._variable, Duration duration) {
+ performance
+ ..variable = _variable
+ ..duration = duration;
+ }
+
+ T get value => _variable.value;
+ void set value(T newValue) {
+ _variable.begin = _variable.value;
+ _variable.end = newValue;
+ if (_variable.value != _variable.end) {
+ performance
+ ..progress = 0.0
+ ..play();
+ }
+ }
+}
+
+class AnimatedContainer extends AnimatedComponent {
+ AnimatedContainer({
+ String key,
+ this.child,
+ this.duration,
+ this.constraints,
+ this.decoration,
+ this.width,
+ this.height,
+ this.margin,
+ this.padding,
+ this.transform
+ }) : super(key: key);
+
+ Widget child;
+ Duration duration; // TODO(abarth): Support separate durations for each value.
+ BoxConstraints constraints;
+ BoxDecoration decoration;
+ EdgeDims margin;
+ EdgeDims padding;
+ Matrix4 transform;
+ double width;
+ double height;
+
+ ImplicitlyAnimatedValue<BoxConstraints> _constraints;
+ ImplicitlyAnimatedValue<BoxDecoration> _decoration;
+ ImplicitlyAnimatedValue<EdgeDims> _margin;
+ ImplicitlyAnimatedValue<EdgeDims> _padding;
+ ImplicitlyAnimatedValue<Matrix4> _transform;
+ ImplicitlyAnimatedValue<double> _width;
+ ImplicitlyAnimatedValue<double> _height;
+
+ void initState() {
+ _updateFields();
+ }
+
+ 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<BoxConstraints>(new AnimatedBoxConstraintsValue(constraints), duration);
+ watch(_constraints.performance);
+ });
+ }
+
+ void _updateDecoration() {
+ _updateField(decoration, _decoration, () {
+ _decoration = new ImplicitlyAnimatedValue<BoxDecoration>(new AnimatedBoxDecorationValue(decoration), duration);
+ watch(_decoration.performance);
+ });
+ }
+
+ void _updateMargin() {
+ _updateField(margin, _margin, () {
+ _margin = new ImplicitlyAnimatedValue<EdgeDims>(new AnimatedEdgeDimsValue(margin), duration);
+ watch(_margin.performance);
+ });
+ }
+
+ void _updatePadding() {
+ _updateField(padding, _padding, () {
+ _padding = new ImplicitlyAnimatedValue<EdgeDims>(new AnimatedEdgeDimsValue(padding), duration);
+ watch(_padding.performance);
+ });
+ }
+
+ void _updateTransform() {
+ _updateField(transform, _transform, () {
+ _transform = new ImplicitlyAnimatedValue<Matrix4>(new AnimatedType<Matrix4>(transform), duration);
+ watch(_transform.performance);
+ });
+ }
+
+ void _updateWidth() {
+ _updateField(width, _width, () {
+ _width = new ImplicitlyAnimatedValue<double>(new AnimatedType<double>(width), duration);
+ watch(_width.performance);
+ });
+ }
+
+ void _updateHeight() {
+ _updateField(height, _height, () {
+ _height = new ImplicitlyAnimatedValue<double>( new AnimatedType<double>(height), duration);
+ watch(_height.performance);
+ });
+ }
+
+ dynamic _getValue(dynamic value, ImplicitlyAnimatedValue animatedValue) {
+ return animatedValue == null ? value : animatedValue.value;
+ }
+
+ 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)
+ );
+ }
+}
diff --git a/sky/sdk/lib/widgets/material.dart b/sky/sdk/lib/widgets/material.dart
index 4efcae3..aa44a68 100644
--- a/sky/sdk/lib/widgets/material.dart
+++ b/sky/sdk/lib/widgets/material.dart
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:sky/animation/animation_performance.dart';
import 'package:sky/painting/box_painter.dart';
-import 'package:sky/widgets/animated_component.dart';
-import 'package:sky/widgets/animation_builder.dart';
+import 'package:sky/theme/shadows.dart';
+import 'package:sky/widgets/animated_container.dart';
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/default_text_style.dart';
import 'package:sky/widgets/theme.dart';
@@ -19,11 +18,7 @@
MaterialType.button: 2.0,
};
-const Duration _kAnimateShadowDuration = const Duration(milliseconds: 100);
-const Duration _kAnimateColorDuration = const Duration(milliseconds: 100);
-
-class Material extends AnimatedComponent {
-
+class Material extends Component {
Material({
String key,
this.child,
@@ -34,64 +29,37 @@
assert(level != null);
}
- Widget child;
- MaterialType type;
- int level;
- Color color;
+ final Widget child;
+ final MaterialType type;
+ final int level;
+ final Color color;
- final AnimationBuilder _builder = new AnimationBuilder();
-
- void initState() {
- _builder
- ..shadow = new AnimatedType<double>(level.toDouble())
- ..backgroundColor = _getBackgroundColor(type, color)
- ..borderRadius = edges[type]
- ..shape = type == MaterialType.circle ? Shape.circle : Shape.rectangle;
- watch(_builder.createPerformance(
- [_builder.shadow],
- duration: _kAnimateShadowDuration
- ));
- watch(_builder.createPerformance(
- [_builder.backgroundColor],
- duration: _kAnimateColorDuration
- ));
- super.initState();
- }
-
- void syncFields(Material source) {
- child = source.child;
- type = source.type;
- level = source.level;
- color = source.color;
- _builder.updateFields(
- shadow: new AnimatedType<double>(level.toDouble()),
- backgroundColor: _getBackgroundColor(type, color),
- borderRadius: edges[type],
- shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
- );
- super.syncFields(source);
- }
-
- AnimatedColor _getBackgroundColor(MaterialType type, Color color) {
- if (color == null) {
- switch (type) {
- case MaterialType.canvas:
- color = Theme.of(this).canvasColor;
- break;
- case MaterialType.card:
- color = Theme.of(this).cardColor;
- break;
- default:
- break;
- }
+ Color get _backgroundColor {
+ if (color != null)
+ return color;
+ switch (type) {
+ case MaterialType.canvas:
+ return Theme.of(this).canvasColor;
+ case MaterialType.card:
+ return Theme.of(this).cardColor;
+ default:
+ return null;
}
- return color == null ? null : new AnimatedColor(color);
}
Widget build() {
- return _builder.build(
- new DefaultTextStyle(style: Theme.of(this).text.body1, child: child)
+ return new AnimatedContainer(
+ duration: const Duration(milliseconds: 1000),
+ decoration: new BoxDecoration(
+ backgroundColor: _backgroundColor,
+ borderRadius: edges[type],
+ boxShadow: level == 0 ? null : shadows[level],
+ shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
+ ),
+ child: new DefaultTextStyle(
+ style: Theme.of(this).text.body1,
+ child: child
+ )
);
}
-
}
diff --git a/sky/tests/examples/sector-expected.txt b/sky/tests/examples/sector-expected.txt
index f7e9dbe..bd6f075 100644
--- a/sky/tests/examples/sector-expected.txt
+++ b/sky/tests/examples/sector-expected.txt
@@ -189,7 +189,7 @@
3 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
3 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-3 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+3 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
3 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
3 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)
@@ -318,7 +318,7 @@
4 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
4 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
4 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-4 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+4 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
4 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
4 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
4 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)
@@ -450,7 +450,7 @@
5 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
5 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
5 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-5 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+5 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
5 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
5 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
5 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)
@@ -585,7 +585,7 @@
6 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
6 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
6 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-6 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+6 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
6 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
6 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
6 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)
@@ -717,7 +717,7 @@
7 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
7 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
7 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-7 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+7 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
7 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
7 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
7 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)
@@ -846,7 +846,7 @@
8 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
8 | | | | | | | | | paintChild RenderDecoratedBox at Point(496.75, 89.0)
8 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
-8 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6)))
+8 | | | | | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffd6d6d6), drawLooper:true))
8 | | | | | | | | | | paintChild RenderInkWell at Point(496.75, 89.0)
8 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
8 | | | | | | | | | | | paintChild RenderPadding at Point(496.75, 89.0)