diff --git a/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart b/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart index f0d35f844c..87c0722205 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart @@ -120,6 +120,7 @@ class _FilteredChildAnimationPageState extends State builder = (BuildContext context, Widget child) => Transform( transform: Matrix4.rotationZ(_controller.value * 2.0 * pi), alignment: Alignment.center, + filterQuality: FilterQuality.low, child: child, ); break; diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 63faa29206..a6df3acbf8 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -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,18 +2392,32 @@ class RenderTransform extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { final Matrix4 transform = _effectiveTransform!; - final Offset? childOffset = MatrixUtils.getAsTranslation(transform); - if (childOffset == null) { - layer = context.pushTransform( - needsCompositing, - offset, - transform, - super.paint, - oldLayer: layer as TransformLayer?, - ); + if (filterQuality == null) { + final Offset? childOffset = MatrixUtils.getAsTranslation(transform); + if (childOffset == null) { + layer = context.pushTransform( + needsCompositing, + offset, + transform, + super.paint, + oldLayer: layer is TransformLayer ? layer as TransformLayer? : null, + ); + } else { + super.paint(context, offset + childOffset); + layer = null; + } } else { - super.paint(context, offset + childOffset); - layer = null; + 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); } } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index d2fd58e50a..02fc803213 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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; } } diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 7f1bc735bb..ded454d77e 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -356,6 +356,7 @@ class ScaleTransition extends AnimatedWidget { Key? key, required Animation 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 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, ); } diff --git a/packages/flutter/test/widgets/transform_test.dart b/packages/flutter/test/widgets/transform_test.dart index 869842abac..0bf91c50d6 100644 --- a/packages/flutter/test/widgets/transform_test.dart +++ b/packages/flutter/test/widgets/transform_test.dart @@ -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().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().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().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(), hasLength(1)); + + await tester.pumpWidget( + Transform.rotate( + angle: math.pi / 4, + child: const SizedBox(width: 100, height: 100), + ), + ); + expect(tester.layers.whereType(), isEmpty); + + await tester.pumpWidget( + Transform.rotate( + angle: math.pi / 4, + filterQuality: FilterQuality.low, + ), + ); + expect(tester.layers.whereType(), isEmpty); + + await tester.pumpWidget( + Transform.rotate( + angle: math.pi / 4, + child: const SizedBox(width: 100, height: 100), + filterQuality: FilterQuality.low, + ), + ); + expect(tester.layers.whereType(), hasLength(1)); + }); + + testWidgets('Transform layers with filterQuality golden', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GridView.count( + crossAxisCount: 3, + children: [ + 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 {