flutterdartresizetransform

Flutter IntrinsicHeight and IntrinsicWidth don't account for Transform.rotate


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:

enter image description here

My desired output is:

enter image description here

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


Solution

  • 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.

    enter image description here

    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"),
                      ),
                    ),
                  ),
                )
              ],
            ),
          ),
        );
      }