Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradient transform #42484

Merged
merged 20 commits into from Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/internal/goldens.version
@@ -1 +1 @@
7efcec3e8b0bbb6748a992b23a0a89300aa323c7
9a2854e5a94f563b1452cbc688b4ff8f7e746991
125 changes: 118 additions & 7 deletions packages/flutter/lib/src/painting/gradient.dart
Expand Up @@ -4,9 +4,11 @@

import 'dart:collection';
import 'dart:math' as math;
import 'dart:typed_data';
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';
Expand Down Expand Up @@ -57,6 +59,63 @@ _ColorsAndStops _interpolateColorsAndStops(
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 `0.785398` radians (i.e. `45` degrees).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trivial nit: I think our style is to not use backticks for literals.

@immutable
abstract class GradientTransform {
/// A const constructor that allows subclasses to be const.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a boilerplate paragraph we use for this kind of thing.

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.
///
/// For example, the following would rotate a sweep gradient by a quarter turn
/// clockwise:
///
/// ```dart
/// SweepGradient(
/// colors: colors,
/// transform: GradientRotation(0.785398),
/// );
/// ```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use the sample code logic for this

@immutable
class GradientRotation extends GradientTransform {
/// Constructs a `GradientRotation` for the specified angle.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use [] for identifiers

///
/// The angle is in radians in the clockwise direction.
const GradientRotation(this.radians);

/// The angle of rotation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more detail, e.g. direction of rotation (clockwise, counter-clockwise), the units (though it's pretty obvious from the name).

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);
}
}

/// A 2D gradient.
///
/// This is an interface that allows [LinearGradient], [RadialGradient], and
Expand All @@ -76,9 +135,17 @@ abstract class Gradient {
/// 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(0.785398)` will result in a [SweepGradient]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and elsewhere, rather than 0.785398 we should say pi/4

/// 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.
Expand Down Expand Up @@ -107,6 +174,12 @@ abstract class Gradient {
/// 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;
Expand All @@ -124,6 +197,9 @@ abstract class Gradient {
/// 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.
Shader createShader(Rect rect, { TextDirection textDirection });

/// Returns a new gradient with its properties scaled by the given factor.
Expand Down Expand Up @@ -220,6 +296,10 @@ abstract class Gradient {
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.
Expand Down Expand Up @@ -284,10 +364,11 @@ class LinearGradient extends Gradient {
@required List<Color> colors,
List<double> stops,
this.tileMode = TileMode.clamp,
GradientTransform transform,
}) : assert(begin != null),
assert(end != null),
assert(tileMode != null),
super(colors: colors, stops: stops);
super(colors: colors, stops: stops, transform: transform);

/// The offset at which stop 0.0 of the gradient is placed.
///
Expand Down Expand Up @@ -334,7 +415,7 @@ class LinearGradient extends Gradient {
return ui.Gradient.linear(
begin.resolve(textDirection).withinRect(rect),
end.resolve(textDirection).withinRect(rect),
colors, _impliedStops(), tileMode,
colors, _impliedStops(), tileMode, _resolveTransform(rect, textDirection),
);
}

Expand Down Expand Up @@ -533,11 +614,12 @@ class RadialGradient extends Gradient {
this.tileMode = TileMode.clamp,
this.focal,
this.focalRadius = 0.0,
GradientTransform transform,
}) : assert(center != null),
assert(radius != null),
assert(tileMode != null),
assert(focalRadius != null),
super(colors: colors, stops: stops);
super(colors: colors, stops: stops, transform: transform);

/// 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.
Expand Down Expand Up @@ -605,7 +687,7 @@ class RadialGradient extends Gradient {
center.resolve(textDirection).withinRect(rect),
radius * rect.shortestSide,
colors, _impliedStops(), tileMode,
null, // transform
_resolveTransform(rect, textDirection),
focal == null ? null : focal.resolve(textDirection).withinRect(rect),
focalRadius * rect.shortestSide,
);
Expand Down Expand Up @@ -771,9 +853,36 @@ class RadialGradient extends Gradient {
/// Color(0xFF4285F4), // blue again to seamlessly transition to the start
/// ],
/// stops: const <double>[0.0, 0.25, 0.5, 0.75, 1.0],
/// ),
/// ),
/// )
/// )
/// ```
/// {@end-tool}
///
/// {@tool sample}
///
/// This sample takes the above gradient and rotates it by 0.785398 radians,
/// i.e. 45 degrees.
///
/// ```dart
/// Container(
/// decoration: BoxDecoration(
/// gradient: SweepGradient(
/// center: FractionalOffset.center,
/// startAngle: 0.0,
/// endAngle: math.pi * 2,
/// colors: const <Color>[
/// Color(0xFF4285F4), // blue
/// Color(0xFF34A853), // green
/// Color(0xFFFBBC05), // yellow
/// Color(0xFFEA4335), // red
/// Color(0xFF4285F4), // blue again to seamlessly transition to the start
/// ],
/// stops: const <double>[0.0, 0.25, 0.5, 0.75, 1.0],
/// transform: GradientRotation(0.785398),
/// ),
/// ),
/// )
/// )
/// ```
/// {@end-tool}
///
Expand All @@ -797,11 +906,12 @@ class SweepGradient extends Gradient {
@required List<Color> colors,
List<double> stops,
this.tileMode = TileMode.clamp,
GradientTransform transform,
}) : assert(center != null),
assert(startAngle != null),
assert(endAngle != null),
assert(tileMode != null),
super(colors: colors, stops: stops);
super(colors: colors, stops: stops, transform: transform);

/// 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.
Expand Down Expand Up @@ -846,6 +956,7 @@ class SweepGradient extends Gradient {
colors, _impliedStops(), tileMode,
startAngle,
endAngle,
_resolveTransform(rect, textDirection),
);
}

Expand Down
72 changes: 72 additions & 0 deletions packages/flutter/test/painting/gradient_test.dart
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:math' as math;

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/painting.dart';

Expand Down Expand Up @@ -765,4 +766,75 @@ void main() {
expect(() { test2a.createShader(rect); }, throwsArgumentError);
expect(() { test2b.createShader(rect); }, throwsArgumentError);
});

