Add SweepGradient (#17368)

This PR adds a SweepGradient class, extending Gradient to expose the engine's ui.Gradient.sweep shader.

Similar to LinearGradient and RadialGradient - SweepGradients can be used in a BoxDecoration or passed to a Paint's shader.
diff --git a/AUTHORS b/AUTHORS
index 4516816..ab63cf6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -23,3 +23,4 @@
 Tetsuhiro Ueda <najeira@gmail.com>
 Dan Field <dfield@gmail.com>
 Noah Groß <gross@ngsger.de>
+Victor Choueiri <victor@ctrlanddev.com>
diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart
index 37a0ab0..f69896b 100644
--- a/packages/flutter/lib/src/painting/gradient.dart
+++ b/packages/flutter/lib/src/painting/gradient.dart
@@ -37,8 +37,8 @@
 
 /// A 2D gradient.
 ///
-/// This is an interface that allows [LinearGradient] and [RadialGradient]
-/// classes to be used interchangeably in [BoxDecoration]s.
+/// This is an interface that allows [LinearGradient], [RadialGradient], and
+/// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s.
 ///
 /// See also:
 ///
@@ -214,9 +214,9 @@
 
 /// A 2D linear gradient.
 ///
-/// This class is used by [BoxDecoration] to represent gradients. This abstracts
-/// out the arguments to the [new ui.Gradient.linear] constructor from the
-/// `dart:ui` library.
+/// This class is used by [BoxDecoration] to represent linear gradients. This
+/// abstracts out the arguments to the [new ui.Gradient.linear] constructor from
+/// the `dart:ui` library.
 ///
 /// A gradient has two anchor points, [begin] and [end]. The [begin] point
 /// corresponds to 0.0, and the [end] point corresponds to 1.0. These points are
@@ -258,6 +258,8 @@
 ///
 ///  * [RadialGradient], which displays a gradient in concentric circles, and
 ///    has an example which shows a different way to use [Gradient] objects.
+///  * [SweepGradient], which displays a gradient in a sweeping arc around a
+///    center point.
 ///  * [BoxDecoration], which can take a [LinearGradient] in its
 ///    [BoxDecoration.gradient] property.
 class LinearGradient extends Gradient {
@@ -278,14 +280,14 @@
 
   /// The offset at which stop 0.0 of the gradient is placed.
   ///
-  /// If this is a [Alignment], then it is expressed as a vector from
+  /// If this is an [Alignment], then it is expressed as a vector from
   /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
   /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
   ///
   /// For example, a begin offset of (-1.0, 0.0) is half way down the
   /// left side of the box.
   ///
-  /// It can also be a [AlignmentDirectional], where the start is the
+  /// It can also be an [AlignmentDirectional], where the start is the
   /// left in left-to-right contexts and the right in right-to-left contexts. If
   /// a text-direction-dependent value is provided here, then the [createShader]
   /// method will need to be given a [TextDirection].
@@ -293,14 +295,14 @@
 
   /// The offset at which stop 1.0 of the gradient is placed.
   ///
-  /// If this is a [Alignment], then it is expressed as a vector from
+  /// If this is an [Alignment], then it is expressed as a vector from
   /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
   /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
   ///
   /// For example, a begin offset of (1.0, 0.0) is half way down the
   /// right side of the box.
   ///
-  /// It can also be a [AlignmentDirectional], where the start is the left in
+  /// It can also be an [AlignmentDirectional], where the start is the left in
   /// left-to-right contexts and the right in right-to-left contexts. If a
   /// text-direction-dependent value is provided here, then the [createShader]
   /// method will need to be given a [TextDirection].
@@ -325,10 +327,10 @@
     );
   }
 
-  /// Returns a new [LinearGradient] with its properties (in particular the
-  /// colors) scaled by the given factor.
+  /// Returns a new [LinearGradient] with its colors scaled by the given factor.
   ///
