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)