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].
+ ///
+ /// 
+ /// 
+ /// 
+ 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
+}