我们如何在颤动中拖动小部件时实现定位线?

Posted

技术标签:

【中文标题】我们如何在颤动中拖动小部件时实现定位线?【英文标题】:How can we achieve the positioned lines while dragging widget in flutter? 【发布时间】:2021-09-07 17:43:58 【问题描述】:

我试图在拖动时获取定位线(紫色一条)。请参阅该附件以了解有关此问题的更多信息。

【问题讨论】:

用 DecoratedBox 或 Container 等装饰围绕您的小部件! 你能举个例子吗? 【参考方案1】:

paint() 方法和canvas.drawPoints() 中使用canvas.drawPoints() 绘制线条 - 您可以更改它以绘制虚线

现在,黑色实线看起来像这样:

class FooResizer extends StatefulWidget 
  @override
  _FooResizerState createState() => _FooResizerState();


class _FooResizerState extends State<FooResizer> with TickerProviderStateMixin, ChangeNotifier 
  Sizer currentSizer;
  double angle = 0.0;
  AnimationController ctrl;

  final Map<String, Sizer> sizers = 
    'l': Sizer('l', Alignment.centerLeft, 't': 0.5, 'b': 0.5, 'M': 0.5),
    't': Sizer('t', Alignment.topCenter, 'l': 0.5, 'r': 0.5, 'M': 0.5),
    'r': Sizer('r', Alignment.centerRight, 't': 0.5, 'b': 0.5, 'R': 1.0, 'M': 0.5),
    'b': Sizer('b', Alignment.bottomCenter, 'l': 0.5, 'r': 0.5, 'R': 1.0, 'M': 0.5),
    'R': Sizer('R', Alignment.bottomRight, ),
    'M': Sizer('M', Alignment.center, ),
  ;

  @override
  void initState() 
    super.initState();
    ctrl = AnimationController(vsync: this, duration: Duration(milliseconds: 300), value: 1.0);
  

  @override
  Widget build(BuildContext context) 
    return ClipRect(
      child: ColoredBox(
        color: Colors.black12,
        // CustomMultiChildLayoutPainter:
        // https://gist.github.com/pskink/0f82724b41d9ebe89604782fbf62fe03#file-multi_layout_painter-dart-L447
        child: CustomMultiChildLayoutPainter(
          delegate: _FooResizerDelegate(sizers, this),
          children: [
            LayoutId(
              id: 'body',
              child: AnimatedBuilder(
                animation: this,
                builder: (ctx, child) 
                  return Transform.rotate(
                    angle: angle,
                    child: child,
                  );
                ,
                child: Material(color: Colors.grey[300], elevation: 4, child: FlutterLogo()),
              ),
            ),
            ...sizers.values.map(_sizerBuilder),
          ],
        ),
      ),
    );
  

  Widget _sizerBuilder(Sizer sizer) 
    final colors = 
      'M': Colors.orange,
      'R': Colors.teal,
    ;
    return LayoutId(
      id: sizer.id,
      child: GestureDetector(
        onPanStart: (details) => _panStart(sizer),
        onPanUpdate: _panUpdate,
        onPanEnd: _panEnd,
        child: AnimatedBuilder(
          animation: ctrl,
          builder: (context, child) 
            final color = colors[sizer.id] ?? Colors.green;
            return Opacity(
              opacity: currentSizer == sizer? 1.0 : Curves.ease.transform(ctrl.value),
              child: Container(
                decoration: ShapeDecoration(
                  shape: CircleBorder(side: BorderSide(width: lerpDouble(0.0, 2.0, ctrl.value), color: Colors.black38)),
                  color: currentSizer == sizer? Color.lerp(Colors.deepPurple, color, ctrl.value) : color,
                  shadows: [BoxShadow.lerp(BoxShadow(spreadRadius: 2, blurRadius: 4, offset: Offset(2, 2)), null, ctrl.value)],
                ),
              ),
            );
          
        ),
      ),
    );
  

  _panStart(Sizer sizer) 
    currentSizer = sizer;
    ctrl.reverse();
  

  _panUpdate(DragUpdateDetails details) 
    assert(currentSizer != null);
    if (currentSizer.id == 'M') 
      // move
      sizers.values.forEach((sizer) => sizer.center += details.delta);
     else
    if (currentSizer.id == 'R') 
      // rotate
      final localCenter = sizers['M'].center;
      final globalCenter = (context.findRenderObject() as RenderBox).localToGlobal(localCenter);

      final angle0 = (details.globalPosition - details.delta - globalCenter).direction;
      final angle1 = (details.globalPosition - globalCenter).direction;
      final deltaAngle = angle1 - angle0;
      sizers.values
        .where((sizer) => sizer.id != 'M')
        .forEach((sizer) 
          final vector = sizer.center - localCenter;
          sizer.center = localCenter + Offset.fromDirection(vector.direction + deltaAngle, vector.distance);
        );
      angle += deltaAngle;
     else 
      // resize
      final adjustedAngle = angle + currentSizer.angleAdjustment;
      final rotatedDistance = details.delta.distance * math.cos(details.delta.direction - adjustedAngle);
      final vector = Offset.fromDirection(adjustedAngle, rotatedDistance);
      currentSizer.center += vector;
      currentSizer.dependents.forEach((id, factor) => sizers[id].center += vector * factor);
    
    notifyListeners();
  

  _panEnd(DragEndDetails details) 
    assert(currentSizer != null);
    // currentSizer = null;
    ctrl.forward();
  



