Unverified Commit 15739345 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] don't composite with a scale of 0.0 (#106982)

parent c51bf2fa
......@@ -2604,6 +2604,13 @@ class RenderTransform extends RenderProxyBox {
if (filterQuality == null) {
final Offset? childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) {
// if the matrix is singular the children would be compressed to a line or
// single point, instead short-circuit and paint nothing.
final double det = transform.determinant();
if (det == 0 || !det.isFinite) {
layer = null;
return;
}
layer = context.pushTransform(
needsCompositing,
offset,
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
import 'package:flutter/animation.dart';
......@@ -1304,7 +1305,7 @@ class Transform extends SingleChildRenderObjectWidget {
this.transformHitTests = true,
this.filterQuality,
super.child,
}) : transform = Matrix4.rotationZ(angle);
}) : transform = _computeRotation(angle);
/// Creates a widget that transforms its child using a translation.
///
......@@ -1381,6 +1382,38 @@ class Transform extends SingleChildRenderObjectWidget {
assert(scale == null || (scaleX == null && scaleY == null), "If 'scale' is non-null then 'scaleX' and 'scaleY' must be left null"),
transform = Matrix4.diagonal3Values(scale ?? scaleX ?? 1.0, scale ?? scaleY ?? 1.0, 1.0);
// Computes a rotation matrix for an angle in radians, attempting to keep rotations
// at integral values for angles of 0, π/2, π, 3π/2.
static Matrix4 _computeRotation(double radians) {
assert(radians.isFinite, 'Cannot compute the rotation matrix for a non-finite angle: $radians');
if (radians == 0.0) {
return Matrix4.identity();
}
final double sin = math.sin(radians);
if (sin == 1.0) {
return _createZRotation(1.0, 0.0);
}
if (sin == -1.0) {
return _createZRotation(-1.0, 0.0);
}
final double cos = math.cos(radians);
if (cos == -1.0) {
return _createZRotation(0.0, -1.0);
}
return _createZRotation(sin, cos);
}
static Matrix4 _createZRotation(double sin, double cos) {
final Matrix4 result = Matrix4.zero();
result.storage[0] = cos;
result.storage[1] = sin;
result.storage[4] = -sin;
result.storage[5] = cos;
result.storage[10] = 1.0;
result.storage[15] = 1.0;
return result;
}
/// The matrix to transform the child by during painting.
final Matrix4 transform;
......
......@@ -338,6 +338,153 @@ void main() {
]);
});
testWidgets('Transform with nan value short-circuits rendering', (WidgetTester tester) async {
await tester.pumpWidget(
Transform(
transform: Matrix4.identity()
..storage[0] = double.nan,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1));
});
testWidgets('Transform with inf value short-circuits rendering', (WidgetTester tester) async {
await tester.pumpWidget(
Transform(
transform: Matrix4.identity()
..storage[0] = double.infinity,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1));
});
testWidgets('Transform with -inf value short-circuits rendering', (WidgetTester tester) async {
await tester.pumpWidget(
Transform(
transform: Matrix4.identity()
..storage[0] = double.negativeInfinity,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1));
});
testWidgets('Transform.rotate does not remove layers due to singular short-circuit', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 2,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(3));
});
testWidgets('Transform.rotate creates nice rotation matrices for 0, 90, 180, 270 degrees', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.rotate(
angle: math.pi / 2,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers[1], isA<TransformLayer>()
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
0.0, -1.0, 0.0, 700.0,
1.0, 0.0, 0.0, -100.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose()))
);
await tester.pumpWidget(
Transform.rotate(
angle: math.pi,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers[1], isA<TransformLayer>()
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
-1.0, 0.0, 0.0, 800.0,
0.0, -1.0, 0.0, 600.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose()))
);
await tester.pumpWidget(
Transform.rotate(
angle: 3 * math.pi / 2,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers[1], isA<TransformLayer>()
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
0.0, 1.0, 0.0, 100.0,
-1.0, 0.0, 0.0, 700.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose()))
);
await tester.pumpWidget(
Transform.rotate(
angle: 0,
child: RepaintBoundary(child: Container()),
),
);
// No transform layer created
expect(tester.layers[1], isA<OffsetLayer>());
expect(tester.layers, hasLength(2));
});
testWidgets('Transform.scale with 0.0 does not paint child layers', (WidgetTester tester) async {
await tester.pumpWidget(
Transform.scale(
scale: 0.0,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1)); // root transform layer
await tester.pumpWidget(
Transform.scale(
scaleX: 0.0,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1));
await tester.pumpWidget(
Transform.scale(
scaleY: 0.0,
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(1));
await tester.pumpWidget(
Transform.scale(
scale: 0.01, // small but non-zero
child: RepaintBoundary(child: Container()),
),
);
expect(tester.layers, hasLength(3));
});
testWidgets('Translated child into translated box - hit test', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey();
bool pointerDown = false;
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -318,13 +316,23 @@ void main() {
await tester.pump();
actualRotatedBox = tester.widget(find.byType(Transform));
actualTurns = actualRotatedBox.transform;
expect(actualTurns, Matrix4.rotationZ(math.pi));
expect(actualTurns, Matrix4.fromList(<double>[
-1.0, 0.0, 0.0, 0.0,
0.0, -1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose());
controller.value = 0.75;
await tester.pump();
actualRotatedBox = tester.widget(find.byType(Transform));
actualTurns = actualRotatedBox.transform;
expect(actualTurns, Matrix4.rotationZ(math.pi * 1.5));
expect(actualTurns, Matrix4.fromList(<double>[
0.0, 1.0, 0.0, 0.0,
-1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose());
});
testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async {
......
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