flutterdart

Resizing a rotated container in flutter


I have a following code:

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math.dart' as vector;
import 'dart:math' as math;

void main() {
  debugPaintSizeEnabled = true;
  return runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.grey,
        body: SafeArea(
          child: TestComp(),
        ),
      ),
    );
  }
}

class TestComp extends StatefulWidget {
  @override
  _TestCompState createState() => _TestCompState();
}

class _TestCompState extends State<TestComp> {
  double _y = 0;
  double _x = 0;

  double _height = 200;
  double _width = 300;

  int angle = -136;

  vector.Vector4 testRotation(var dx, var dy) {
    vector.Matrix4 matrix = vector.Matrix4.identity();
    matrix..rotateZ(math.pi / 180 * angle);
    return matrix * vector.Vector4(dx, dy, 0, 0);
  }

  double get _aspectRatio {
    return _height / _width;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned(
          top: _y,
          left: _x,
          child: Transform.rotate(
            angle: math.pi / 180 * angle, //-136,
            child: Container(
              height: _height,
              width: _width,
              color: Colors.black,
              child: Stack(
                overflow: Overflow.visible,
                children: <Widget>[
                  Positioned(
                    top: 0,
                    left: 0,
                    child: GestureDetector(
                      onPanUpdate: (details) {
                        var newdx =
                            testRotation(details.delta.dx, details.delta.dy)[0];
                        var newdy =
                            testRotation(details.delta.dx, details.delta.dy)[1];
                        setState(() {
                          _y += newdy;
                          _x += newdx;
                        });
                      },
                      child: Image.network(
                        "https://via.placeholder.com/300x200",
                        height: _height,
                        width: _width,
                        fit: BoxFit.fill,
                      ),
                    ),
                  ),
                  Positioned(
                    top: 0,
                    left: 0,
                    child: GestureDetector(
                      onPanUpdate: (DragUpdateDetails details) {
                        var position = testRotation(
                          details.delta.dx,
                          details.delta.dy,
                        );
                        // print(position);
                        var dx = position[0];
                        var dy = position[1];

                        var newWidth = _width - dx;
                        var newHeight = newWidth * _aspectRatio;
                        setState(() {
                          _y = _y + (_height - newHeight);
                          _x = _x + dx;
                          _width = newWidth;
                          _height = newHeight;
                        });
                      },
                      child: Container(
                        width: 10,
                        height: 10,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          borderRadius: BorderRadius.circular(10),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ],
    );
  }
}

There's a handle in top left of image which will be used to resize. I expect the rotation to be like:

enter image description here

But it doesn't work that way. The resize is messed up. Please help me in this.


Solution

  • try this FooResizer widget - you could add 4 additional corner sizers in the same way:

    final SIZE = 64.0;
    final sizerSize = Size.square(SIZE);
    
    final sizers = [
      Sizer(Alignment.centerLeft, EdgeInsets.only(left: 1), Offset(1, 0)),
      Sizer(Alignment.topCenter, EdgeInsets.only(top: 1), Offset(0, 1)),
      Sizer(Alignment.centerRight, EdgeInsets.only(right: 1), Offset(-1, 0)),
      Sizer(Alignment.bottomCenter, EdgeInsets.only(bottom: 1), Offset(0, -1)),
    ];
    
    class Sizer {
      final Alignment alignment;
      final EdgeInsets insets;
      final Offset mask;
      Sizer(this.alignment, this.insets, this.mask);
    }
    
    class FooResizer extends StatefulWidget {
      @override
      _FooResizerState createState() => _FooResizerState();
    }
    
    class _FooResizerState extends State<FooResizer> with TickerProviderStateMixin {
      ValueNotifier<Rect> rect;
      ValueNotifier<int> serial = ValueNotifier<int>(0);
      Sizer currentSizer;
      Rect savedRect;
      Offset savedOffset;
      AnimationController controller;
      GlobalKey stackKey = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        controller ??= AnimationController(vsync: this, duration: Duration(milliseconds: 300));
        return LayoutBuilder(
          builder: (context, constraints) {
            _init(constraints.biggest);
            return AnimatedBuilder(
              animation: Listenable.merge([rect, serial, controller]),
              builder: (context, child) {
                return Transform.rotate(
                  angle: math.pi / 8,
                  child: Stack(
                    key: stackKey,
                    overflow: Overflow.visible,
                    children: [
                      Positioned.fromRect(
                        rect: rect.value,
                        child: Material(
                          borderRadius: BorderRadius.circular(12),
                          color: Colors.grey[300],
                          elevation: 4,
                          child: FlutterLogo(),
                        ),
                      ),
                      ...sizers.map(_sizerBuilder),
                    ],
                  ),
                );
              },
            );
          },
        );
      }
    
      Widget _sizerBuilder(Sizer sizer) {
        var isCurrent = currentSizer == sizer;
        var finalRect = sizer.alignment.inscribe(sizerSize, rect.value).shift(sizer.mask * SIZE * -0.33);
        var centerRect = Alignment.center.inscribe(sizerSize, rect.value);
        var interpolated = Rect.lerp(finalRect, centerRect, isCurrent? 0 : controller.value);
        return Positioned.fromRect(
          rect: interpolated,
          child: GestureDetector(
            onPanStart: (details) => _panStart(sizer, details),
            onPanUpdate: _panUpdate,
            onPanEnd: _panEnd,
            child: Opacity(
              opacity: isCurrent? 1 : 1 - controller.value,
              child: AnimatedContainer(
                duration: isCurrent? Duration(milliseconds: 500) : Duration.zero,
                decoration: ShapeDecoration(
                  shape: isCurrent? RoundedRectangleBorder() : CircleBorder(),
                  color: isCurrent? Colors.orange[800] : Colors.green,
                ),
              ),
            ),
          ),
        );
      }
    
      _panStart(Sizer sizer, DragStartDetails details) {
        // timeDilation = 10;
        currentSizer = sizer;
        savedRect = rect.value;
        savedOffset = _invertedOffset(details.globalPosition);
        serial.value++;
        controller.forward();
      }
    
      _panUpdate(DragUpdateDetails details) {
        assert(currentSizer != null);
        var delta = savedOffset - _invertedOffset(details.globalPosition);
        var insets = EdgeInsets.fromLTRB(
          currentSizer.insets.left * delta.dx * currentSizer.mask.dx,
          currentSizer.insets.top * delta.dy * currentSizer.mask.dy,
          currentSizer.insets.right * delta.dx * currentSizer.mask.dx,
          currentSizer.insets.bottom * delta.dy * currentSizer.mask.dy,
        );
        rect.value = insets.inflateRect(savedRect);
      }
    
      _panEnd(DragEndDetails details) {
        assert(currentSizer != null);
        currentSizer = null;
        serial.value++;
        controller.reverse();
      }
    
      Offset _invertedOffset(Offset offset) {
        RenderBox rb = stackKey.currentContext.findRenderObject();
        return rb.globalToLocal(offset);
      }
    
      void _init(Size size) {
        var r = Offset.zero & size;
        rect = ValueNotifier(r.deflate(100));
      }
    }
    

    and the result would be something like this:

    enter image description here