| // Copyright 2014 The Flutter 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:collection'; |
| import 'dart:math' as math; |
| import 'dart:ui' as ui show Gradient, lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:vector_math/vector_math_64.dart'; |
| |
| import 'alignment.dart'; |
| import 'basic_types.dart'; |
| |
| class _ColorsAndStops { |
| _ColorsAndStops(this.colors, this.stops); |
| final List<Color> colors; |
| final List<double> stops; |
| } |
| |
| /// 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); |
| } |
| |
| /// Base class for transforming gradient shaders without applying the same |
| /// transform to the entire canvas. |
| /// |
| /// For example, a [SweepGradient] normally starts its gradation at 3 o'clock |
| /// and draws clockwise. To have the sweep appear to start at 6 o'clock, supply |
| /// a [GradientRotation] of `pi/4` radians (i.e. 45 degrees). |
| @immutable |
| abstract class GradientTransform { |
| /// Abstract const constructor. This constructor enables subclasses to provide |
| /// const constructors so that they can be used in const expressions. |
| const GradientTransform(); |
| |
| /// When a [Gradient] creates its [Shader], it will call this method to |
| /// determine what transform to apply to the shader for the given [Rect] and |
| /// [TextDirection]. |
| /// |
| /// Implementers may return null from this method, which achieves the same |
| /// final effect as returning [Matrix4.identity]. |
| Matrix4? transform(Rect bounds, {TextDirection? textDirection}); |
| } |
| |
| /// A [GradientTransform] that rotates the gradient around the center-point of |
| /// its bounding box. |
| /// |
| /// {@tool snippet} |
| /// |
| /// This sample would rotate a sweep gradient by a quarter turn clockwise: |
| /// |
| /// ```dart |
| /// const SweepGradient gradient = SweepGradient( |
| /// colors: <Color>[Color(0xFFFFFFFF), Color(0xFF009900)], |
| /// transform: GradientRotation(math.pi/4), |
| /// ); |
| /// ``` |
| /// {@end-tool} |
| @immutable |
| class GradientRotation extends GradientTransform { |
| /// Constructs a [GradientRotation] for the specified angle. |
| /// |
| /// The angle is in radians in the clockwise direction. |
| const GradientRotation(this.radians); |
| |
| /// The angle of rotation in radians in the clockwise direction. |
| final double radians; |
| |
| @override |
| Matrix4 transform(Rect bounds, {TextDirection? textDirection}) { |
| assert(bounds != null); |
| final double sinRadians = math.sin(radians); |
| final double oneMinusCosRadians = 1 - math.cos(radians); |
| final Offset center = bounds.center; |
| final double originX = sinRadians * center.dy + oneMinusCosRadians * center.dx; |
| final double originY = -sinRadians * center.dx + oneMinusCosRadians * center.dy; |
| |
| return Matrix4.identity() |
| ..translate(originX, originY) |
| ..rotateZ(radians); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is GradientRotation |
| && other.radians == radians; |
| } |
| |
| @override |
| int get hashCode => radians.hashCode; |
| |
| @override |
| String toString() { |
| return '${objectRuntimeType(this, 'GradientRotation')}(radians: ${debugFormatDouble(radians)})'; |
| } |
| } |
| |
| /// A 2D gradient. |
| /// |
| /// This is an interface that allows [LinearGradient], [RadialGradient], and |
| /// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s. |
| /// |
| /// See also: |
| /// |
| /// * [Gradient](dart-ui/Gradient-class.html), the class in the [dart:ui] library. |
| /// |
| @immutable |
| abstract class Gradient { |
| /// Initialize the gradient's colors and stops. |
| /// |
| /// The [colors] argument must not be null, and must have at least two colors |
| /// (the length is not verified until the [createShader] method is called). |
| /// |
| /// If specified, the [stops] argument must have the same number of entries as |
| /// [colors] (this is also not verified until the [createShader] method is |
| /// called). |
| /// |
| /// The [transform] argument can be applied to transform _only_ the gradient, |
| /// without rotating the canvas itself or other geometry on the canvas. For |
| /// example, a `GradientRotation(math.pi/4)` will result in a [SweepGradient] |
| /// that starts from a position of 6 o'clock instead of 3 o'clock, assuming |
| /// no other rotation or perspective transformations have been applied to the |
| /// [Canvas]. If null, no transformation is applied. |
| const Gradient({ |
| required this.colors, |
| this.stops, |
| this.transform, |
| }) : assert(colors != null); |
| |
| /// The colors the gradient should obtain at each of the stops. |
| /// |
| /// If [stops] is non-null, this list must have the same length as [stops]. |
| /// |
| /// This list must have at least two colors in it (otherwise, it's not a |
| /// gradient!). |
| final List<Color> colors; |
| |
| /// A list of values from 0.0 to 1.0 that denote fractions along the gradient. |
| /// |
| /// If non-null, this list must have the same length as [colors]. |
| /// |
| /// If the first value is not 0.0, then a stop with position 0.0 and a color |
| /// equal to the first color in [colors] is implied. |
| /// |
| /// If the last value is not 1.0, then a stop with position 1.0 and a color |
| /// equal to the last color in [colors] is implied. |
| /// |
| /// The values in the [stops] list must be in ascending order. If a value in |
| /// the [stops] list is less than an earlier value in the list, then its value |
| /// is assumed to equal the previous value. |
| /// |
| /// If stops is null, then a set of uniformly distributed stops is implied, |
| /// with the first stop at 0.0 and the last stop at 1.0. |
| final List<double>? stops; |
| |
| /// The transform, if any, to apply to the gradient. |
| /// |
| /// This transform is in addition to any other transformations applied to the |
| /// canvas, but does not add any transformations to the canvas. |
| final GradientTransform? transform; |
| |
| List<double> _impliedStops() { |
| if (stops != null) { |
| return stops!; |
| } |
| assert(colors.length >= 2, 'colors list must have at least two colors'); |
| final double separation = 1.0 / (colors.length - 1); |
| return List<double>.generate( |
| colors.length, |
| (int index) => index * separation, |
| growable: false, |
| ); |
| } |
| |
| /// Creates a [Shader] for this gradient to fill the given rect. |
| /// |
| /// If the gradient's configuration is text-direction-dependent, for example |
| /// it uses [AlignmentDirectional] objects instead of [Alignment] |
| /// objects, then the `textDirection` argument must not be null. |
| /// |
| /// The shader's transform will be resolved from the [transform] of this |
| /// gradient. |
| @factory |
| 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 another [Gradient] 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]. |
| /// |
| /// The `t` argument represents 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 `this` (or something equivalent to `this`), and values in |
| /// between meaning that the interpolation is at the relevant point on the |
| /// timeline between `a` and `this`. 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]. |
| /// |
| /// 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 another [Gradient]. |
| /// |
| /// 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]. |
| /// |
| /// The `t` argument represents position on the timeline, with 0.0 meaning |
| /// that the interpolation has not started, returning `this` (or something |
| /// equivalent to `this`), 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 `this` 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]. |
| /// |
| /// 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 between two [Gradient]s. |
| /// |
| /// This defers to `b`'s [lerpTo] function if `b` is not null. If `b` is |
| /// null or if its [lerpTo] returns null, it uses `a`'s [lerpFrom] |
| /// function instead. If both return null, it returns `a` before `t == 0.5` |
| /// and `b` after `t == 0.5`. |
| /// |
| /// {@macro dart.ui.shadow.lerp} |
| static Gradient? lerp(Gradient? a, Gradient? b, double t) { |
| assert(t != null); |
| Gradient? result; |
| if (b != null) { |
| result = b.lerpFrom(a, t); // if a is null, this must return non-null |
| } |
| if (result == null && a != null) { |
| result = a.lerpTo(b, t); // if b is null, this must return non-null |
| } |
| if (result != null) { |
| return result; |
| } |
| if (a == null && b == null) { |
| return null; |
| } |
| assert(a != null && b != null); |
| return t < 0.5 ? a!.scale(1.0 - (t * 2.0)) : b!.scale((t - 0.5) * 2.0); |
| } |
| |
| Float64List? _resolveTransform(Rect bounds, TextDirection? textDirection) { |
| return transform?.transform(bounds, textDirection: textDirection)?.storage; |
| } |
| } |
| |
| /// A 2D linear gradient. |
| /// |
| /// This class is used by [BoxDecoration] to represent linear gradients. This |
| /// abstracts out the arguments to the [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 |
| /// expressed in fractions, so that the same gradient can be reused with varying |
| /// sized boxes without changing the parameters. (This contrasts with [ |
| /// ui.Gradient.linear], whose arguments are expressed in logical pixels.) |
| /// |
| /// 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 [begin] and after [end] is colored according |
| /// to [tileMode]. |
| /// |
| /// Typically this class is used with [BoxDecoration], which does the painting. |
| /// To use a [LinearGradient] to paint on a canvas directly, see [createShader]. |
| /// |
| /// {@tool dartpad} |
| /// This sample draws a picture with a gradient sweeping through different |
| /// colors, by having a [Container] display a [BoxDecoration] with a |
| /// [LinearGradient]. |
| /// |
| /// ** See code in examples/api/lib/painting/gradient/linear_gradient.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [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 { |
| /// Creates a linear gradient. |
| /// |
| /// The [colors] argument must not be null. If [stops] is non-null, it must |
| /// have the same length as [colors]. |
| const LinearGradient({ |
| this.begin = Alignment.centerLeft, |
| this.end = Alignment.centerRight, |
| required super.colors, |
| super.stops, |
| this.tileMode = TileMode.clamp, |
| super.transform, |
| }) : assert(begin != null), |
| assert(end != null), |
| assert(tileMode != null); |
| |
| /// The offset at which stop 0.0 of the gradient is placed. |
| /// |
| /// 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 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 begin; |
| |
| /// The offset at which stop 1.0 of the gradient is placed. |
| /// |
| /// 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 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 end; |
| |
| /// How this gradient should tile the plane beyond in the region before |
| /// [begin] and after [end]. |
| /// |
| /// For details, see [TileMode]. |
| /// |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_linear.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) |
| final TileMode tileMode; |
| |
| @override |
| Shader createShader(Rect rect, { TextDirection? textDirection }) { |
| return ui.Gradient.linear( |
| begin.resolve(textDirection).withinRect(rect), |
| end.resolve(textDirection).withinRect(rect), |
| colors, _impliedStops(), tileMode, _resolveTransform(rect, textDirection), |
| ); |
| } |
| |
| /// Returns a new [LinearGradient] 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 |
| LinearGradient scale(double factor) { |
| return LinearGradient( |
| begin: begin, |
| end: end, |
| 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 LinearGradient)) { |
| return LinearGradient.lerp(a as LinearGradient?, this, t); |
| } |
| return super.lerpFrom(a, t); |
| } |
| |
| @override |
| Gradient? lerpTo(Gradient? b, double t) { |
| if (b == null || (b is LinearGradient)) { |
| return LinearGradient.lerp(this, b as LinearGradient?, t); |
| } |
| return super.lerpTo(b, t); |
| } |
| |
| /// Linearly interpolate between two [LinearGradient]s. |
| /// |
| /// If either gradient is null, this function linearly interpolates from a |
| /// a gradient that matches the other gradient in [begin], [end], [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]. |
| /// |
| /// 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 LinearGradient? lerp(LinearGradient? a, LinearGradient? 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._impliedStops(), |
| b.colors, |
| b._impliedStops(), |
| t, |
| ); |
| return LinearGradient( |
| begin: AlignmentGeometry.lerp(a.begin, b.begin, t)!, |
| end: AlignmentGeometry.lerp(a.end, b.end, t)!, |
| colors: interpolated.colors, |
| stops: interpolated.stops, |
| tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode |
| ); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is LinearGradient |
| && other.begin == begin |
| && other.end == end |
| && other.tileMode == tileMode |
| && other.transform == transform |
| && listEquals<Color>(other.colors, colors) |
| && listEquals<double>(other.stops, stops); |
| } |
| |
| @override |
| int get hashCode => Object.hash( |
| begin, |
| end, |
| tileMode, |
| transform, |
| Object.hashAll(colors), |
| stops == null ? null : Object.hashAll(stops!), |
| ); |
| |
| @override |
| String toString() { |
| final List<String> description = <String>[ |
| 'begin: $begin', |
| 'end: $end', |
| 'colors: $colors', |
| if (stops != null) 'stops: $stops', |
| 'tileMode: $tileMode', |
| if (transform != null) 'transform: $transform', |
| ]; |
| |
| return '${objectRuntimeType(this, 'LinearGradient')}(${description.join(', ')})'; |
| } |
| } |
| |
| /// A 2D radial gradient. |
| /// |
| /// This class is used by [BoxDecoration] to represent radial gradients. This |
| /// abstracts out the arguments to the [ui.Gradient.radial] constructor from |
| /// the `dart:ui` library. |
| /// |
| /// A normal radial 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 lengths are expressed in fractions, so that the same gradient |
| /// can be reused with varying sized boxes without changing the parameters. |
| /// (This contrasts with [ui.Gradient.radial], whose arguments are expressed |
| /// in logical pixels.) |
| /// |
| /// It is also possible to create a two-point (or focal pointed) radial gradient |
| /// (which is sometimes referred to as a two point conic gradient, but is not the |
| /// same as a CSS conic gradient which corresponds to a [SweepGradient]). A [focal] |
| /// point and [focalRadius] can be specified similarly to [center] and [radius], |
| /// which will make the rendered gradient appear to be pointed or directed in the |
| /// direction of the [focal] point. This is only important if [focal] and [center] |
| /// are not equal or [focalRadius] > 0.0 (as this case is visually identical to a |
| /// normal radial gradient). One important case to avoid is having [focal] and |
| /// [center] both resolve to [Offset.zero] when [focalRadius] > 0.0. In such a case, |
| /// a valid shader cannot be created by the framework. |
| /// |
| /// 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 radius between 0.0 and 1.0, |
| /// giving concentric rings for each color stop. If it is null, a uniform |
| /// distribution is assumed. |
| /// |
| /// The region of the canvas beyond [radius] from the [center] is colored |
| /// according to [tileMode]. |
| /// |
| /// Typically this class is used with [BoxDecoration], which does the painting. |
| /// To use a [RadialGradient] to paint on a canvas directly, see [createShader]. |
| /// |
| /// {@tool snippet} |
| /// |
| /// This function draws a gradient that looks like a sun in a blue sky. |
| /// |
| /// ```dart |
| /// void paintSky(Canvas canvas, Rect rect) { |
| /// const RadialGradient gradient = RadialGradient( |
| /// center: Alignment(0.7, -0.6), // near the top right |
| /// radius: 0.2, |
| /// colors: <Color>[ |
| /// Color(0xFFFFFF00), // yellow sun |
| /// Color(0xFF0099FF), // blue sky |
| /// ], |
| /// stops: <double>[0.4, 1.0], |
| /// ); |
| /// // rect is the area we are painting over |
| /// final Paint paint = Paint() |
| /// ..shader = gradient.createShader(rect); |
| /// canvas.drawRect(rect, paint); |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [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 |
| /// painter. |
| class RadialGradient extends Gradient { |
| /// Creates a radial gradient. |
| /// |
| /// The [colors] argument must not be null. If [stops] is non-null, it must |
| /// have the same length as [colors]. |
| const RadialGradient({ |
| this.center = Alignment.center, |
| this.radius = 0.5, |
| required super.colors, |
| super.stops, |
| this.tileMode = TileMode.clamp, |
| this.focal, |
| this.focalRadius = 0.0, |
| super.transform, |
| }) : assert(center != null), |
| assert(radius != null), |
| assert(tileMode != null), |
| assert(focalRadius != null); |
| |
| /// 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 radial |
| /// 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 radius of the gradient, as a fraction of the shortest side |
| /// of the paint box. |
| /// |
| /// For example, if a radial gradient is painted on a box that is |
| /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0 |
| /// will place the 1.0 stop at 100.0 pixels from the [center]. |
| final double radius; |
| |
| /// How this gradient should tile the plane beyond the outer ring at [radius] |
| /// pixels from the [center]. |
| /// |
| /// For details, see [TileMode]. |
| /// |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radial.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) |
| /// |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radialWithFocal.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radialWithFocal.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radialWithFocal.png) |
| /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radialWithFocal.png) |
| final TileMode tileMode; |
| |
| /// The focal point of the gradient. If specified, the gradient will appear |
| /// to be focused along the vector from [center] to focal. |
| /// |
| /// See [center] for a description of how the coordinates are mapped. |
| /// |
| /// If this value is specified and [focalRadius] > 0.0, care should be taken |
| /// to ensure that either this value or [center] will not both resolve to |
| /// [Offset.zero], which would fail to create a valid gradient. |
| final AlignmentGeometry? focal; |
| |
| /// The radius of the focal point of gradient, as a fraction of the shortest |
| /// side of the paint box. |
| /// |
| /// For example, if a radial gradient is painted on a box that is |
| /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0 |
| /// will place the 1.0 stop at 100.0 pixels from the [focal] point. |
| /// |
| /// If this value is specified and is greater than 0.0, either [focal] or |
| /// [center] must not resolve to [Offset.zero], which would fail to create |
| /// a valid gradient. |
| final double focalRadius; |
| |
| @override |
| Shader createShader(Rect rect, { TextDirection? textDirection }) { |
| return ui.Gradient.radial( |
| center.resolve(textDirection).withinRect(rect), |
| radius * rect.shortestSide, |
| colors, _impliedStops(), tileMode, |
| _resolveTransform(rect, textDirection), |
| focal == null ? null : focal!.resolve(textDirection).withinRect(rect), |
| focalRadius * rect.shortestSide, |
| ); |
| } |
| |
| /// Returns a new [RadialGradient] 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 |
| RadialGradient scale(double factor) { |
| return RadialGradient( |
| center: center, |
| radius: radius, |
| colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(), |
| stops: stops, |
| tileMode: tileMode, |
| focal: focal, |
| focalRadius: focalRadius, |
| ); |
| } |
| |
| @override |
| Gradient? lerpFrom(Gradient? a, double t) { |
| if (a == null || (a is RadialGradient)) { |
| return RadialGradient.lerp(a as RadialGradient?, this, t); |
| } |
| return super.lerpFrom(a, t); |
| } |
| |
| @override |
| Gradient? lerpTo(Gradient? b, double t) { |
| if (b == null || (b is RadialGradient)) { |
| return RadialGradient.lerp(this, b as RadialGradient?, 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]. |
| /// |
| /// 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 RadialGradient? lerp(RadialGradient? a, RadialGradient? 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._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)!), |
| colors: interpolated.colors, |
| stops: interpolated.stops, |
| tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode |
| focal: AlignmentGeometry.lerp(a.focal, b.focal, t), |
| focalRadius: math.max(0.0, ui.lerpDouble(a.focalRadius, b.focalRadius, t)!), |
| ); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is RadialGradient |
| && other.center == center |
| && other.radius == radius |
| && other.tileMode == tileMode |
| && other.transform == transform |
| && listEquals<Color>(other.colors, colors) |
| && listEquals<double>(other.stops, stops) |
| && other.focal == focal |
| && other.focalRadius == focalRadius; |
| } |
| |
| @override |
| int get hashCode => Object.hash( |
| center, |
| radius, |
| tileMode, |
| transform, |
| Object.hashAll(colors), |
| stops == null ? null : Object.hashAll(stops!), |
| focal, |
| focalRadius, |
| ); |
| |
| @override |
| String toString() { |
| final List<String> description = <String>[ |
| 'center: $center', |
| 'radius: ${debugFormatDouble(radius)}', |
| 'colors: $colors', |
| if (stops != null) 'stops: $stops', |
| 'tileMode: $tileMode', |
| if (focal != null) 'focal: $focal', |
| 'focalRadius: ${debugFormatDouble(focalRadius)}', |
| if (transform != null) 'transform: $transform', |
| ]; |
| |
| return '${objectRuntimeType(this, 'RadialGradient')}(${description.join(', ')})'; |
| } |
| } |
| |
| /// A 2D sweep gradient. |
| /// |
| /// This class is used by [BoxDecoration] to represent sweep gradients. This |
| /// abstracts out the arguments to the [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]. |
| /// |
| /// {@tool snippet} |
| /// |
| /// This sample draws a different color in each quadrant. |
| /// |
| /// ```dart |
| /// Container( |
| /// decoration: const BoxDecoration( |
| /// gradient: SweepGradient( |
| /// center: FractionalOffset.center, |
| /// colors: <Color>[ |
| /// Color(0xFF4285F4), // blue |
| /// Color(0xFF34A853), // green |
| /// Color(0xFFFBBC05), // yellow |
| /// Color(0xFFEA4335), // red |
| /// Color(0xFF4285F4), // blue again to seamlessly transition to the start |
| /// ], |
| /// stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0], |
| /// ), |
| /// ) |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// {@tool snippet} |
| /// |
| /// This sample takes the above gradient and rotates it by `math.pi/4` radians, |
| /// i.e. 45 degrees. |
| /// |
| /// ```dart |
| /// Container( |
| /// decoration: const BoxDecoration( |
| /// gradient: SweepGradient( |
| /// center: FractionalOffset.center, |
| /// colors: <Color>[ |
| /// Color(0xFF4285F4), // blue |
| /// Color(0xFF34A853), // green |
| /// Color(0xFFFBBC05), // yellow |
| /// Color(0xFFEA4335), // red |
| /// Color(0xFF4285F4), // blue again to seamlessly transition to the start |
| /// ], |
| /// stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0], |
| /// transform: GradientRotation(math.pi/4), |
| /// ), |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// 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 super.colors, |
| super.stops, |
| this.tileMode = TileMode.clamp, |
| super.transform, |
| }) : assert(center != null), |
| assert(startAngle != null), |
| assert(endAngle != null), |
| assert(tileMode != null); |
| |
| /// 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_decal_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 ui.Gradient.sweep( |
| center.resolve(textDirection).withinRect(rect), |
| colors, _impliedStops(), tileMode, |
| startAngle, |
| endAngle, |
| _resolveTransform(rect, textDirection), |
| ); |
| } |
| |
| /// 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 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)) { |
| return SweepGradient.lerp(a as SweepGradient?, this, t); |
| } |
| return super.lerpFrom(a, t); |
| } |
| |
| @override |
| Gradient? lerpTo(Gradient? b, double t) { |
| if (b == null || (b is SweepGradient)) { |
| return SweepGradient.lerp(this, b as SweepGradient?, 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._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)!), |
| 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 ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is SweepGradient |
| && other.center == center |
| && other.startAngle == startAngle |
| && other.endAngle == endAngle |
| && other.tileMode == tileMode |
| && other.transform == transform |
| && listEquals<Color>(other.colors, colors) |
| && listEquals<double>(other.stops, stops); |
| } |
| |
| @override |
| int get hashCode => Object.hash( |
| center, |
| startAngle, |
| endAngle, |
| tileMode, |
| transform, |
| Object.hashAll(colors), |
| stops == null ? null : Object.hashAll(stops!), |
| ); |
| |
| @override |
| String toString() { |
| final List<String> description = <String>[ |
| 'center: $center', |
| 'startAngle: ${debugFormatDouble(startAngle)}', |
| 'endAngle: ${debugFormatDouble(endAngle)}', |
| 'colors: $colors', |
| if (stops != null) 'stops: $stops', |
| 'tileMode: $tileMode', |
| if (transform != null) 'transform: $transform', |
| ]; |
| |
| return '${objectRuntimeType(this, 'SweepGradient')}(${description.join(', ')})'; |
| } |
| } |