Add a bitmap operation property to transform widgets to enable/control bitmap transforms (#76742)
This commit is contained in:
parent
132a746aab
commit
ae12bf6bcb
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user