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>
builder = (BuildContext context, Widget child) => Transform(
transform: Matrix4.rotationZ(_controller.value * 2.0 * pi),
alignment: Alignment.center,
filterQuality: FilterQuality.low,
child: child,
);
break;
......
......@@ -2205,12 +2205,14 @@ class RenderTransform extends RenderProxyBox {
AlignmentGeometry? alignment,
TextDirection? textDirection,
this.transformHitTests = true,
FilterQuality? filterQuality,
RenderBox? child,
}) : assert(transform != null),
super(child) {
this.transform = transform;
this.alignment = alignment;
this.textDirection = textDirection;
this.filterQuality = filterQuality;
this.origin = origin;
}
......@@ -2264,6 +2266,9 @@ class RenderTransform extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
@override
bool get alwaysNeedsCompositing => child != null && _filterQuality != null;
/// 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
/// ignoring the transformation.
......@@ -2285,6 +2290,21 @@ class RenderTransform extends RenderProxyBox {
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.
void setIdentity() {
_transform!.setIdentity();
......@@ -2372,6 +2392,7 @@ class RenderTransform extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform = _effectiveTransform!;
if (filterQuality == null) {
final Offset? childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) {
layer = context.pushTransform(
......@@ -2379,12 +2400,25 @@ class RenderTransform extends RenderProxyBox {
offset,
transform,
super.paint,
oldLayer: layer as TransformLayer?,
oldLayer: layer is TransformLayer ? layer as TransformLayer? : null,
);
} else {
super.paint(context, offset + childOffset);
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 {
this.origin,
this.alignment,
this.transformHitTests = true,
this.filterQuality,
Widget? child,
}) : assert(transform != null),
super(key: key, child: child);
......@@ -1215,6 +1216,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.origin,
this.alignment = Alignment.center,
this.transformHitTests = true,
this.filterQuality,
Widget? child,
}) : transform = Matrix4.rotationZ(angle),
super(key: key, child: child);
......@@ -1242,6 +1244,7 @@ class Transform extends SingleChildRenderObjectWidget {
Key? key,
required Offset offset,
this.transformHitTests = true,
this.filterQuality,
Widget? child,
}) : transform = Matrix4.translationValues(offset.dx, offset.dy, 0.0),
origin = null,
......@@ -1283,6 +1286,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.origin,
this.alignment = Alignment.center,
this.transformHitTests = true,
this.filterQuality,
Widget? child,
}) : transform = Matrix4.diagonal3Values(scale, scale, 1.0),
super(key: key, child: child);
......@@ -1314,6 +1318,15 @@ class Transform extends SingleChildRenderObjectWidget {
/// Whether to apply the transformation when performing hit tests.
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
RenderTransform createRenderObject(BuildContext context) {
return RenderTransform(
......@@ -1322,6 +1335,7 @@ class Transform extends SingleChildRenderObjectWidget {
alignment: alignment,
textDirection: Directionality.maybeOf(context),
transformHitTests: transformHitTests,
filterQuality: filterQuality,
);
}
......@@ -1332,7 +1346,8 @@ class Transform extends SingleChildRenderObjectWidget {
..origin = origin
..alignment = alignment
..textDirection = Directionality.maybeOf(context)
..transformHitTests = transformHitTests;
..transformHitTests = transformHitTests
..filterQuality = filterQuality;
}
}
......
......@@ -356,6 +356,7 @@ class ScaleTransition extends AnimatedWidget {
Key? key,
required Animation<double> scale,
this.alignment = Alignment.center,
this.filterQuality,
this.child,
}) : assert(scale != null),
super(key: key, listenable: scale);
......@@ -373,6 +374,11 @@ class ScaleTransition extends AnimatedWidget {
/// an alignment of (0.0, 1.0).
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.
///
/// {@macro flutter.widgets.ProxyWidget.child}
......@@ -380,12 +386,10 @@ class ScaleTransition extends AnimatedWidget {
@override
Widget build(BuildContext context) {
final double scaleValue = scale.value;
final Matrix4 transform = Matrix4.identity()
..scale(scaleValue, scaleValue, 1.0);
return Transform(
transform: transform,
return Transform.scale(
scale: scale.value,
alignment: alignment,
filterQuality: filterQuality,
child: child,
);
}
......@@ -449,6 +453,7 @@ class RotationTransition extends AnimatedWidget {
Key? key,
required Animation<double> turns,
this.alignment = Alignment.center,
this.filterQuality,
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
......@@ -466,6 +471,11 @@ class RotationTransition extends AnimatedWidget {
/// an alignment of (1.0, -1.0) or use [Alignment.topRight]
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.
///
/// {@macro flutter.widgets.ProxyWidget.child}
......@@ -473,11 +483,10 @@ class RotationTransition extends AnimatedWidget {
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform = Matrix4.rotationZ(turnsValue * math.pi * 2.0);
return Transform(
transform: transform,
return Transform.rotate(
angle: turns.value * math.pi * 2.0,
alignment: alignment,
filterQuality: filterQuality,
child: child,
);
}
......
......@@ -388,6 +388,119 @@ void main() {
},
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 {
......
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