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(
|
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
..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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
|
|
||||||
@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() {
|
|||||||
},
|
},
|
||||||
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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user