Unverified Commit ae12bf6b authored by Jim Graham's avatar Jim Graham Committed by GitHub

Add a bitmap operation property to transform widgets to enable/control bitmap transforms (#76742)

parent 132a746a
...@@ -120,6 +120,7 @@ class _FilteredChildAnimationPageState extends State<FilteredChildAnimationPage> ...@@ -120,6 +120,7 @@ class _FilteredChildAnimationPageState extends State<FilteredChildAnimationPage>
builder = (BuildContext context, Widget child) => Transform( builder = (BuildContext context, Widget child) => Transform(
transform: Matrix4.rotationZ(_controller.value * 2.0 * pi), transform: Matrix4.rotationZ(_controller.value * 2.0 * pi),
alignment: Alignment.center, alignment: Alignment.center,
filterQuality: FilterQuality.low,
child: child, child: child,
); );
break; break;
......
...@@ -2205,12 +2205,14 @@ class RenderTransform extends RenderProxyBox { ...@@ -2205,12 +2205,14 @@ class RenderTransform extends RenderProxyBox {
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
TextDirection? textDirection, TextDirection? textDirection,
this.transformHitTests = true, this.transformHitTests = true,
FilterQuality? filterQuality,
RenderBox? child, RenderBox? child,
}) : assert(transform != null), }) : assert(transform != null),
super(child) { super(child) {
this.transform = transform; this.transform = transform;
this.alignment = alignment; this.alignment = alignment;
this.textDirection = textDirection; this.textDirection = textDirection;
this.filterQuality = filterQuality;
this.origin = origin; this.origin = origin;
} }
...@@ -2264,6 +2266,9 @@ class RenderTransform extends RenderProxyBox { ...@@ -2264,6 +2266,9 @@ class RenderTransform extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
@override
bool get alwaysNeedsCompositing => child != null && _filterQuality != null;
/// When set to true, hit tests are performed based on the position of the /// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed /// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation. /// ignoring the transformation.
...@@ -2285,6 +2290,21 @@ class RenderTransform extends RenderProxyBox { ...@@ -2285,6 +2290,21 @@ class RenderTransform extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// The filter quality with which to apply the transform as a bitmap operation.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
FilterQuality? get filterQuality => _filterQuality;
FilterQuality? _filterQuality;
set filterQuality(FilterQuality? value) {
if (_filterQuality == value)
return;
final bool didNeedCompositing = alwaysNeedsCompositing;
_filterQuality = value;
if (didNeedCompositing != alwaysNeedsCompositing)
markNeedsCompositingBitsUpdate();
markNeedsPaint();
}
/// Sets the transform to the identity matrix. /// Sets the transform to the identity matrix.
void setIdentity() { void setIdentity() {
_transform!.setIdentity(); _transform!.setIdentity();
...@@ -2372,6 +2392,7 @@ class RenderTransform extends RenderProxyBox { ...@@ -2372,6 +2392,7 @@ class RenderTransform extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
final Matrix4 transform = _effectiveTransform!; final Matrix4 transform = _effectiveTransform!;
if (filterQuality == null) {
final Offset? childOffset = MatrixUtils.getAsTranslation(transform); final Offset? childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) { if (childOffset == null) {
layer = context.pushTransform( layer = context.pushTransform(
...@@ -2379,12 +2400,25 @@ class RenderTransform extends RenderProxyBox { ...@@ -2379,12 +2400,25 @@ class RenderTransform extends RenderProxyBox {
offset, offset,
transform, transform,
super.paint, super.paint,
oldLayer: layer as TransformLayer?, oldLayer: layer is TransformLayer ? layer as TransformLayer? : null,
); );
} else { } else {
super.paint(context, offset + childOffset); super.paint(context, offset + childOffset);
layer = null; layer = null;
} }
} else {
final ui.ImageFilter filter = ui.ImageFilter.matrix(
transform.storage,
filterQuality: filterQuality!,
);
if (layer is ImageFilterLayer) {
final ImageFilterLayer filterLayer = layer! as ImageFilterLayer;
filterLayer.imageFilter = filter;
} else {
layer = ImageFilterLayer(imageFilter: filter);
}
context.pushLayer(layer!, super.paint, offset);
}
} }
} }
......
...@@ -1178,6 +1178,7 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1178,6 +1178,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.origin, this.origin,
this.alignment, this.alignment,
this.transformHitTests = true, this.transformHitTests = true,
this.filterQuality,
Widget? child, Widget? child,
}) : assert(transform != null), }) : assert(transform != null),
super(key: key, child: child); super(key: key, child: child);
...@@ -1215,6 +1216,7 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1215,6 +1216,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.origin, this.origin,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.transformHitTests = true, this.transformHitTests = true,
this.filterQuality,
Widget? child, Widget? child,
}) : transform = Matrix4.rotationZ(angle), }) : transform = Matrix4.rotationZ(angle),
super(key: key, child: child); super(key: key, child: child);
...@@ -1242,6 +1244,7 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1242,6 +1244,7 @@ class Transform extends SingleChildRenderObjectWidget {
Key? key, Key? key,
required Offset offset, required Offset offset,
this.transformHitTests = true, this.transformHitTests = true,
this.filterQuality,
Widget? child, Widget? child,
}) : transform = Matrix4.translationValues(offset.dx, offset.dy, 0.0), }) : transform = Matrix4.translationValues(offset.dx, offset.dy, 0.0),
origin = null, origin = null,
...@@ -1283,6 +1286,7 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1283,6 +1286,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.origin, this.origin,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.transformHitTests = true, this.transformHitTests = true,
this.filterQuality,
Widget? child, Widget? child,
}) : transform = Matrix4.diagonal3Values(scale, scale, 1.0), }) : transform = Matrix4.diagonal3Values(scale, scale, 1.0),
super(key: key, child: child); super(key: key, child: child);
...@@ -1314,6 +1318,15 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1314,6 +1318,15 @@ class Transform extends SingleChildRenderObjectWidget {
/// Whether to apply the transformation when performing hit tests. /// Whether to apply the transformation when performing hit tests.
final bool transformHitTests; final bool transformHitTests;
/// The filter quality with which to apply the transform as a bitmap operation.
///
/// {@template flutter.widgets.Transform.optional.FilterQuality}
/// The transform will be applied by re-rendering the child if [filterQuality] is null,
/// otherwise it controls the quality of an [ImageFilter.matrix] applied to a bitmap
/// rendering of the child.
/// {@endtemplate}
final FilterQuality? filterQuality;
@override @override
RenderTransform createRenderObject(BuildContext context) { RenderTransform createRenderObject(BuildContext context) {
return RenderTransform( return RenderTransform(
...@@ -1322,6 +1335,7 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1322,6 +1335,7 @@ class Transform extends SingleChildRenderObjectWidget {
alignment: alignment, alignment: alignment,
textDirection: Directionality.maybeOf(context), textDirection: Directionality.maybeOf(context),
transformHitTests: transformHitTests, transformHitTests: transformHitTests,
filterQuality: filterQuality,
); );
} }
...@@ -1332,7 +1346,8 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -1332,7 +1346,8 @@ class Transform extends SingleChildRenderObjectWidget {
..origin = origin ..origin = origin
..alignment = alignment ..alignment = alignment
..textDirection = Directionality.maybeOf(context) ..textDirection = Directionality.maybeOf(context)
..transformHitTests = transformHitTests; ..transformHitTests = transformHitTests
..filterQuality = filterQuality;
} }
} }
......
...@@ -356,6 +356,7 @@ class ScaleTransition extends AnimatedWidget { ...@@ -356,6 +356,7 @@ class ScaleTransition extends AnimatedWidget {
Key? key, Key? key,
required Animation<double> scale, required Animation<double> scale,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.filterQuality,
this.child, this.child,
}) : assert(scale != null), }) : assert(scale != null),
super(key: key, listenable: scale); super(key: key, listenable: scale);
...@@ -373,6 +374,11 @@ class ScaleTransition extends AnimatedWidget { ...@@ -373,6 +374,11 @@ class ScaleTransition extends AnimatedWidget {
/// an alignment of (0.0, 1.0). /// an alignment of (0.0, 1.0).
final Alignment alignment; final Alignment alignment;
/// The filter quality with which to apply the transform as a bitmap operation.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
final FilterQuality? filterQuality;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
...@@ -380,12 +386,10 @@ class ScaleTransition extends AnimatedWidget { ...@@ -380,12 +386,10 @@ class ScaleTransition extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double scaleValue = scale.value; return Transform.scale(
final Matrix4 transform = Matrix4.identity() scale: scale.value,
..scale(scaleValue, scaleValue, 1.0);
return Transform(
transform: transform,
alignment: alignment, alignment: alignment,
filterQuality: filterQuality,
child: child, child: child,
); );
} }
...@@ -449,6 +453,7 @@ class RotationTransition extends AnimatedWidget { ...@@ -449,6 +453,7 @@ class RotationTransition extends AnimatedWidget {
Key? key, Key? key,
required Animation<double> turns, required Animation<double> turns,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.filterQuality,
this.child, this.child,
}) : assert(turns != null), }) : assert(turns != null),
super(key: key, listenable: turns); super(key: key, listenable: turns);
...@@ -466,6 +471,11 @@ class RotationTransition extends AnimatedWidget { ...@@ -466,6 +471,11 @@ class RotationTransition extends AnimatedWidget {
/// an alignment of (1.0, -1.0) or use [Alignment.topRight] /// an alignment of (1.0, -1.0) or use [Alignment.topRight]
final Alignment alignment; final Alignment alignment;
/// The filter quality with which to apply the transform as a bitmap operation.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
final FilterQuality? filterQuality;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
...@@ -473,11 +483,10 @@ class RotationTransition extends AnimatedWidget { ...@@ -473,11 +483,10 @@ class RotationTransition extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double turnsValue = turns.value; return Transform.rotate(
final Matrix4 transform = Matrix4.rotationZ(turnsValue * math.pi * 2.0); angle: turns.value * math.pi * 2.0,
return Transform(
transform: transform,
alignment: alignment, alignment: alignment,
filterQuality: filterQuality,
child: child, child: child,
); );
} }
......
...@@ -388,6 +388,119 @@ void main() { ...@@ -388,6 +388,119 @@ void main() {
}, },
skip: isBrowser, // due to https://github.com/flutter/flutter/issues/42767 skip: isBrowser, // due to https://github.com/flutter/flutter/issues/42767
); );
testWidgets('Transform.translate with FilterQuality produces filter layer', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.translate(
offset: const Offset(25.0, 25.0),
child: const SizedBox(width: 100, height: 100),
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>().length, 1);
});
testWidgets('Transform.scale with FilterQuality produces filter layer', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.scale(
scale: 3.14159,
child: const SizedBox(width: 100, height: 100),
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>().length, 1);
});
testWidgets('Transform.rotate with FilterQuality produces filter layer', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 4,
child: const SizedBox(width: 100, height: 100),
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>().length, 1);
});
testWidgets('Transform layers update to match child and filterQuality', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 4,
child: const SizedBox(width: 100, height: 100),
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>(), hasLength(1));
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 4,
child: const SizedBox(width: 100, height: 100),
),
);
expect(tester.layers.whereType<ImageFilterLayer>(), isEmpty);
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 4,
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>(), isEmpty);
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 4,
child: const SizedBox(width: 100, height: 100),
filterQuality: FilterQuality.low,
),
);
expect(tester.layers.whereType<ImageFilterLayer>(), hasLength(1));
});
testWidgets('Transform layers with filterQuality golden', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: GridView.count(
crossAxisCount: 3,
children: <Widget>[
Transform.rotate(
angle: math.pi / 6,
child: Center(child: Container(width: 100, height: 20, color: const Color(0xffffff00))),
),
Transform.scale(
scale: 1.5,
child: Center(child: Container(width: 100, height: 20, color: const Color(0xffffff00))),
),
Transform.translate(
offset: const Offset(20.0, 60.0),
child: Center(child: Container(width: 100, height: 20, color: const Color(0xffffff00))),
),
Transform.rotate(
angle: math.pi / 6,
child: Center(child: Container(width: 100, height: 20, color: const Color(0xff00ff00))),
filterQuality: FilterQuality.low,
),
Transform.scale(
scale: 1.5,
child: Center(child: Container(width: 100, height: 20, color: const Color(0xff00ff00))),
filterQuality: FilterQuality.low,
),
Transform.translate(
offset: const Offset(20.0, 60.0),
child: Center(child: Container(width: 100, height: 20, color: const Color(0xff00ff00))),
filterQuality: FilterQuality.low,
),
],
),
),
);
await expectLater(
find.byType(GridView),
matchesGoldenFile('transform_golden.BitmapRotate.png'),
);
});
} }
class TestRectPainter extends CustomPainter { class TestRectPainter extends CustomPainter {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment