Add lerping between Gradients with arbitrary number of colors and stops (#27435)

diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart
index a345b1f..9dd4784 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:collection';
 import 'dart:math' as math;
 import 'dart:ui' as ui show Gradient, lerpDouble;
 
@@ -16,22 +17,43 @@
   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);
-    interpolatedStops = <double>[];
-    for (int i = 0; i < aStops.length; i += 1)
-      interpolatedStops.add(ui.lerpDouble(aStops[i], bStops[i], t).clamp(0.0, 1.0));
-  }
+/// Calculate the color at position [t] of the gradient defined by [colors] and [stops].
+Color _sample(List<Color> colors, List<double> stops, double t) {
+  assert(colors != null);
+  assert(colors.isNotEmpty);
+  assert(stops != null);
+  assert(stops.isNotEmpty);
+  assert(t != null);
+  if (t <= stops.first)
+    return colors.first;
+  if (t >= stops.last)
+    return colors.last;
+  final int index = stops.lastIndexWhere((double s) => s <= t);
+  assert(index != -1);
+  return Color.lerp(
+      colors[index], colors[index + 1],
+      (t - stops[index]) / (stops[index + 1] - stops[index])
+  );
+}
+
+_ColorsAndStops _interpolateColorsAndStops(
+    List<Color> aColors,
+    List<double> aStops,
+    List<Color> bColors,
+    List<double> bStops,
+    double t,
+) {
+  assert(aColors.length >= 2);
+  assert(bColors.length >= 2);
+  assert(aStops.length == aColors.length);
+  assert(bStops.length == bColors.length);
+  final SplayTreeSet<double> stops = SplayTreeSet<double>()
+    ..addAll(aStops)
+    ..addAll(bStops);
+  final List<double> interpolatedStops = stops.toList(growable: false);
+  final List<Color> interpolatedColors = interpolatedStops.map<Color>(
+          (double stop) => Color.lerp(_sample(aColors, aStops, stop), _sample(bColors, bStops, stop), t)
+  ).toList(growable: false);
   return _ColorsAndStops(interpolatedColors, interpolatedStops);
 }
 
@@ -88,8 +110,6 @@
   List<double> _impliedStops() {
     if (stops != null)
       return stops;
-    if (colors.length == 2)
-      return null;
     assert(colors.length >= 2, 'colors list must have at least two colors');
     final double separation = 1.0 / (colors.length - 1);
     return List<double>.generate(
@@ -335,14 +355,14 @@
 
   @override
   Gradient lerpFrom(Gradient a, double t) {
-    if (a == null || (a is LinearGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+    if (a == null || (a is LinearGradient))
       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
+    if (b == null || (b is LinearGradient))
       return LinearGradient.lerp(this, b, t);
     return super.lerpTo(b, t);
   }
@@ -374,7 +394,13 @@
       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);
+    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
+        a.colors,
+        a._impliedStops(),
+        b.colors,
+        b._impliedStops(),
+        t,
+    );
     return LinearGradient(
       begin: AlignmentGeometry.lerp(a.begin, b.begin, t),
       end: AlignmentGeometry.lerp(a.end, b.end, t),
@@ -604,14 +630,14 @@
 
   @override
   Gradient lerpFrom(Gradient a, double t) {
-    if (a == null || (a is RadialGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+    if (a == null || (a is RadialGradient))
       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
+    if (b == null || (b is RadialGradient))
       return RadialGradient.lerp(this, b, t);
     return super.lerpTo(b, t);
   }
@@ -643,7 +669,13 @@
       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);
+    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
+        a.colors,
+        a._impliedStops(),
+        b.colors,
+        b._impliedStops(),
+        t,
+    );
     return RadialGradient(
       center: AlignmentGeometry.lerp(a.center, b.center, t),
       radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)),
@@ -835,14 +867,14 @@
 
   @override
   Gradient lerpFrom(Gradient a, double t) {
-    if (a == null || (a is SweepGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+    if (a == null || (a is SweepGradient))
       return SweepGradient.lerp(a, this, t);
     return super.lerpFrom(a, t);
   }
 
   @override
   Gradient lerpTo(Gradient b, double t) {
-    if (b == null || (b is SweepGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
+    if (b == null || (b is SweepGradient))
       return SweepGradient.lerp(this, b, t);
     return super.lerpTo(b, t);
   }
@@ -873,7 +905,13 @@
       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);
+    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
+        a.colors,
+        a._impliedStops(),
+        b.colors,
+        b._impliedStops(),
+        t,
+    );
     return SweepGradient(
       center: AlignmentGeometry.lerp(a.center, b.center, t),
       startAngle: math.max(0.0, ui.lerpDouble(a.startAngle, b.startAngle, t)),
diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart
index f9eb55e..963ffc2 100644
--- a/packages/flutter/test/painting/gradient_test.dart
+++ b/packages/flutter/test/painting/gradient_test.dart
@@ -56,6 +56,7 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[0, 1],
     ));
   });
 
@@ -91,11 +92,84 @@
       end: Alignment(-1.0, 0.0),
       colors: <Color>[
         Color(0x3B3B3B3B),
+        Color(0x55555555),
         Color(0x77777777),
       ],
       stops: <double>[
-        0.25,
-        0.75,
+        0.0,
+        0.5,
+        1.0,
+      ],
+    ));
+  });
+
+  test('LinearGradient lerp test with unequal number of colors', () {
+    const LinearGradient testGradient1 = LinearGradient(
+      colors: <Color>[
+        Color(0x22222222),
+        Color(0x66666666),
+      ],
+    );
+    const LinearGradient testGradient2 = LinearGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x66666666),
+        Color(0x88888888),
+      ],
+    );
+
+    final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const LinearGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x55555555),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        1.0,
+      ],
+    ));
+  });
+
+  test('LinearGradient lerp test with stops and unequal number of colors', () {
+    const LinearGradient testGradient1 = LinearGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x66666666),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+      ],
+    );
+    const LinearGradient testGradient2 = LinearGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x48484848),
+        Color(0x88888888),
+      ],
+      stops: <double>[
+        0.5,
+        0.7,
+        1.0,
+      ],
+    );
+
+    final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const LinearGradient(
+      colors: <Color>[
+        Color(0x3B3B3B3B),
+        Color(0x55555555),
+        Color(0x57575757),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        0.7,
+        1.0,
       ],
     ));
   });
