diff --git a/AUTHORS b/AUTHORS index 4516816920..ab63cf6af5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,3 +23,4 @@ Ali Bitek Tetsuhiro Ueda Dan Field Noah Groß +Victor Choueiri diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index 37a0ab00e4..f69896b58c 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -37,8 +37,8 @@ _ColorsAndStops _interpolateColorsAndStops(List aColors, List aSt /// 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 @@ abstract class Gradient { /// 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 @@ abstract class Gradient { /// /// * [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 @@ class LinearGradient extends Gradient { /// 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 @@ class LinearGradient extends Gradient { /// 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 @@ class LinearGradient extends Gradient { ); } - /// 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 @@ class LinearGradient extends Gradient { /// /// 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 @@ class LinearGradient extends Gradient { /// 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 @@ class LinearGradient extends Gradient { /// /// * [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 @@ class RadialGradient extends Gradient { /// 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 @@ class RadialGradient extends Gradient { /// 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 @@ class RadialGradient extends Gradient { /// /// 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 @@ class RadialGradient extends Gradient { 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 [ +/// 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 [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 colors, + List 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.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], 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 616fdd36c8..af094832e8 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -233,9 +233,9 @@ class ShaderMask extends SingleChildRenderObjectWidget { /// 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 166a971a8a..181710390a 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 @@ void main() { ); }); + test('SweepGradient with AlignmentDirectional', () { + expect( + () { + return const SweepGradient( + center: AlignmentDirectional.topStart, + colors: const [ 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 [ 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 [ 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 [ 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 @@ void main() { )); }); + test('SweepGradient lerp test', () { + const SweepGradient testGradient1 = const SweepGradient( + center: Alignment.topLeft, + startAngle: 0.0, + endAngle: math.pi / 2, + colors: const [ + const Color(0x33333333), + const Color(0x66666666), + ], + ); + const SweepGradient testGradient2 = const SweepGradient( + center: Alignment.topRight, + startAngle: math.pi / 2, + endAngle: math.pi, + colors: const [ + 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 [ + 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 [ + const Color(0x33333333), + const Color(0x66666666), + ], + stops: const [ + 0.0, + 0.5, + ], + ); + const SweepGradient testGradient2 = const SweepGradient( + center: Alignment.topRight, + startAngle: math.pi / 2, + endAngle: math.pi, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + stops: const [ + 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 [ + const Color(0x3B3B3B3B), + const Color(0x77777777), + ], + stops: const [ + 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 [ + 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 [ + const Color(0x80333333), + const Color(0x80666666), + ], + )); + }); + test('Gradient lerp test (with RadialGradient)', () { const RadialGradient testGradient1 = const RadialGradient( center: Alignment.topLeft, @@ -357,4 +497,4 @@ void main() { expect(() { test2a.createShader(rect); }, throwsArgumentError); expect(() { test2b.createShader(rect); }, throwsArgumentError); }); -} \ No newline at end of file +}