From 587a1b77084458d484da78bef98ba8a7db680b52 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 30 May 2018 21:45:21 -0400 Subject: [PATCH] Expose Gradient.radial (#17826) * Expose Gradient.radial * cleanup doc * update tests, null check * add asset images --- .../flutter/lib/src/painting/gradient.dart | 67 ++++++++++-- .../flutter/test/painting/gradient_test.dart | 102 +++++++++++------- 2 files changed, 119 insertions(+), 50 deletions(-) diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index f69896b58c..8d29af7fa7 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -440,12 +440,23 @@ class LinearGradient extends Gradient { /// 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 -/// lengths are expressed in fractions, so that the same gradient can be reused -/// with varying sized boxes without changing the parameters. (This contrasts -/// with [new ui.Gradient.radial], whose arguments are expressed in logical -/// pixels.) +/// 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 [new 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 @@ -502,9 +513,12 @@ class RadialGradient extends Gradient { @required List colors, List stops, this.tileMode: TileMode.clamp, + this.focal, + this.focalRadius: 0.0 }) : assert(center != null), assert(radius != null), assert(tileMode != null), + assert(focalRadius != 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) @@ -539,14 +553,43 @@ class RadialGradient extends Gradient { /// ![](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_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_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 [focus]. + /// + /// 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 new ui.Gradient.radial( center.resolve(textDirection).withinRect(rect), radius * rect.shortestSide, colors, _impliedStops(), tileMode, + null, // transform + focal == null ? null : focal.resolve(textDirection).withinRect(rect), + focalRadius * rect.shortestSide, ); } @@ -562,6 +605,8 @@ class RadialGradient extends Gradient { colors: colors.map((Color color) => Color.lerp(null, color, factor)).toList(), stops: stops, tileMode: tileMode, + focal: focal, + focalRadius: focalRadius ); } @@ -613,6 +658,8 @@ class RadialGradient extends Gradient { 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)), ); } @@ -627,7 +674,9 @@ class RadialGradient extends Gradient { radius != typedOther.radius || tileMode != typedOther.tileMode || colors?.length != typedOther.colors?.length || - stops?.length != typedOther.stops?.length) + stops?.length != typedOther.stops?.length || + focal != typedOther.focal || + focalRadius != typedOther.focalRadius) return false; if (colors != null) { assert(typedOther.colors != null); @@ -649,11 +698,11 @@ class RadialGradient extends Gradient { } @override - int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops)); + int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops), focal, focalRadius); @override String toString() { - return '$runtimeType($center, $radius, $colors, $stops, $tileMode)'; + return '$runtimeType($center, $radius, $colors, $stops, $tileMode, $focal, $focalRadius)'; } } diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart index 181710390a..9687557387 100644 --- a/packages/flutter/test/painting/gradient_test.dart +++ b/packages/flutter/test/painting/gradient_test.dart @@ -165,48 +165,10 @@ void main() { }, throwsAssertionError, ); - expect( - () { - return const RadialGradient( - 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 RadialGradient( - 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 RadialGradient( - 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('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( + return const RadialGradient( 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); @@ -215,7 +177,7 @@ void main() { ); expect( () { - return const SweepGradient( + return const RadialGradient( 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); @@ -224,7 +186,7 @@ void main() { ); expect( () { - return const SweepGradient( + return const RadialGradient( center: Alignment.topLeft, colors: const [ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ] ).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0)); @@ -289,6 +251,9 @@ void main() { ); final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5); + + expect(actual.focal, isNull); + expect(actual, const RadialGradient( center: const Alignment(0.0, -1.0), radius: 15.0, @@ -303,6 +268,61 @@ void main() { )); }); + test('RadialGradient lerp test with focal', () { + const RadialGradient testGradient1 = const RadialGradient( + center: Alignment.topLeft, + focal: Alignment.centerLeft, + radius: 20.0, + focalRadius: 10.0, + colors: const [ + const Color(0x33333333), + const Color(0x66666666), + ], + ); + const RadialGradient testGradient2 = const RadialGradient( + center: Alignment.topRight, + focal: Alignment.centerRight, + radius: 10.0, + focalRadius: 5.0, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + ); + const RadialGradient testGradient3 = const RadialGradient( + center: Alignment.topRight, + radius: 10.0, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + ); + + final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const RadialGradient( + center: const Alignment(0.0, -1.0), + focal: const Alignment(0.0, 0.0), + radius: 15.0, + focalRadius: 7.5, + colors: const [ + const Color(0x3B3B3B3B), + const Color(0x77777777), + ], + )); + + final RadialGradient actual2 = RadialGradient.lerp(testGradient1, testGradient3, 0.5); + expect(actual2, const RadialGradient( + center: const Alignment(0.0, -1.0), + focal: const Alignment(-0.5, 0.0), + radius: 15.0, + focalRadius: 5.0, + colors: const [ + const Color(0x3B3B3B3B), + const Color(0x77777777), + ], + )); + }); + test('SweepGradient lerp test', () { const SweepGradient testGradient1 = const SweepGradient( center: Alignment.topLeft,