class _FooResizerDelegate extends MultiChildLayoutPainterDelegate 
  static const SIZE = 48.0;
  final Map<String, Sizer> sizers;

  _FooResizerDelegate(this.sizers, Listenable relayout) : super(relayout: relayout);

  @override
  void performLayout(Size size) 
    sizers['M'].center ??= init(size);

    for (var sizer in sizers.values) 
      layoutChild(sizer.id, BoxConstraints.tight(Size(SIZE, SIZE)));
      positionChild(sizer.id, sizer.center - Offset(SIZE / 2, SIZE / 2));
    
    final w = (sizers['l'].center - sizers['r'].center).distance;
    final h = (sizers['t'].center - sizers['b'].center).distance;
    layoutChild('body', BoxConstraints.tight(Size(w, h)));
    positionChild('body', sizers['M'].center - Offset(w / 2, h / 2));
  

  Offset init(Size size) 
    final rect = (Offset.zero & size).deflate(24);
    print('init rect: $rect');
    for (var sizer in sizers.values) 
      sizer
        ..center = sizer.alignment.withinRect(rect)
        ..angleAdjustment = sizer.alignment.x == 0? math.pi / 2 : 0;
    
    return sizers['M'].center;
  

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;

  @override
  void foregroundPaint(Canvas canvas, Size size) 
  

  @override
  void paint(Canvas canvas, Size size) 
    final w = (sizers['r'].center - sizers['l'].center).distance;
    final h = (sizers['b'].center - sizers['t'].center).distance;
    final rect = Rect.fromCenter(center: sizers['M'].center, width: w, height: h);
    final angle = (sizers['r'].center - sizers['l'].center).direction;
    final matrix = Matrix4.identity()
      ..translate(rect.center.dx, rect.center.dy)
      ..rotateZ(angle)
      ..translate(-rect.center.dx, -rect.center.dy);
    final transformedRect = MatrixUtils.transformRect(matrix, rect);
    final points = [
      Offset(transformedRect.left, 0), Offset(transformedRect.left, size.height),
      Offset(0, transformedRect.top), Offset(size.width, transformedRect.top),
      Offset(transformedRect.right, 0), Offset(transformedRect.right, size.height),
      Offset(0, transformedRect.bottom), Offset(size.width, transformedRect.bottom),
    ];
    canvas.drawPoints(PointMode.lines, points, Paint());
  


class Sizer 
  final String id;
  final Alignment alignment;
  final Map<String, double> dependents;
  Offset center;
  double angleAdjustment;
  Sizer(this.id, this.alignment, this.dependents);

【讨论】:

Sizer class Sizer final String id;最终对齐对齐;最终的 EdgeInsets 插图;最终偏移掩码; Sizer(this.id, this.alignment, this.insets, this.mask); 两个问题: 1. 当widget变得非常小时如何停止调整大小? 2. 如何在从角落拖动时以相同的比例调整高度和小部件的大小? 检查 _panUpdate 方法 - 特别是带有 // resize 注释的 else 分支 - 这是 currentSizer.center += vector; 更改 currentSizer 中心位置的地方 我明白了,但我必须检查什么才能让它在某个点停止调整大小? 你必须钳制vector,以便整个宽度/高度不会低于一些最小值 - 它只是一个矢量数学 - 我不熟悉 that library 但很可能是这里非常有用

以上是关于我们如何在颤动中拖动小部件时实现定位线?的主要内容,如果未能解决你的问题,请参考以下文章

如何在颤动中加载主小部件之前显示加载小部件?

如何在颤动中制作音频修剪小部件

在颤动中单击按钮时添加小部件时处理动态复选框列表

如何在颤动的另一个有状态小部件中访问在一个有状态小部件中创建的对象

如何检查两个小部件是不是在颤动中重叠?

颤动中的移动小部件