Make BoxDecoration lerp gradients (#12451)

This still is very limited in what it can lerp, but it sets the stage for arbitrary lerps later.
diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart
index a008430..690c9e0 100644
--- a/packages/flutter/lib/src/painting/box_decoration.dart
+++ b/packages/flutter/lib/src/painting/box_decoration.dart
@@ -148,14 +148,13 @@
 
   /// Returns a new box decoration that is scaled by the given factor.
   BoxDecoration scale(double factor) {
-    // TODO(abarth): Scale ALL the things.
     return new BoxDecoration(
       color: Color.lerp(null, color, factor),
-      image: image,
+      image: image, // TODO(ianh): fade the image from transparent
       border: BoxBorder.lerp(null, border, factor),
       borderRadius: BorderRadius.lerp(null, borderRadius, factor),
       boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
-      gradient: gradient,
+      gradient: gradient?.scale(factor),
       shape: shape,
     );
   }
@@ -165,6 +164,8 @@
 
   @override
   BoxDecoration lerpFrom(Decoration a, double t) {
+    if (a == null)
+      return scale(t);
     if (a is BoxDecoration)
       return BoxDecoration.lerp(a, this, t);
     return super.lerpFrom(a, t);
@@ -172,6 +173,8 @@
 
   @override
   BoxDecoration lerpTo(Decoration b, double t) {
+    if (b == null)
+      return scale(1.0 - t);
     if (b is BoxDecoration)
       return BoxDecoration.lerp(this, b, t);
     return super.lerpTo(b, t);
@@ -181,6 +184,16 @@
   ///
   /// Interpolates each parameter of the box decoration separately.
   ///
+  /// The [shape] is not interpolated. To interpolate the shape, consider using
+  /// a [ShapeDecoration] with different border shapes.
+  ///
+  /// If both values are null, this returns null. Otherwise, it returns a
+  /// non-null value. If one of the values is null, then the result is obtained
+  /// by applying [scale] to the other value. If neither value is null and `t ==
+  /// 0.0`, then `a` is returned unmodified; if `t == 1.0` then `b` is returned
+  /// unmodified. Otherwise, the values are computed by interpolating the
+  /// properties appropriately.
+  ///
   /// See also:
   ///
   ///  * [Decoration.lerp], which can interpolate between any two types of
@@ -195,14 +208,17 @@
       return b.scale(t);
     if (b == null)
       return a.scale(1.0 - t);
-    // TODO(abarth): lerp ALL the fields.
+    if (t == 0.0)
+      return a;
+    if (t == 1.0)
+      return b;
     return new BoxDecoration(
       color: Color.lerp(a.color, b.color, t),
-      image: t < 0.5 ? a.image : b.image,
+      image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image
       border: BoxBorder.lerp(a.border, b.border, t),
       borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
       boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
-      gradient: t < 0.5 ? a.gradient : b.gradient,
+      gradient: Gradient.lerp(a.gradient, b.gradient, t),
       shape: t < 0.5 ? a.shape : b.shape,
     );
   }
diff --git a/packages/flutter/lib/src/painting/decoration.dart b/packages/flutter/lib/src/painting/decoration.dart
index 3595f0c..fb32d99 100644
--- a/packages/flutter/lib/src/painting/decoration.dart
+++ b/packages/flutter/lib/src/painting/decoration.dart
@@ -95,26 +95,26 @@
 
   /// Linearly interpolates from `begin` to `end`.
   ///
-  /// This defers to `end`'s [lerpTo] function if `end` is not null. If `end` is
-  /// null or if its [lerpTo] returns null, it uses `begin`'s [lerpFrom]
-  /// function instead. If both return null, it attempts to lerp from `begin` to
-  /// null if `t<0.5`, or null to `end` if `t≥0.5`.
+  /// This attempts to use [lerpFrom] and [lerpTo] on `end` and `begin`
+  /// respectively to find a solution. If the two values can't directly be
+  /// interpolated, then the interpolation is done via null (at `t == 0.5`).
+  ///
+  /// If the values aren't null, then for `t == 0.0` and `t == 1.0` the values
+  /// `begin` and `end` are return verbatim.
   static Decoration lerp(Decoration begin, Decoration end, double t) {
-    Decoration result;
-    if (end != null)
-      result = end.lerpFrom(begin, t);
-    if (result == null && begin != null)
-      result = begin.lerpTo(end, t);
-    if (result == null && begin != null && end != null) {
-      if (t < 0.5) {
-        result = begin.lerpTo(null, t * 2.0);
-      } else {
-        result = end.lerpFrom(null, (t - 0.5) * 2.0);
-      }
-    }
-    if (result == null)
-      result = t < 0.5 ? begin : end;
-    return result;
+    if (begin == null && end == null)
+      return null;
+    if (begin == null)
+      return end.lerpFrom(null, t) ?? end;
+    if (end == null)
+      return begin.lerpTo(null, t) ?? begin;
+    if (t == 0.0)
+      return begin;
+    if (t == 1.0)
+      return end;
+    return end.lerpFrom(begin, t)
+        ?? begin.lerpTo(end, t)
+        ?? (t < 0.5 ? begin.lerpTo(null, t * 2.0) : end.lerpFrom(null, (t - 0.5) * 2.0));
   }
 
   /// Tests whether the given point, on a rectangle of a given size,
diff --git a/packages/flutter/lib/src/painting/flutter_logo.dart b/packages/flutter/lib/src/painting/flutter_logo.dart
index a058d62..ee3449c 100644
--- a/packages/flutter/lib/src/painting/flutter_logo.dart
+++ b/packages/flutter/lib/src/painting/flutter_logo.dart
@@ -123,6 +123,13 @@
   ///
   /// Interpolates both the color and the style in a continuous fashion.
   ///
+  /// If both values are null, this returns null. Otherwise, it returns a
+  /// non-null value. If one of the values is null, then the result is obtained
+  /// by scaling the other value's opacity and [margin]. If neither value is
+  /// null and `t == 0.0`, then `a` is returned unmodified; if `t == 1.0` then
+  /// `b` is returned unmodified. Otherwise, the values are computed by
+  /// interpolating the properties appropriately.
+  ///
   /// See also [Decoration.lerp].
   static FlutterLogoDecoration lerp(FlutterLogoDecoration a, FlutterLogoDecoration b, double t) {
     assert(a == null || a.debugAssertIsValid());
@@ -151,6 +158,10 @@
         a._opacity * (1.0 - t).clamp(0.0, 1.0),
       );
     }
+    if (t == 0.0)
+      return a;
+    if (t == 1.0)
+      return b;
     return new FlutterLogoDecoration._(
       Color.lerp(a.lightColor, b.lightColor, t),
       Color.lerp(a.darkColor, b.darkColor, t),
diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart
index 510677c..14cc35f 100644
--- a/packages/flutter/lib/src/painting/gradient.dart
+++ b/packages/flutter/lib/src/painting/gradient.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:math' as math;
 import 'dart:ui' as ui show Gradient, lerpDouble;
 
 import 'package:flutter/foundation.dart';
@@ -9,6 +10,30 @@
 import 'alignment.dart';
 import 'basic_types.dart';
 
+class _ColorsAndStops {
+  _ColorsAndStops(this.colors, this.stops);
+  final List<Color> colors;
+  final List<double> stops;
+}
+
+_ColorsAndStops interpolateColorsAndStops(List<Color> aColors, List<double> aStops, List<Color> bColors, List<double> bStops, double t) {
+  assert(aColors.length == bColors.length, 'Cannot interpolate between two gradients with a different number of colors.'); // TODO(ianh): remove limitation
+  assert((aStops == null && aColors.length == 2) || (aStops != null && aStops.length == aColors.length));
+  assert((bStops == null && bColors.length == 2) || (bStops != null && bStops.length == bColors.length));
+  final List<Color> interpolatedColors = <Color>[];
+  for (int i = 0; i < aColors.length; i += 1)
+    interpolatedColors.add(Color.lerp(aColors[i], bColors[i], t));
+  List<double> interpolatedStops;
+  if (aStops != null || bStops != null) {
+    aStops ??= const <double>[0.0, 1.0];
+    bStops ??= const <double>[0.0, 1.0];
+    assert(aStops.length == bStops.length);
+    for (int i = 0; i < aStops.length; i += 1)
+      interpolatedStops.add(ui.lerpDouble(aStops[i], bStops[i], t).clamp(0.0, 1.0));
+  }
+  return new _ColorsAndStops(interpolatedColors, interpolatedStops);
+}
+
 /// A 2D gradient.
 ///
 /// This is an interface that allows [LinearGradient] and [RadialGradient]
@@ -30,6 +55,73 @@
   /// it uses [AlignmentDirectional] objects instead of [Alignment]
   /// objects, then the `textDirection` argument must not be null.
   Shader createShader(Rect rect, { TextDirection textDirection });
+
+  /// Returns a new gradient with its properties scaled by the given factor.
+  ///
+  /// A factor of 0.0 (or less) should result in a variant of the gradient that
+  /// is invisible; any two factors epsilon apart should be unnoticeably
+  /// different from each other at first glance. From this it follows that
+  /// scaling a gradient with values from 1.0 to 0.0 over time should cause the
+  /// gradient to smoothly disappear.
+  ///
+  /// Typically this is the same as interpolating from null (with [lerp]).
+  Gradient scale(double factor);
+
+  /// Linearly interpolates from `a` to [this].
+  ///
+  /// When implementing this method in subclasses, return null if this class
+  /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
+  /// method instead.
+  ///
+  /// If `a` is null, this must not return null. The base class implements this
+  /// by deferring to [scale].
+  ///
+  /// Instead of calling this directly, use [Gradient.lerp].
+  @protected
+  Gradient lerpFrom(Gradient a, double t) {
+    if (a == null)
+      return scale(t);
+    return null;
+  }
+
+  /// Linearly interpolates from [this] to `b`.
+  ///
+  /// This is called if `b`'s [lerpTo] did not know how to handle this class.
+  ///
+  /// When implementing this method in subclasses, return null if this class
+  /// cannot interpolate from `b`. In that case, [lerp] will apply a default
+  /// behavior instead.
+  ///
+  /// If `b` is null, this must not return null. The base class implements this
+  /// by deferring to [scale].
+  ///
+  /// Instead of calling this directly, use [Gradient.lerp].
+  @protected
+  Gradient lerpTo(Gradient b, double t) {
+    if (b == null)
+      return scale(1.0 - t);
+    return null;
+  }
+
+  /// Linearly interpolates from `begin` to `end`.
+  ///
+  /// This defers to `end`'s [lerpTo] function if `end` is not null. If `end` is
+  /// null or if its [lerpTo] returns null, it uses `begin`'s [lerpFrom]
+  /// function instead. If both return null, it returns `begin` before `t=0.5`
+  /// and `end` after `t=0.5`.
+  static Gradient lerp(Gradient begin, Gradient end, double t) {
+    Gradient result;
+    if (end != null)
+      result = end.lerpFrom(begin, t); // if begin is null, this must return non-null
+    if (result == null && begin != null)
+      result = begin.lerpTo(end, t); // if end is null, this must return non-null
+    if (result != null)
+      return result;
+    if (begin == null && end == null)
+      return null;
+    assert(begin != null && end != null);
+    return t < 0.5 ? begin.scale(1.0 - (t * 2.0)) : end.scale((t - 0.5) * 2.0);
+  }
 }
 
 /// A 2D linear gradient.
@@ -173,9 +265,8 @@
   /// Returns a new [LinearGradient] with its properties (in particular the
   /// colors) scaled by the given factor.
   ///
-  /// If the factor is 1.0 or greater, then the gradient is returned unmodified.
   /// If the factor is 0.0 or less, then the gradient is fully transparent.
-  /// Values in between scale the opacity of the colors.
+  @override
   LinearGradient scale(double factor) {
     return new LinearGradient(
       begin: begin,
@@ -186,6 +277,20 @@
     );
   }
 
+  @override
+  Gradient lerpFrom(Gradient a, double t) {
+    if (a == null || (a is LinearGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+      return LinearGradient.lerp(a, this, t);
+    return super.lerpFrom(a, t);
+  }
+
+  @override
+  Gradient lerpTo(Gradient b, double t) {
+    if (b == null || (b is LinearGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
+      return LinearGradient.lerp(this, b, t);
+    return super.lerpTo(b, t);
+  }
+
   /// Linearly interpolate between two [LinearGradient]s.
   ///
   /// If either gradient is null, this function linearly interpolates from a
@@ -200,24 +305,13 @@
       return b.scale(t);
     if (b == null)
       return a.scale(1.0 - t);
-    assert(a.colors.length == b.colors.length, 'Cannot interpolate between two gradients with a different number of colors.');
-    assert(a.stops == null || b.stops == null || a.stops.length == b.stops.length);
-    final List<Color> interpolatedColors = <Color>[];
-    for (int i = 0; i < a.colors.length; i += 1)
-      interpolatedColors.add(Color.lerp(a.colors[i], b.colors[i], t));
-    List<double> interpolatedStops;
-    if (a.stops != null && b.stops != null) {
-      for (int i = 0; i < a.stops.length; i += 1)
-        interpolatedStops.add(ui.lerpDouble(a.stops[i], b.stops[i], t));
-    } else {
-      interpolatedStops = a.stops ?? b.stops;
-    }
+    final _ColorsAndStops interpolated = interpolateColorsAndStops(a.colors, a.stops, b.colors, b.stops, t);
     return new LinearGradient(
       begin: AlignmentGeometry.lerp(a.begin, b.begin, t),
       end: AlignmentGeometry.lerp(a.end, b.end, t),
-      colors: interpolatedColors,
-      stops: interpolatedStops,
-      tileMode: t < 0.5 ? a.tileMode : b.tileMode,
+      colors: interpolated.colors,
+      stops: interpolated.stops,
+      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
     );
   }
 
@@ -403,6 +497,58 @@
     );
   }
 
+  /// Returns a new [RadialGradient] with its colors scaled by the given factor.
+  ///
+  /// If the factor is 0.0 or less, then the gradient is fully transparent.
+  @override
+  RadialGradient scale(double factor) {
+    return new RadialGradient(
+      center: center,
+      radius: radius,
+      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
+      stops: stops,
+      tileMode: tileMode,
+    );
+  }
+
+  @override
+  Gradient lerpFrom(Gradient a, double t) {
+    if (a == null || (a is RadialGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+      return RadialGradient.lerp(a, this, t);
+    return super.lerpFrom(a, t);
+  }
+
+  @override
+  Gradient lerpTo(Gradient b, double t) {
+    if (b == null || (b is RadialGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
+      return RadialGradient.lerp(this, b, t);
+    return super.lerpTo(b, t);
+  }
+
+  /// Linearly interpolate between two [RadialGradient]s.
+  ///
+  /// If either gradient is null, this function linearly interpolates from a
+  /// a gradient that matches the other gradient in [center], [radius], [stops] and
+  /// [tileMode] and with the same [colors] but transparent (using [scale]).
+  ///
+  /// If neither gradient is null, they must have the same number of [colors].
+  static RadialGradient lerp(RadialGradient a, RadialGradient 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);
+    final _ColorsAndStops interpolated = interpolateColorsAndStops(a.colors, a.stops, b.colors, b.stops, t);
+    return new RadialGradient(
+      center: AlignmentGeometry.lerp(a.center, b.center, t),
+      radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)),
+      colors: interpolated.colors,
+      stops: interpolated.stops,
+      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
+    );
+  }
+
   @override
   bool operator ==(dynamic other) {
     if (identical(this, other))
diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart
index 96c8af5..039f2b8 100644
--- a/packages/flutter/test/painting/decoration_test.dart
+++ b/packages/flutter/test/painting/decoration_test.dart
@@ -282,15 +282,14 @@
   });
 
   test('BoxDecoration.lerp - gradients', () {
-    // We don't lerp the gradients, we just switch from one to the other at t=0.5.
-    final Gradient gradient = new LinearGradient(colors: <Color>[ const Color(0x00000000), const Color(0xFFFFFFFF) ]);
+    final Gradient gradient = const LinearGradient(colors: const <Color>[ const Color(0x00000000), const Color(0xFFFFFFFF) ]);
     expect(
       BoxDecoration.lerp(
         const BoxDecoration(),
         new BoxDecoration(gradient: gradient),
         -1.0,
       ),
-      const BoxDecoration()
+      const BoxDecoration(gradient: const LinearGradient(colors: const <Color>[ const Color(0x00000000), const Color(0x00FFFFFF) ]))
     );
     expect(
       BoxDecoration.lerp(
@@ -306,7 +305,7 @@
         new BoxDecoration(gradient: gradient),
         0.25,
       ),
-      const BoxDecoration()
+      const BoxDecoration(gradient: const LinearGradient(colors: const <Color>[ const Color(0x00000000), const Color(0x40FFFFFF) ]))
     );
     expect(
       BoxDecoration.lerp(
@@ -314,7 +313,7 @@
         new BoxDecoration(gradient: gradient),
         0.75,
       ),
-      new BoxDecoration(gradient: gradient)
+      const BoxDecoration(gradient: const LinearGradient(colors: const <Color>[ const Color(0x00000000), const Color(0xBFFFFFFF) ]))
     );
     expect(
       BoxDecoration.lerp(
diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart
index de87252..44bae0b 100644
--- a/packages/flutter/test/painting/gradient_test.dart
+++ b/packages/flutter/test/painting/gradient_test.dart
@@ -38,7 +38,6 @@
         const Color(0x66666666),
       ],
     );
-
     final LinearGradient testGradient2 = const LinearGradient(
       begin: Alignment.topRight,
       end: Alignment.topLeft,
@@ -47,8 +46,8 @@
         const Color(0x88888888),
       ],
     );
-    final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5);
 
+    final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5);
     expect(actual, const LinearGradient(
       begin: const Alignment(0.0, -1.0),
       end: const Alignment(-1.0, 0.0),
@@ -152,4 +151,90 @@
       returnsNormally,
     );
   });
