mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Gradient transform (#42484)
This commit is contained in:
parent
e76160703f
commit
de499c2a93
@ -1 +1 @@
|
||||
7efcec3e8b0bbb6748a992b23a0a89300aa323c7
|
||||
fa13c1b039e693123888e434e4ee1f9ff79d3b6e
|
||||
|
||||
@ -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';
|
||||
@ -57,6 +59,64 @@ _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 `pi/4` radians (i.e. 45 degrees).
|
||||
@immutable
|
||||
abstract class GradientTransform {
|
||||
/// A const constructor so that subclasses may be const.
|
||||
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 sample}
|
||||
///
|
||||
/// 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),
|
||||
/// );
|
||||
/// ```
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/// A 2D gradient.
|
||||
///
|
||||
/// This is an interface that allows [LinearGradient], [RadialGradient], and
|
||||
@ -76,9 +136,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(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.
|
||||
@ -107,6 +175,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;
|
||||
@ -124,6 +198,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.
|
||||
@ -220,6 +297,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.
|
||||
@ -284,10 +365,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.
|
||||
///
|
||||
@ -334,7 +416,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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -533,11 +615,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.
|
||||
@ -605,7 +688,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,
|
||||
);
|
||||
@ -771,9 +854,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 `math.pi/4` 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(math.pi/4),
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
@ -797,11 +907,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.
|
||||
@ -846,6 +957,7 @@ class SweepGradient extends Gradient {
|
||||
colors, _impliedStops(), tileMode,
|
||||
startAngle,
|
||||
endAngle,
|
||||
_resolveTransform(rect, textDirection),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(math.pi/4)),
|
||||
// A radial gradient won't be interesting to rotate unless the center is changed.
|
||||
RadialGradient(colors: colors, center: Alignment.topCenter, transform: GradientRotation(math.pi/4)),
|
||||
SweepGradient(colors: colors, transform: GradientRotation(math.pi/4)),
|
||||
];
|
||||
const List<Gradient> gradients90 = <Gradient>[
|
||||
LinearGradient(colors: colors, transform: GradientRotation(math.pi/2)),
|
||||
// A radial gradient won't be interesting to rotate unless the center is changed.
|
||||
RadialGradient(colors: colors, center: Alignment.topCenter, transform: GradientRotation(math.pi/2)),
|
||||
SweepGradient(colors: colors, transform: GradientRotation(math.pi/2)),
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user