Skip to content

Commit 1573934

Browse files
author
Jonah Williams
authored
[framework] don't composite with a scale of 0.0 (#106982)
1 parent c51bf2f commit 1573934

File tree

4 files changed

+200
-5
lines changed

4 files changed

+200
-5
lines changed

packages/flutter/lib/src/rendering/proxy_box.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2604,6 +2604,13 @@ class RenderTransform extends RenderProxyBox {
26042604
if (filterQuality == null) {
26052605
final Offset? childOffset = MatrixUtils.getAsTranslation(transform);
26062606
if (childOffset == null) {
2607+
// if the matrix is singular the children would be compressed to a line or
2608+
// single point, instead short-circuit and paint nothing.
2609+
final double det = transform.determinant();
2610+
if (det == 0 || !det.isFinite) {
2611+
layer = null;
2612+
return;
2613+
}
26072614
layer = context.pushTransform(
26082615
needsCompositing,
26092616
offset,

packages/flutter/lib/src/widgets/basic.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:math' as math;
56
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
67

78
import 'package:flutter/animation.dart';
@@ -1304,7 +1305,7 @@ class Transform extends SingleChildRenderObjectWidget {
13041305
this.transformHitTests = true,
13051306
this.filterQuality,
13061307
super.child,
1307-
}) : transform = Matrix4.rotationZ(angle);
1308+
}) : transform = _computeRotation(angle);
13081309

13091310
/// Creates a widget that transforms its child using a translation.
13101311
///
@@ -1381,6 +1382,38 @@ class Transform extends SingleChildRenderObjectWidget {
13811382
assert(scale == null || (scaleX == null && scaleY == null), "If 'scale' is non-null then 'scaleX' and 'scaleY' must be left null"),
13821383
transform = Matrix4.diagonal3Values(scale ?? scaleX ?? 1.0, scale ?? scaleY ?? 1.0, 1.0);
13831384

1385+
// Computes a rotation matrix for an angle in radians, attempting to keep rotations
1386+
// at integral values for angles of 0, π/2, π, 3π/2.
1387+
static Matrix4 _computeRotation(double radians) {
1388+
assert(radians.isFinite, 'Cannot compute the rotation matrix for a non-finite angle: $radians');
1389+
if (radians == 0.0) {
1390+
return Matrix4.identity();
1391+
}
1392+
final double sin = math.sin(radians);
1393+
if (sin == 1.0) {
1394+
return _createZRotation(1.0, 0.0);
1395+
}
1396+
if (sin == -1.0) {
1397+
return _createZRotation(-1.0, 0.0);
1398+
}
1399+
final double cos = math.cos(radians);
1400+
if (cos == -1.0) {
1401+
return _createZRotation(0.0, -1.0);
1402+
}
1403+
return _createZRotation(sin, cos);
1404+
}
1405+
1406+
static Matrix4 _createZRotation(double sin, double cos) {
1407+
final Matrix4 result = Matrix4.zero();
1408+
result.storage[0] = cos;
1409+
result.storage[1] = sin;
1410+
result.storage[4] = -sin;
1411+
result.storage[5] = cos;
1412+
result.storage[10] = 1.0;
1413+
result.storage[15] = 1.0;
1414+
return result;
1415+
}
1416+
13841417
/// The matrix to transform the child by during painting.
13851418
final Matrix4 transform;
13861419

packages/flutter/test/widgets/transform_test.dart

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,153 @@ void main() {
338338
]);
339339
});
340340

