Commit 3b56f122 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Fix composited transform bounds calculations (#6302)

We weren't computing the bounds for composited transforms correctly. We
need to conjugate the transform by the offset in order to get the
correct paint bounds for the composited layer. We now also use the same
math in the non-composited case for consistency.

Also, don't scale the z-coordinate in RenderFittedBox.

Fixes #6293
parent 5b292aab
...@@ -107,13 +107,13 @@ class MatrixUtils { ...@@ -107,13 +107,13 @@ class MatrixUtils {
return math.max(a, math.max(b, math.max(c, d))); return math.max(a, math.max(b, math.max(c, d)));
} }
/// Returns a rect that bounds the result of applying the given matrix as a /// Returns a rect that bounds the result of applying the inverse of the given
/// perspective transform to the given rect. /// matrix as a perspective transform to the given rect.
/// ///
/// This function assumes the given rect is in the plane with z equals 0.0. /// This function assumes the given rect is in the plane with z equals 0.0.
/// The transformed rect is then projected back into the plane with z equals /// The transformed rect is then projected back into the plane with z equals
/// 0.0 before computing its bounding rect. /// 0.0 before computing its bounding rect.
static Rect transformRect(Rect rect, Matrix4 transform) { static Rect inverseTransformRect(Rect rect, Matrix4 transform) {
assert(rect != null); assert(rect != null);
assert(transform.determinant != 0.0); assert(transform.determinant != 0.0);
if (isIdentity(transform)) if (isIdentity(transform))
......
...@@ -445,17 +445,20 @@ class TransformLayer extends OffsetLayer { ...@@ -445,17 +445,20 @@ class TransformLayer extends OffsetLayer {
/// The [transform] property must be non-null before the compositing phase of /// The [transform] property must be non-null before the compositing phase of
/// the pipeline. /// the pipeline.
TransformLayer({ TransformLayer({
Offset offset: Offset.zero,
this.transform this.transform
}): super(offset: offset); });
/// The matrix to apply /// The matrix to apply
Matrix4 transform; Matrix4 transform;
@override @override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) { void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
Matrix4 effectiveTransform = new Matrix4.translationValues(offset.dx + layerOffset.dx, offset.dy + layerOffset.dy, 0.0) assert(offset == Offset.zero);
..multiply(transform); Matrix4 effectiveTransform = transform;
if (layerOffset != Offset.zero) {
effectiveTransform = new Matrix4.translationValues(layerOffset.dx, layerOffset.dy, 0.0)
..multiply(transform);
}
builder.pushTransform(effectiveTransform.storage); builder.pushTransform(effectiveTransform.storage);
addChildrenToScene(builder, Offset.zero); addChildrenToScene(builder, Offset.zero);
builder.pop(); builder.pop();
......
...@@ -241,17 +241,17 @@ class PaintingContext { ...@@ -241,17 +241,17 @@ class PaintingContext {
/// * `painter` is a callback that will paint with the [clipRect] applied. This /// * `painter` is a callback that will paint with the [clipRect] applied. This
/// function calls the [painter] synchronously. /// function calls the [painter] synchronously.
void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) { void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) {
Rect offsetClipRect = clipRect.shift(offset); final Rect offsetClipRect = clipRect.shift(offset);
if (needsCompositing) { if (needsCompositing) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
ClipRectLayer clipLayer = new ClipRectLayer(clipRect: offsetClipRect); final ClipRectLayer clipLayer = new ClipRectLayer(clipRect: offsetClipRect);
_appendLayer(clipLayer); _appendLayer(clipLayer);
PaintingContext childContext = new PaintingContext._(clipLayer, offsetClipRect); final PaintingContext childContext = new PaintingContext._(clipLayer, offsetClipRect);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} else { } else {
canvas.save(); canvas.save();
canvas.clipRect(clipRect.shift(offset)); canvas.clipRect(offsetClipRect);
painter(this, offset); painter(this, offset);
canvas.restore(); canvas.restore();
} }
...@@ -270,13 +270,13 @@ class PaintingContext { ...@@ -270,13 +270,13 @@ class PaintingContext {
/// * `painter` is a callback that will paint with the `clipRRect` applied. This /// * `painter` is a callback that will paint with the `clipRRect` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter) { void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter) {
Rect offsetBounds = bounds.shift(offset); final Rect offsetBounds = bounds.shift(offset);
RRect offsetClipRRect = clipRRect.shift(offset); final RRect offsetClipRRect = clipRRect.shift(offset);
if (needsCompositing) { if (needsCompositing) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
ClipRRectLayer clipLayer = new ClipRRectLayer(clipRRect: offsetClipRRect); final ClipRRectLayer clipLayer = new ClipRRectLayer(clipRRect: offsetClipRRect);
_appendLayer(clipLayer); _appendLayer(clipLayer);
PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds); final PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} else { } else {
...@@ -300,13 +300,13 @@ class PaintingContext { ...@@ -300,13 +300,13 @@ class PaintingContext {
/// * `painter` is a callback that will paint with the `clipPath` applied. This /// * `painter` is a callback that will paint with the `clipPath` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter) { void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter) {
Rect offsetBounds = bounds.shift(offset); final Rect offsetBounds = bounds.shift(offset);
Path offsetClipPath = clipPath.shift(offset); final Path offsetClipPath = clipPath.shift(offset);
if (needsCompositing) { if (needsCompositing) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
ClipPathLayer clipLayer = new ClipPathLayer(clipPath: offsetClipPath); final ClipPathLayer clipLayer = new ClipPathLayer(clipPath: offsetClipPath);
_appendLayer(clipLayer); _appendLayer(clipLayer);
PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds); final PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} else { } else {
...@@ -327,20 +327,20 @@ class PaintingContext { ...@@ -327,20 +327,20 @@ class PaintingContext {
/// * `painter` is a callback that will paint with the `transform` applied. This /// * `painter` is a callback that will paint with the `transform` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) { void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) {
final Matrix4 effectiveTransform = new Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)..translate(-offset.dx, -offset.dy);
if (needsCompositing) { if (needsCompositing) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
TransformLayer transformLayer = new TransformLayer(offset: offset, transform: transform); final TransformLayer transformLayer = new TransformLayer(transform: effectiveTransform);
_appendLayer(transformLayer); _appendLayer(transformLayer);
// TODO(abarth): We need to run _paintBounds through the inverse of transform. final Rect transformedPaintBounds = MatrixUtils.inverseTransformRect(_paintBounds, effectiveTransform);
PaintingContext childContext = new PaintingContext._(transformLayer, _paintBounds); final PaintingContext childContext = new PaintingContext._(transformLayer, transformedPaintBounds);
painter(childContext, Offset.zero); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} else { } else {
Matrix4 offsetMatrix = new Matrix4.translationValues(offset.dx, offset.dy, 0.0);
Matrix4 transformWithOffset = offsetMatrix * transform;
canvas.save(); canvas.save();
canvas.transform(transformWithOffset.storage); canvas.transform(effectiveTransform.storage);
painter(this, Offset.zero); painter(this, offset);
canvas.restore(); canvas.restore();
} }
} }
...@@ -356,9 +356,9 @@ class PaintingContext { ...@@ -356,9 +356,9 @@ class PaintingContext {
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
OpacityLayer opacityLayer = new OpacityLayer(alpha: alpha); final OpacityLayer opacityLayer = new OpacityLayer(alpha: alpha);
_appendLayer(opacityLayer); _appendLayer(opacityLayer);
PaintingContext childContext = new PaintingContext._(opacityLayer, _paintBounds); final PaintingContext childContext = new PaintingContext._(opacityLayer, _paintBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} }
...@@ -377,13 +377,13 @@ class PaintingContext { ...@@ -377,13 +377,13 @@ class PaintingContext {
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushShaderMask(Offset offset, Shader shader, Rect maskRect, TransferMode transferMode, PaintingContextCallback painter) { void pushShaderMask(Offset offset, Shader shader, Rect maskRect, TransferMode transferMode, PaintingContextCallback painter) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
ShaderMaskLayer shaderLayer = new ShaderMaskLayer( final ShaderMaskLayer shaderLayer = new ShaderMaskLayer(
shader: shader, shader: shader,
maskRect: maskRect, maskRect: maskRect,
transferMode: transferMode transferMode: transferMode
); );
_appendLayer(shaderLayer); _appendLayer(shaderLayer);
PaintingContext childContext = new PaintingContext._(shaderLayer, _paintBounds); final PaintingContext childContext = new PaintingContext._(shaderLayer, _paintBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} }
...@@ -395,9 +395,9 @@ class PaintingContext { ...@@ -395,9 +395,9 @@ class PaintingContext {
// TODO(abarth): I don't quite understand how this API is supposed to work. // TODO(abarth): I don't quite understand how this API is supposed to work.
void pushBackdropFilter(Offset offset, ui.ImageFilter filter, PaintingContextCallback painter) { void pushBackdropFilter(Offset offset, ui.ImageFilter filter, PaintingContextCallback painter) {
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
BackdropFilterLayer backdropFilterLayer = new BackdropFilterLayer(filter: filter); final BackdropFilterLayer backdropFilterLayer = new BackdropFilterLayer(filter: filter);
_appendLayer(backdropFilterLayer); _appendLayer(backdropFilterLayer);
PaintingContext childContext = new PaintingContext._(backdropFilterLayer, _paintBounds); final PaintingContext childContext = new PaintingContext._(backdropFilterLayer, _paintBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} }
...@@ -487,7 +487,7 @@ class _SemanticsGeometry { ...@@ -487,7 +487,7 @@ class _SemanticsGeometry {
} else { } else {
Matrix4 clipTransform = new Matrix4.identity(); Matrix4 clipTransform = new Matrix4.identity();
parent.applyPaintTransform(child, clipTransform); parent.applyPaintTransform(child, clipTransform);
clipRect = MatrixUtils.transformRect(clipRect, clipTransform); clipRect = MatrixUtils.inverseTransformRect(clipRect, clipTransform);
} }
} }
parent.applyPaintTransform(child, transform); parent.applyPaintTransform(child, transform);
......
...@@ -1479,7 +1479,7 @@ class RenderFittedBox extends RenderProxyBox { ...@@ -1479,7 +1479,7 @@ class RenderFittedBox extends RenderProxyBox {
final Rect destinationRect = _alignment.inscribe(sizes.destination, Point.origin & size); final Rect destinationRect = _alignment.inscribe(sizes.destination, Point.origin & size);
_hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.width; _hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.width;
_transform = new Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0) _transform = new Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0)
..scale(scaleX, scaleY) ..scale(scaleX, scaleY, 1.0)
..translate(-sourceRect.left, -sourceRect.top); ..translate(-sourceRect.left, -sourceRect.top);
} }
} }
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
void main() { void main() {
testWidgets('Transform origin', (WidgetTester tester) async { testWidgets('Transform origin', (WidgetTester tester) async {
...@@ -18,9 +20,9 @@ void main() { ...@@ -18,9 +20,9 @@ void main() {
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFF0000FF) backgroundColor: new Color(0xFF0000FF),
) ),
) ),
), ),
new Positioned( new Positioned(
top: 100.0, top: 100.0,
...@@ -37,15 +39,15 @@ void main() { ...@@ -37,15 +39,15 @@ void main() {
}, },
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFF00FFFF) backgroundColor: new Color(0xFF00FFFF),
) ),
) ),
) ),
) ),
) ),
) ),
] ],
) ),
); );
expect(didReceiveTap, isFalse); expect(didReceiveTap, isFalse);
...@@ -67,9 +69,9 @@ void main() { ...@@ -67,9 +69,9 @@ void main() {
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFF0000FF) backgroundColor: new Color(0xFF0000FF),
) ),
) ),
), ),
new Positioned( new Positioned(
top: 100.0, top: 100.0,
...@@ -86,15 +88,15 @@ void main() { ...@@ -86,15 +88,15 @@ void main() {
}, },
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFF00FFFF) backgroundColor: new Color(0xFF00FFFF),
) ),
) ),
) ),
) ),
) ),
) ),
] ],
) ),
); );
expect(didReceiveTap, isFalse); expect(didReceiveTap, isFalse);
...@@ -106,46 +108,44 @@ void main() { ...@@ -106,46 +108,44 @@ void main() {
testWidgets('Transform offset + alignment', (WidgetTester tester) async { testWidgets('Transform offset + alignment', (WidgetTester tester) async {
bool didReceiveTap = false; bool didReceiveTap = false;
await tester.pumpWidget( await tester.pumpWidget(new Stack(
new Stack( children: <Widget>[
children: <Widget>[ new Positioned(
new Positioned( top: 100.0,
top: 100.0, left: 100.0,
left: 100.0, child: new Container(
child: new Container( width: 100.0,
width: 100.0, height: 100.0,
height: 100.0, decoration: new BoxDecoration(
decoration: new BoxDecoration( backgroundColor: new Color(0xFF0000FF),
backgroundColor: new Color(0xFF0000FF) ),
)
)
), ),
new Positioned( ),
top: 100.0, new Positioned(
left: 100.0, top: 100.0,
child: new Container( left: 100.0,
width: 100.0, child: new Container(
height: 100.0, width: 100.0,
child: new Transform( height: 100.0,
transform: new Matrix4.diagonal3Values(0.5, 0.5, 1.0), child: new Transform(
origin: new Offset(100.0, 0.0), transform: new Matrix4.diagonal3Values(0.5, 0.5, 1.0),
alignment: new FractionalOffset(0.0, 0.5), origin: new Offset(100.0, 0.0),
child: new GestureDetector( alignment: new FractionalOffset(0.0, 0.5),
onTap: () { child: new GestureDetector(
didReceiveTap = true; onTap: () {
}, didReceiveTap = true;
child: new Container( },
decoration: new BoxDecoration( child: new Container(
backgroundColor: new Color(0xFF00FFFF) decoration: new BoxDecoration(
) backgroundColor: new Color(0xFF00FFFF),
) ),
) ),
) ),
) ),
) ),
] ),
) ],
); ));
expect(didReceiveTap, isFalse); expect(didReceiveTap, isFalse);
await tester.tapAt(new Point(110.0, 110.0)); await tester.tapAt(new Point(110.0, 110.0));
...@@ -153,4 +153,36 @@ void main() { ...@@ -153,4 +153,36 @@ void main() {
await tester.tapAt(new Point(190.0, 150.0)); await tester.tapAt(new Point(190.0, 150.0));
expect(didReceiveTap, isTrue); expect(didReceiveTap, isTrue);
}); });
testWidgets('Composited transform offset', (WidgetTester tester) async {
await tester.pumpWidget(
new Center(
child: new SizedBox(
width: 400.0,
height: 300.0,
child: new ClipRect(
child: new Transform(
transform: new Matrix4.diagonal3Values(0.5, 0.5, 1.0),
child: new Opacity(
opacity: 0.9,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: const Color(0xFF00FF00),
),
),
),
),
),
),
),
);
List<Layer> layers = tester.layers
..retainWhere((Layer layer) => layer is TransformLayer);
expect(layers.length, 2);
// The first transform is from the render view.
TransformLayer layer = layers[1];
Matrix4 transform = layer.transform;
expect(transform.getTranslation(), equals(new Vector3(100.0, 75.0, 0.0)));
});
} }
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