+
+  test('RadialGradient lerp test', () {
+    final RadialGradient testGradient1 = const RadialGradient(
+      center: Alignment.topLeft,
+      radius: 20.0,
+      colors: const <Color>[
+        const Color(0x33333333),
+        const Color(0x66666666),
+      ],
+    );
+    final RadialGradient testGradient2 = const RadialGradient(
+      center: Alignment.topRight,
+      radius: 10.0,
+      colors: const <Color>[
+        const Color(0x44444444),
+        const Color(0x88888888),
+      ],
+    );
+
+    final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const RadialGradient(
+      center: const Alignment(0.0, -1.0),
+      radius: 15.0,
+      colors: const <Color>[
+        const Color(0x3B3B3B3B),
+        const Color(0x77777777),
+      ],
+    ));
+  });
+
+  test('Gradient lerp test (with RadialGradient)', () {
+    final RadialGradient testGradient1 = const RadialGradient(
+      center: Alignment.topLeft,
+      radius: 20.0,
+      colors: const <Color>[
+        const Color(0x33333333),
+        const Color(0x66666666),
+      ],
+    );
+    final RadialGradient testGradient2 = const RadialGradient(
+      center: const Alignment(0.0, -1.0),
+      radius: 15.0,
+      colors: const <Color>[
+        const Color(0x3B3B3B3B),
+        const Color(0x77777777),
+      ],
+    );
+    final RadialGradient testGradient3 = const RadialGradient(
+      center: Alignment.topRight,
+      radius: 10.0,
+      colors: const <Color>[
+        const Color(0x44444444),
+        const Color(0x88888888),
+      ],
+    );
+
+    expect(Gradient.lerp(testGradient1, testGradient3, 0.0), testGradient1);
+    expect(Gradient.lerp(testGradient1, testGradient3, 0.5), testGradient2);
+    expect(Gradient.lerp(testGradient1, testGradient3, 1.0), testGradient3);
+    expect(Gradient.lerp(testGradient3, testGradient1, 0.0), testGradient3);
+    expect(Gradient.lerp(testGradient3, testGradient1, 0.5), testGradient2);
+    expect(Gradient.lerp(testGradient3, testGradient1, 1.0), testGradient1);
+  });
+
+  test('Gradient lerp test (LinearGradient to RadialGradient)', () {
+    final LinearGradient testGradient1 = const LinearGradient(
+      begin: Alignment.topLeft,
+      end: Alignment.bottomRight,
+      colors: const <Color>[
+        const Color(0x33333333),
+        const Color(0x66666666),
+      ],
+    );
+    final RadialGradient testGradient2 = const RadialGradient(
+      center: Alignment.center,
+      radius: 20.0,
+      colors: const <Color>[
+        const Color(0x44444444),
+        const Color(0x88888888),
+      ],
+    );
+
+    expect(Gradient.lerp(testGradient1, testGradient2, 0.0), testGradient1);
+    expect(Gradient.lerp(testGradient1, testGradient2, 1.0), testGradient2);
+    expect(Gradient.lerp(testGradient1, testGradient2, 0.5), testGradient2.scale(0.0));
+  });
 }
\ No newline at end of file