-  /// If the factor is 0.0 or less, then the gradient is fully transparent.
+  /// Since the alpha component of the Color is what is scaled, a factor
+  /// of 0.0 or less results in a gradient that is fully transparent.
   @override
   LinearGradient scale(double factor) {
     return new LinearGradient(
@@ -362,7 +364,7 @@
   ///
   /// If neither gradient is null, they must have the same number of [colors].
   ///
-  /// The `t` argument represents position on the timeline, with 0.0 meaning
+  /// The `t` argument represents a position on the timeline, with 0.0 meaning
   /// that the interpolation has not started, returning `a` (or something
   /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
   /// returning `b` (or something equivalent to `b`), and values in between
@@ -434,9 +436,9 @@
 
 /// A 2D radial gradient.
 ///
-/// This class is used by [BoxDecoration] to represent gradients. This abstracts
-/// out the arguments to the [new ui.Gradient.radial] constructor from the
-/// `dart:ui` library.
+/// This class is used by [BoxDecoration] to represent radial gradients. This
+/// abstracts out the arguments to the [new ui.Gradient.radial] constructor from
+/// the `dart:ui` library.
 ///
 /// A gradient has a [center] and a [radius]. The [center] point corresponds to
 /// 0.0, and the ring at [radius] from the center corresponds to 1.0. These
@@ -483,6 +485,8 @@
 ///
 ///  * [LinearGradient], which displays a gradient in parallel lines, and has an
 ///    example which shows a different way to use [Gradient] objects.
+///  * [SweepGradient], which displays a gradient in a sweeping arc around a
+///    center point.
 ///  * [BoxDecoration], which can take a [RadialGradient] in its
 ///    [BoxDecoration.gradient] property.
 ///  * [CustomPainter], which shows how to use the above sample code in a custom
@@ -509,11 +513,11 @@
   /// For example, an alignment of (0.0, 0.0) will place the radial
   /// gradient in the center of the box.
   ///
-  /// If this is a [Alignment], then it is expressed as a vector from
+  /// If this is an [Alignment], then it is expressed as a vector from
   /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
   /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
   ///
-  /// It can also be a [AlignmentDirectional], where the start is the left in
+  /// It can also be an [AlignmentDirectional], where the start is the left in
   /// left-to-right contexts and the right in right-to-left contexts. If a
   /// text-direction-dependent value is provided here, then the [createShader]
   /// method will need to be given a [TextDirection].
@@ -548,7 +552,8 @@
 
   /// 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.
+  /// Since the alpha component of the Color is what is scaled, a factor
+  /// of 0.0 or less results in a gradient that is fully transparent.
   @override
   RadialGradient scale(double factor) {
     return new RadialGradient(
@@ -582,7 +587,7 @@
   ///
   /// If neither gradient is null, they must have the same number of [colors].
   ///
-  /// The `t` argument represents position on the timeline, with 0.0 meaning
+  /// The `t` argument represents a position on the timeline, with 0.0 meaning
   /// that the interpolation has not started, returning `a` (or something
   /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
   /// returning `b` (or something equivalent to `b`), and values in between
@@ -651,3 +656,230 @@
     return '$runtimeType($center, $radius, $colors, $stops, $tileMode)';
   }
 }
+
+/// A 2D sweep gradient.
+///
+/// This class is used by [BoxDecoration] to represent sweep gradients. This
+/// abstracts out the arguments to the [new ui.Gradient.sweep] constructor from
+/// the `dart:ui` library.
+///
+/// A gradient has a [center], a [startAngle], and an [endAngle]. The [startAngle]
+/// corresponds to 0.0, and the [endAngle] corresponds to 1.0. These angles are
+/// expressed in radians.
+///
+/// The [colors] are described by a list of [Color] objects. There must be at
+/// least two colors. The [stops] list, if specified, must have the same length
+/// as [colors]. It specifies fractions of the vector from start to end, between
+/// 0.0 and 1.0, for each color. If it is null, a uniform distribution is
+/// assumed.
+///
+/// The region of the canvas before [startAngle] and after [endAngle] is colored
+/// according to [tileMode].
+///
+/// Typically this class is used with [BoxDecoration], which does the painting.
+/// To use a [SweepGradient] to paint on a canvas directly, see [createShader].
+///
+/// ## Sample code
+///
+/// This sample draws a different color in each quadrant.
+///
+/// ```dart
+/// new Container(
+///   decoration: new BoxDecoration(
+///     gradient: new SweepGradient(
+///       center: FractionalOffset.center,
+///       startAngle: 0.0,
+///       endAngle: math.pi * 2,
+///       colors: const <Color>[
+///         const Color(0xFF4285F4), // blue
+///         const Color(0xFF34A853), // green
+///         const Color(0xFFFBBC05), // yellow
+///         const Color(0xFFEA4335), // red
+///         const Color(0xFF4285F4), // blue again to seamlessly transition to the start
+///       ],
+///       stops: const <double>[0.0, 0.25, 0.5, 0.75, 1.0],
+///      ),
+///   ),
+///  )
+/// ```
+///
+/// See also:
+///
+///  * [LinearGradient], which displays a gradient in parallel lines, and has an
+///    example which shows a different way to use [Gradient] objects.
+///  * [RadialGradient], which displays a gradient in concentric circles, and
+///    has an example which shows a different way to use [Gradient] objects.
+///  * [BoxDecoration], which can take a [SweepGradient] in its
+///    [BoxDecoration.gradient] property.
+class SweepGradient extends Gradient {
+  /// Creates a sweep gradient.
+  ///
+  /// The [colors] argument must not be null. If [stops] is non-null, it must
+  /// have the same length as [colors].
+  const SweepGradient({
+    this.center: Alignment.center,
+    this.startAngle: 0.0,
+    this.endAngle: math.pi * 2,
+    @required List<Color> colors,
+    List<double> stops,
+    this.tileMode: TileMode.clamp,
+  }) : assert(center != null),
+       assert(startAngle != null),
+       assert(endAngle != null),
+       assert(tileMode != null),
+       super(colors: colors, stops: stops);
+
+  /// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
+  /// square describing the gradient which will be mapped onto the paint box.
+  ///
+  /// For example, an alignment of (0.0, 0.0) will place the sweep
+  /// gradient in the center of the box.
+  ///
+  /// If this is an [Alignment], then it is expressed as a vector from
+  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
+  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
+  ///
+  /// It can also be an [AlignmentDirectional], where the start is the left in
+  /// left-to-right contexts and the right in right-to-left contexts. If a
+  /// text-direction-dependent value is provided here, then the [createShader]
+  /// method will need to be given a [TextDirection].
+  final AlignmentGeometry center;
+
+  /// The angle in radians at which stop 0.0 of the gradient is placed.
+  ///
+  /// Defaults to 0.0.
+  final double startAngle;
+
+  /// The angle in radians at which stop 1.0 of the gradient is placed.
+  ///
+  /// Defaults to math.pi * 2.
+  final double endAngle;
+
+  /// How this gradient should tile the plane beyond in the region before
+  /// [startAngle] and after [endAngle].
+  ///
+  /// For details, see [TileMode].
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png)
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png)
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png)
+  final TileMode tileMode;
+
+  @override
+  Shader createShader(Rect rect, { TextDirection textDirection }) {
+    return new ui.Gradient.sweep(
+      center.resolve(textDirection).withinRect(rect),
+      colors, _impliedStops(), tileMode,
+      startAngle,
+      endAngle,
+    );
+  }
+
+  /// Returns a new [SweepGradient] with its colors scaled by the given factor.
+  ///
+  /// Since the alpha component of the Color is what is scaled, a factor
+  /// of 0.0 or less results in a gradient that is fully transparent.
+  @override
+  SweepGradient scale(double factor) {
+    return new SweepGradient(
+      center: center,
+      startAngle: startAngle,
+      endAngle: endAngle,
+      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 SweepGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
+      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
+      return SweepGradient.lerp(this, b, t);
+    return super.lerpTo(b, t);
+  }
+
+  /// Linearly interpolate between two [SweepGradient]s.
+  ///
+  /// If either gradient is null, then the non-null gradient is returned with
+  /// its color scaled in the same way as the [scale] function.
+  ///
+  /// If neither gradient is null, they must have the same number of [colors].
+  ///
+  /// The `t` argument represents a position on the timeline, with 0.0 meaning
+  /// that the interpolation has not started, returning `a` (or something
+  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
+  /// returning `b` (or something equivalent to `b`), and values in between
+  /// meaning that the interpolation is at the relevant point on the timeline
+  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
+  /// 1.0, so negative values and values greater than 1.0 are valid (and can
+  /// easily be generated by curves such as [Curves.elasticInOut]).
+  ///
+  /// Values for `t` are usually obtained from an [Animation<double>], such as
+  /// an [AnimationController].
+  static SweepGradient lerp(SweepGradient a, SweepGradient b, double t) {
+    assert(t != null);
+    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 SweepGradient(
+      center: AlignmentGeometry.lerp(a.center, b.center, t),
+      startAngle: math.max(0.0, ui.lerpDouble(a.startAngle, b.startAngle, t)),
+      endAngle: math.max(0.0, ui.lerpDouble(a.endAngle, b.endAngle, 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))
+      return true;
+    if (runtimeType != other.runtimeType)
+      return false;
+    final SweepGradient typedOther = other;
+    if (center != typedOther.center ||
+        startAngle != typedOther.startAngle ||
+        endAngle != typedOther.endAngle ||
+        tileMode != typedOther.tileMode ||
+        colors?.length != typedOther.colors?.length ||
+        stops?.length != typedOther.stops?.length)
+      return false;
+    if (colors != null) {
+      assert(typedOther.colors != null);
+      assert(colors.length == typedOther.colors.length);
+      for (int i = 0; i < colors.length; i += 1) {
+        if (colors[i] != typedOther.colors[i])
+          return false;
+      }
+    }
+    if (stops != null) {
+      assert(typedOther.stops != null);
+      assert(stops.length == typedOther.stops.length);
+      for (int i = 0; i < stops.length; i += 1) {
+        if (stops[i] != typedOther.stops[i])
+          return false;
+      }
+    }
+    return true;
+  }
+
+  @override
+  int get hashCode => hashValues(center, startAngle, endAngle, tileMode, hashList(colors), hashList(stops));
+
+  @override
+  String toString() {
+    return '$runtimeType($center, $startAngle, $endAngle, $colors, $stops, $tileMode)';
+  }
+}
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 616fdd3..af09483 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -233,9 +233,9 @@
   /// The shader callback is called with the current size of the child so that
   /// it can customize the shader to the size and location of the child.
   ///
-  /// Typically this will use a [LinearGradient] or [RadialGradient] to create
-  /// the [dart:ui.Shader], though the [dart:ui.ImageShader] class could also be
-  /// used.
+  /// Typically this will use a [LinearGradient], [RadialGradient], or
+  /// [SweepGradient] to create the [dart:ui.Shader], though the 
+  /// [dart:ui.ImageShader] class could also be used.
   final ShaderCallback shaderCallback;
 
   /// The [BlendMode] to use when applying the shader to the child.
diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart
index 166a971..1817103 100644
--- a/packages/flutter/test/painting/gradient_test.dart
+++ b/packages/flutter/test/painting/gradient_test.dart
@@ -1,6 +1,7 @@
 // Copyright 2016 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 'dart:math' as math;
 
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/painting.dart';
@@ -193,6 +194,45 @@
     );
   });
 