341+
testWidgets('Transform with nan value short-circuits rendering', (WidgetTester tester) async {
342+
await tester.pumpWidget(
343+
Transform(
344+
transform: Matrix4.identity()
345+
..storage[0] = double.nan,
346+
child: RepaintBoundary(child: Container()),
347+
),
348+
);
349+
350+
expect(tester.layers, hasLength(1));
351+
});
352+
353+
testWidgets('Transform with inf value short-circuits rendering', (WidgetTester tester) async {
354+
await tester.pumpWidget(
355+
Transform(
356+
transform: Matrix4.identity()
357+
..storage[0] = double.infinity,
358+
child: RepaintBoundary(child: Container()),
359+
),
360+
);
361+
362+
expect(tester.layers, hasLength(1));
363+
});
364+
365+
testWidgets('Transform with -inf value short-circuits rendering', (WidgetTester tester) async {
366+
await tester.pumpWidget(
367+
Transform(
368+
transform: Matrix4.identity()
369+
..storage[0] = double.negativeInfinity,
370+
child: RepaintBoundary(child: Container()),
371+
),
372+
);
373+
374+
expect(tester.layers, hasLength(1));
375+
});
376+
377+
testWidgets('Transform.rotate does not remove layers due to singular short-circuit', (WidgetTester tester) async {
378+
await tester.pumpWidget(
379+
Transform.rotate(
380+
angle: math.pi / 2,
381+
child: RepaintBoundary(child: Container()),
382+
),
383+
);
384+
385+
expect(tester.layers, hasLength(3));
386+
});
387+
388+
testWidgets('Transform.rotate creates nice rotation matrices for 0, 90, 180, 270 degrees', (WidgetTester tester) async {
389+
await tester.pumpWidget(
390+
Transform.rotate(
391+
angle: math.pi / 2,
392+
child: RepaintBoundary(child: Container()),
393+
),
394+
);
395+
396+
expect(tester.layers[1], isA<TransformLayer>()
397+
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
398+
0.0, -1.0, 0.0, 700.0,
399+
1.0, 0.0, 0.0, -100.0,
400+
0.0, 0.0, 1.0, 0.0,
401+
0.0, 0.0, 0.0, 1.0,
402+
])..transpose()))
403+
);
404+
405+
await tester.pumpWidget(
406+
Transform.rotate(
407+
angle: math.pi,
408+
child: RepaintBoundary(child: Container()),
409+
),
410+
);
411+
412+
expect(tester.layers[1], isA<TransformLayer>()
413+
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
414+
-1.0, 0.0, 0.0, 800.0,
415+
0.0, -1.0, 0.0, 600.0,
416+
0.0, 0.0, 1.0, 0.0,
417+
0.0, 0.0, 0.0, 1.0,
418+
])..transpose()))
419+
);
420+
421+
await tester.pumpWidget(
422+
Transform.rotate(
423+
angle: 3 * math.pi / 2,
424+
child: RepaintBoundary(child: Container()),
425+
),
426+
);
427+
428+
expect(tester.layers[1], isA<TransformLayer>()
429+
.having((TransformLayer layer) => layer.transform, 'transform', equals(Matrix4.fromList(<double>[
430+
0.0, 1.0, 0.0, 100.0,
431+
-1.0, 0.0, 0.0, 700.0,
432+
0.0, 0.0, 1.0, 0.0,
433+
0.0, 0.0, 0.0, 1.0,
434+
])..transpose()))
435+
);
436+
437+
await tester.pumpWidget(
438+
Transform.rotate(
439+
angle: 0,
440+
child: RepaintBoundary(child: Container()),
441+
),
442+
);
443+
444+
// No transform layer created
445+
expect(tester.layers[1], isA<OffsetLayer>());
446+
expect(tester.layers, hasLength(2));
447+
});
448+
449+
testWidgets('Transform.scale with 0.0 does not paint child layers', (WidgetTester tester) async {
450+
await tester.pumpWidget(
451+
Transform.scale(
452+
scale: 0.0,
453+
child: RepaintBoundary(child: Container()),
454+
),
455+
);
456+
457+
expect(tester.layers, hasLength(1)); // root transform layer
458+
459+
await tester.pumpWidget(
460+
Transform.scale(
461+
scaleX: 0.0,
462+
child: RepaintBoundary(child: Container()),
463+
),
464+
);
465+
466+
expect(tester.layers, hasLength(1));
467+
468+
await tester.pumpWidget(
469+
Transform.scale(
470+
scaleY: 0.0,
471+
child: RepaintBoundary(child: Container()),
472+
),
473+
);
474+
475+
expect(tester.layers, hasLength(1));
476+
477+
await tester.pumpWidget(
478+
Transform.scale(
479+
scale: 0.01, // small but non-zero
480+
child: RepaintBoundary(child: Container()),
481+
),
482+
);
483+
484+
expect(tester.layers, hasLength(3));
485+
});
486+
487+
341488
testWidgets('Translated child into translated box - hit test', (WidgetTester tester) async {
342489
final GlobalKey key1 = GlobalKey();
343490
bool pointerDown = false;

packages/flutter/test/widgets/transitions_test.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:math' as math;
6-
75
import 'package:flutter/material.dart';
86
import 'package:flutter/rendering.dart';
97
import 'package:flutter_test/flutter_test.dart';
@@ -318,13 +316,23 @@ void main() {
318316
await tester.pump();
319317
actualRotatedBox = tester.widget(find.byType(Transform));
320318
actualTurns = actualRotatedBox.transform;
321-
expect(actualTurns, Matrix4.rotationZ(math.pi));
319+
expect(actualTurns, Matrix4.fromList(<double>[
320+
-1.0, 0.0, 0.0, 0.0,
321+
0.0, -1.0, 0.0, 0.0,
322+
0.0, 0.0, 1.0, 0.0,
323+
0.0, 0.0, 0.0, 1.0,
324+
])..transpose());
322325

323326
controller.value = 0.75;
324327
await tester.pump();
325328
actualRotatedBox = tester.widget(find.byType(Transform));
326329
actualTurns = actualRotatedBox.transform;
327-
expect(actualTurns, Matrix4.rotationZ(math.pi * 1.5));
330+
expect(actualTurns, Matrix4.fromList(<double>[
331+
0.0, 1.0, 0.0, 0.0,
332+
-1.0, 0.0, 0.0, 0.0,
333+
0.0, 0.0, 1.0, 0.0,
334+
0.0, 0.0, 0.0, 1.0,
335+
])..transpose());
328336
});
329337

330338
testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async {

0 commit comments

Comments
 (0)