@@ -221,6 +295,10 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     ));
   });
 
@@ -259,11 +337,84 @@
       radius: 15.0,
       colors: <Color>[
         Color(0x3B3B3B3B),
+        Color(0x55555555),
         Color(0x77777777),
       ],
       stops: <double>[
-        0.25,
-        0.75,
+        0.0,
+        0.5,
+        1.0
+      ],
+    ));
+  });
+
+  test('RadialGradient lerp test with unequal number of colors', () {
+    const RadialGradient testGradient1 = RadialGradient(
+      colors: <Color>[
+        Color(0x22222222),
+        Color(0x66666666),
+      ],
+    );
+    const RadialGradient testGradient2 = RadialGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x66666666),
+        Color(0x88888888),
+      ],
+    );
+
+    final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const RadialGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x55555555),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        1.0,
+      ],
+    ));
+  });
+
+  test('RadialGradient lerp test with stops and unequal number of colors', () {
+    const RadialGradient testGradient1 = RadialGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x66666666),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+      ],
+    );
+    const RadialGradient testGradient2 = RadialGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x48484848),
+        Color(0x88888888),
+      ],
+      stops: <double>[
+        0.5,
+        0.7,
+        1.0,
+      ],
+    );
+
+    final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const RadialGradient(
+      colors: <Color>[
+        Color(0x3B3B3B3B),
+        Color(0x55555555),
+        Color(0x57575757),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        0.7,
+        1.0,
       ],
     ));
   });
@@ -308,6 +459,10 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     ));
 
     final RadialGradient actual2 = RadialGradient.lerp(testGradient1, testGradient3, 0.5);
@@ -320,6 +475,10 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     ));
   });
 
@@ -352,6 +511,10 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     ));
   });
 
@@ -390,11 +553,84 @@
       endAngle: math.pi * 3/4,
       colors: <Color>[
         Color(0x3B3B3B3B),
+        Color(0x55555555),
         Color(0x77777777),
       ],
       stops: <double>[
-        0.25,
-        0.75,
+        0.0,
+        0.5,
+        1.0,
+      ],
+    ));
+  });
+
+  test('SweepGradient lerp test with unequal number of colors', () {
+    const SweepGradient testGradient1 = SweepGradient(
+      colors: <Color>[
+        Color(0x22222222),
+        Color(0x66666666),
+      ],
+    );
+    const SweepGradient testGradient2 = SweepGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x66666666),
+        Color(0x88888888),
+      ],
+    );
+
+    final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const SweepGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x55555555),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        1.0,
+      ],
+    ));
+  });
+
+  test('SweepGradient lerp test with stops and unequal number of colors', () {
+    const SweepGradient testGradient1 = SweepGradient(
+      colors: <Color>[
+        Color(0x33333333),
+        Color(0x66666666),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+      ],
+    );
+    const SweepGradient testGradient2 = SweepGradient(
+      colors: <Color>[
+        Color(0x44444444),
+        Color(0x48484848),
+        Color(0x88888888),
+      ],
+      stops: <double>[
+        0.5,
+        0.7,
+        1.0,
+      ],
+    );
+
+    final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const SweepGradient(
+      colors: <Color>[
+        Color(0x3B3B3B3B),
+        Color(0x55555555),
+        Color(0x57575757),
+        Color(0x77777777),
+      ],
+      stops: <double>[
+        0.0,
+        0.5,
+        0.7,
+        1.0,
       ],
     ));
   });
@@ -431,6 +667,10 @@
         Color(0x33333333),
         Color(0x66666666),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     );
     const RadialGradient testGradient2 = RadialGradient(
       center: Alignment(0.0, -1.0),
@@ -439,6 +679,10 @@
         Color(0x3B3B3B3B),
         Color(0x77777777),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     );
     const RadialGradient testGradient3 = RadialGradient(
       center: Alignment.topRight,
@@ -447,6 +691,10 @@
         Color(0x44444444),
         Color(0x88888888),
       ],
+      stops: <double>[
+        0.0,
+        1.0,
+      ],
     );
 
     expect(Gradient.lerp(testGradient1, testGradient3, 0.0), testGradient1);