I have a Container
with a child that I want to rotate. When rotating, I want the size of the Container
to match the new borders of the child.
Let's say I have the following setup:
Container(
margin: EdgeInsets.all(200),
child: Stack(
children: [
IntrinsicHeight(
child: Container(
color: Colors.green,
child: Transform.rotate(
angle: 45 * 3.14 / 180,
child: Container(
color: Colors.amber,
child: Text("Whatever"),
)
),
),
),
],
),
),
This gives the following output:
My desired output is:
So that the Container
matches the acutal dimentions of the rotated child. How can I achieve this?
I thought IntrinsicHeight
would do the trick but it doesn't seem to care about rotation. I also tried rotating with RotationTransition
, but that didn't work either
Quite a quiz! I think it is possible, but I do not know how to achieve this with any predefined widget. Instead, I tried to implement this logic with custom RenderObject
and RenderBox
. Here is what I got.
In 2 words: we apply transformations to inner container during layout mode and calculate size for outer widget based on size of transformed container.
First of all, have to say that will include some heavy math computations, so take it to account.
Now, we need some imports for calculating rotations (we will do it on our own, because unfortunately Transform.rotate
will not provide us necessary info about rotation angles):
import 'dart:math' as math;
import 'package:vector_math/vector_math_64.dart' show Vector3;
Next, create container-like widget, which will take one child (exclusively) and rotation. I also added color parameter, but you can add more properties if you need.
class FittedRotationContainer extends StatelessWidget {
const FittedRotationContainer({
Key? key,
required this.child,
required this.transform,
this.color,
}) : super(key: key);
final Widget child;
final Matrix4 transform;
final Color? color;
@override
Widget build(BuildContext context) {
return FittedTransformBox(
transform: transform,
color: color,
child: child,
);
}
}
Now, things getting a little heavy. We have to implement actual painting logic. We will use SingleChildRenderObjectWidget
and RenderProxyBox
inside it. They work together kinda like StatefulWidget
and State
.
class FittedTransformBox extends SingleChildRenderObjectWidget {
const FittedTransformBox({
super.key,
required this.transform,
super.child,
this.color,
});
final Matrix4 transform;
final Color? color;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderFittedTransformBox(transform: transform, color: color);
}
@override
void updateRenderObject(
BuildContext context, _RenderFittedTransformBox renderObject) {
renderObject
..transform = transform
..color = color;
}
}
class _RenderFittedTransformBox extends RenderProxyBox {
_RenderFittedTransformBox({
required Matrix4 transform,
Color? color,
}) : _transform = transform,
_color = color;
Matrix4 get transform => _transform;
Matrix4 _transform;
set transform(Matrix4 value) {
if (_transform != value) {
_transform = value;
markNeedsLayout();
}
}
Color? get color => _color;
Color? _color;
set color(Color? value) {
if (_color != value) {
_color = value;
markNeedsPaint();
}
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData();
}
}
@override
void performLayout() {
if (child != null) {
child!.layout(constraints.loosen(), parentUsesSize: true);
final childSize = child!.size;
final childParentData = child!.parentData as BoxParentData;
final transformedCorners = [
_transform.transform3(Vector3(0, 0, 0)),
_transform.transform3(Vector3(childSize.width, 0, 0)),
_transform.transform3(Vector3(0, childSize.height, 0)),
_transform.transform3(Vector3(childSize.width, childSize.height, 0)),
];
double minX = double.infinity, minY = double.infinity;
double maxX = double.negativeInfinity, maxY = double.negativeInfinity;
for (final corner in transformedCorners) {
minX = math.min(minX, corner.x);
minY = math.min(minY, corner.y);
maxX = math.max(maxX, corner.x);
maxY = math.max(maxY, corner.y);
}
final fittedSize = Size(maxX - minX, maxY - minY);
size = constraints.constrain(fittedSize);
childParentData.offset = Offset(
(size.width - fittedSize.width) / 2 - minX,
(size.height - fittedSize.height) / 2 - minY,
);
} else {
size = constraints.smallest;
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (_color != null) {
context.canvas.drawRect(offset & size, Paint()..color = _color!);
}
if (child != null) {
final childParentData = child!.parentData as BoxParentData;
context.pushTransform(
needsCompositing,
offset + childParentData.offset,
_transform,
super.paint,
);
}
}
}
And that's how you can use it inside your app:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
margin: EdgeInsets.all(200),
child: Stack(
children: [
Center(
child: FittedRotationContainer(
color: Colors.green,
transform: Matrix4.rotationZ(90 * math.pi / 180),
child: Container(
color: Colors.amber,
child: const Padding(
padding: EdgeInsets.all(20.0),
child: Text("Whatever"),
),
),
),
)
],
),
),
);
}