🚚

【Flutter】Draggableでドラッグ中のオブジェクトを範囲内で制御したい

2023/11/21に公開

経緯

Draggable Widgetをドラッグすると自由にWindow内を動かせるが、特定の範囲外(下記緑範囲)に出ないように制御したかった。

※今回の手法以外に良い方法があれば、コメントでご教授頂きたいです。

Draggableの挙動

実行例

赤色のブロックを緑色の範囲内で動かしたいが、範囲を出てしまう。

赤色ブロック
final double _moveBlockSize = 100;

Draggable(
  childWhenDragging: Container(),
  feedback: Container(
    color: Colors.red,
    height: _moveBlockSize,
    width: _moveBlockSize0,
  ),
  child: Container(
    color: Colors.red,
    height: _moveBlockSize,
    width: _moveBlockSize,
  ),
),

※childWhenDraggingはドラッグ中のchildの状態

Draggable Widget と Show Widgetに分ける

Draggable Widgetは透過し、表示するのはShow Widgetとする。
Draggable Widgetの位置情報をonDragUpdateで取得し、それをShow Widgetの位置とすることで擬似的にDraggable Widgetを動かす。
これで赤色のブロックの位置を数値で扱える。

位置情報を取得、更新
Offset _moveBlockOffset = Offset.zero;// 赤色ブロックの位置

Stack(
  children: [
    // Show Widget
    Positioned(
      left: _moveBlockOffset.dx,
      top: _moveBlockOffset.dy,
      child: Container(
        color: Colors.red,
        height: _moveBlockSize,
        width: _moveBlockSize,
      ),
    ),
    // Draggable Widget
    Positioned(
      left: _moveBlockOffset.dx,
      top: _moveBlockOffset.dy,
      child: Draggable(
        onDragUpdate: (details) {
          setState(() {
            _moveBlockOffset = Offset(
              _moveBlockOffset.dx + details.delta.dx,
              _moveBlockOffset.dy + details.delta.dy,
            );
          });
        },
        childWhenDragging: Container(),
        feedback: Container(),
        child: Container(
          color: Colors.transparent,// draggableは透明にする
          height: _moveBlockSize,
          width: _moveBlockSize,
        ),
      ),
    ),
  ],
),

位置情報に制約を加える

あとは、位置情報(数値)を特定の範囲内とするように制限する。
表示されているダミーはこの範囲内を動く。
※clamp(5, 10)は5~10の値に制限する。3->5, 15->10

制約メソッド
final Size _moveRange = const Size(500, 500);// 移動範囲

void clampMoveBlockOffset() {
  double clampedX = _moveBlockOffset.dx.clamp(0, _moveRange.width - _moveBlockSize);
  double clampedY = _moveBlockOffset.dy.clamp(0, _moveRange.height - _moveBlockSize);
  _moveBlockOffset = Offset(clampedX, clampedY);
}

https://api.dart.dev/stable/3.2.0/dart-core/num/clamp.html

コードサンプル

実行例

コード全文
import 'package:flutter/material.dart';

class DraggableRange extends StatefulWidget {
  const DraggableRange({
    super.key,
  });
  
  State<DraggableRange> createState() => _DraggableRangeState();
}

class _DraggableRangeState extends State<DraggableRange> {
  final double _moveBlockSize = 100;
  final Size _moveRange = const Size(500, 500);
  Offset _moveBlockOffset = Offset.zero;

  void clampMoveBlockOffset() {
    double clampedX = _moveBlockOffset.dx.clamp(0, _moveRange.width - _moveBlockSize);
    double clampedY = _moveBlockOffset.dy.clamp(0, _moveRange.height - _moveBlockSize);
    _moveBlockOffset = Offset(clampedX, clampedY);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: _moveRange.width,
        height: _moveRange.height,
        color: Colors.green,
        margin: const EdgeInsets.only(top: 100, left: 100),
        child: Stack(
          children: [
            Positioned(
              left: _moveBlockOffset.dx,
              top: _moveBlockOffset.dy,
              child: Container(
                color: Colors.red,
                height: _moveBlockSize,
                width: _moveBlockSize,
              ),
            ),
            Positioned(
              left: _moveBlockOffset.dx,
              top: _moveBlockOffset.dy,
              child: Draggable(
                onDragUpdate: (details) {
                  setState(() {
                    _moveBlockOffset = Offset(
                      _moveBlockOffset.dx + details.delta.dx,
                      _moveBlockOffset.dy + details.delta.dy,
                    );
                    clampMoveBlockOffset();
                  });
                },
                childWhenDragging: Container(),
                feedback: Container(),
                child: Container(
                  color: Colors.transparent,
                  height: _moveBlockSize,
                  width: _moveBlockSize,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Discussion