group('Transforms', () {
const List<Color> colors = <Color>[Color(0xFFFFFFFF), Color(0xFF000088)];
const Rect rect = Rect.fromLTWH(0.0, 0.0, 300.0, 400.0);
const List<Gradient> gradients45 = <Gradient>[
LinearGradient(colors: colors, transform: GradientRotation(0.785398)),
// A radial gradient won't be interesting to rotate unless the center is changed.
RadialGradient(colors: colors, center: Alignment.topCenter, transform: GradientRotation(0.785398)),
SweepGradient(colors: colors, transform: GradientRotation(0.785398)),
];
const List<Gradient> gradients90 = <Gradient>[
LinearGradient(colors: colors, transform: GradientRotation(1.5708)),
// A radial gradient won't be interesting to rotate unless the center is changed.
RadialGradient(colors: colors, center: Alignment.topCenter, transform: GradientRotation(1.5708)),
SweepGradient(colors: colors, transform: GradientRotation(1.5708)),
];

const Map<Type, String> gradientSnakeCase = <Type, String> {
LinearGradient: 'linear_gradient',
RadialGradient: 'radial_gradient',
SweepGradient: 'sweep_gradient',
};

Future<void> runTest(WidgetTester tester, Gradient gradient, double degrees) async {
final String goldenName = '${gradientSnakeCase[gradient.runtimeType]}_$degrees.png';
final Shader shader = gradient.createShader(
rect,
);
final Key painterKey = UniqueKey();
await tester.pumpWidget(Center(
child: SizedBox.fromSize(
size: rect.size,
child: RepaintBoundary(
key: painterKey,
child: CustomPaint(
painter: GradientPainter(shader, rect)
),
),
),
));
await expectLater(find.byKey(painterKey), matchesGoldenFile(goldenName));
}

testWidgets('Gradients - 45 degrees', (WidgetTester tester) async {
for (Gradient gradient in gradients45) {
await runTest(tester, gradient, 45);
}
});

testWidgets('Gradients - 90 degrees', (WidgetTester tester) async {
for (Gradient gradient in gradients90) {
await runTest(tester, gradient, 90);
}
});
});
}

class GradientPainter extends CustomPainter {
const GradientPainter(this.shader, this.rect);

final Shader shader;
final Rect rect;

@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(rect, Paint()..shader = shader);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => true;

}