+  test('SweepGradient with AlignmentDirectional', () {
+    expect(
+      () {
+        return const SweepGradient(
+          center: AlignmentDirectional.topStart,
+          colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
+        ).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0));
+      },
+      throwsAssertionError,
+    );
+    expect(
+      () {
+        return const SweepGradient(
+          center: AlignmentDirectional.topStart,
+          colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
+        ).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0), textDirection: TextDirection.rtl);
+      },
+      returnsNormally,
+    );
+    expect(
+      () {
+        return const SweepGradient(
+          center: AlignmentDirectional.topStart,
+          colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
+        ).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0), textDirection: TextDirection.ltr);
+      },
+      returnsNormally,
+    );
+    expect(
+      () {
+        return const SweepGradient(
+          center: Alignment.topLeft,
+          colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
+        ).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0));
+      },
+      returnsNormally,
+    );
+  });
+
   test('RadialGradient lerp test', () {
     const RadialGradient testGradient1 = const RadialGradient(
       center: Alignment.topLeft,
@@ -263,6 +303,106 @@
     ));
   });
 
+  test('SweepGradient lerp test', () {
+    const SweepGradient testGradient1 = const SweepGradient(
+      center: Alignment.topLeft,
+      startAngle: 0.0,
+      endAngle: math.pi / 2,
+      colors: const <Color>[
+        const Color(0x33333333),
+        const Color(0x66666666),
+      ],
+    );
+    const SweepGradient testGradient2 = const SweepGradient(
+      center: Alignment.topRight,
+      startAngle: math.pi / 2,
+      endAngle: math.pi,
+      colors: const <Color>[
+        const Color(0x44444444),
+        const Color(0x88888888),
+      ],
+    );
+
+    final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const SweepGradient(
+      center: const Alignment(0.0, -1.0),
+      startAngle: math.pi / 4,
+      endAngle: math.pi * 3/4,
+      colors: const <Color>[
+        const Color(0x3B3B3B3B),
+        const Color(0x77777777),
+      ],
+    ));
+  });
+
+  test('SweepGradient lerp test with stops', () {
+    const SweepGradient testGradient1 = const SweepGradient(
+      center: Alignment.topLeft,
+      startAngle: 0.0,
+      endAngle: math.pi / 2,
+      colors: const <Color>[
+        const Color(0x33333333),
+        const Color(0x66666666),
+      ],
+      stops: const <double>[
+        0.0,
+        0.5,
+      ],
+    );
+    const SweepGradient testGradient2 = const SweepGradient(
+      center: Alignment.topRight,
+      startAngle: math.pi / 2,
+      endAngle:  math.pi,
+      colors: const <Color>[
+        const Color(0x44444444),
+        const Color(0x88888888),
+      ],
+      stops: const <double>[
+        0.5,
+        1.0,
+      ],
+    );
+
+    final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
+    expect(actual, const SweepGradient(
+      center: const Alignment(0.0, -1.0),
+      startAngle: math.pi / 4,
+      endAngle: math.pi * 3/4,
+      colors: const <Color>[
+        const Color(0x3B3B3B3B),
+        const Color(0x77777777),
+      ],
+      stops: const <double>[
+        0.25,
+        0.75,
+      ],
+    ));
+  });
+
+  test('SweepGradient scale test)', () {
+    const SweepGradient testGradient = const SweepGradient(
+      center: Alignment.topLeft,
+      startAngle: 0.0,
+      endAngle: math.pi / 2,
+      colors: const <Color>[
+        const Color(0xff333333),
+        const Color(0xff666666),
+      ],
+    );
+    
+    final SweepGradient actual = testGradient.scale(0.5);
+    
+    expect(actual, const SweepGradient(
+      center: Alignment.topLeft,
+      startAngle: 0.0,
+      endAngle: math.pi / 2,
+      colors: const <Color>[
+        const Color(0x80333333),
+        const Color(0x80666666),
+      ],
+    ));
+  });
+
   test('Gradient lerp test (with RadialGradient)', () {
     const RadialGradient testGradient1 = const RadialGradient(
       center: Alignment.topLeft,
@@ -357,4 +497,4 @@
     expect(() { test2a.createShader(rect); }, throwsArgumentError);
     expect(() { test2b.createShader(rect); }, throwsArgumentError);
   });
-}
\ No newline at end of file
+}