Flutter2.2で自由配置ができるウィジェットを作る

前回は緑の矩形を出すウィジェットを作ることに成功しました。

今度はこのウィジェットをクリックしながら掴んで自由に移動させる方法を学んでみます。

理想的には自由に動かせて離したら自動でグリッドに吸着していい感じに位置補正してくれるものを作りたいです。

まずは動きとしてはクリックイベントの実装が必要になりそうですね。

ゴリ押し的に作る場合考え方的にはクリックされたときにウィジェットの情報を取得して、 現在の座標からマウスを動かした分の座標をリアルタイムにウィジェットの座標に適用して。

って感じでしょうか?

ググったらドラッグでアイコンを自由に移動させている方法があったので それを応用すればウィジェットもいい感じに動かせるのではないでしょうか。

グリッドに関してはOSSで用意されてるライブラリを使ったほうがよさそうな感じがするので後回しですね。

Draggableを使ってみる

調べているとどうやらウィジェットをドラッグするには「Draggable」という組み込みラクラスを使うようです。

とりあえず現状の矩形を動かせるようにしてみましょう。

// MyWindowのステートを管理するクラス
class _MyWindowState extends State<MyWindow> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Draggable(
        feedback: Container(
        width:200,
        height:100,
        color: Colors.green,
        ),
        child: Container(
        width:200,
        height:100,
        color: Colors.blue,
        ),
      ),
    );
  }
}

bodyをContainerからDraggableに変えました。 そしてその中に「feedback」「child」を定義しないとDraggableはエラーがでるので実装します。

とりあえず先程のコンテナをそのまま子として実装しました。

わかりやすいようにfeedbackとchildで色を変えてみました。

実際にコンパイルしてみるとまず青い矩形が表示されているので、 それをマウスで掴んで動かしてみると緑色の矩形が自由に動かせます。

しかしマウスのクリックを離すともとに戻ってしまいます。

理想の動きとしては掴むと元の矩形は消えて掴んでいるものだけ表示させて なおかつ離したときに離した位置においてほしいものです。

掴んで動かすと元の矩形を非表示にする方法

掴んだ後、青(childに指定したもの)が残っているということは ドラッグ中そのchildを非表示にすればよさそうです。

以下のような感じで定義するとドラッグ中のchildを設定できます。

// MyWindowのステートを管理するクラス
class _MyWindowState extends State<MyWindow> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Draggable(
        feedback: Container(
        width:200,
        height:100,
        // ここもいい感じに変更
        color: Colors.blue[100],
        ),
        child: Container(
        width:200,
        height:100,
        color: Colors.blue,
        ),
        // 追加箇所
        childWhenDragging: Container(),
      ),
    );
  }
}

掴んでるときはコンテナを空にするという感じですね。

そしてfeedbackのカラーを緑から青に統一して、 その後にカラープロパティの後ろにブラケットをつけると透明度を指定することができます。

color: Colors.blue[100]

これで掴んで動かすと元の矩形は消えて掴んでいる矩形は薄くなって 掴んでいる状態というイメージをユーザーに伝わりやすくなります。

後は動かした位置に座標を変更したいですね。

しかしコンテナにはxやy、leftやTopなんかといった座標を指定するようなプロパティはない感じがします。

Positionedで絶対配置を行う

Positionedウィジェットを使うと自由にウィジェットを配置できるようになる。

おそらく親を起点としてどれだけ左上離れているかを設定するものだと思います。

CSSでいうposition:absoluteみたいなイメージですね。

VSCodeだとDraggableのところにカーソルを合わせると 左側に電球マークが出てくるのでそれを押すと「Wrap with Widget...」というのがあるので それを押すと今あるものをWidgetで囲ってくれます。

名前がWidgetのままだとダメなので「Stack」に名前を変えます。

次に直下のDraggableがchildになっているのにchildrenにしてあげないといけません。

これもchildをクリックすると電球マークがでてfixできるので 「Convert to Children」を選ぶといい感じに直してくれます。

「Change」ではなく「Convert」を選ぶのに注意しましょう。

Childrenの中身はWidgetリストなので型を明示的に記述します。

今の所はこんな感じのコードになっています。

// MyWindowのステートを管理するクラス
class _MyWindowState extends State<MyWindow> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>
          [Draggable(
            feedback: Container(
            width:200,
            height:100,
            color: Colors.blue[100],
            ),
            child: Container(
            width:200,
            height:100,
            color: Colors.blue,
            ),
            childWhenDragging: Container(),
          ),
        ],
      ),
    );
  }
}

次にPositionedウィジェットを作ってその中のchildに 元々あったDraggableウィジェットを入れる感じにコードを変えます。

次に座標を記憶するための変数をクラス変数として用意します。

クラス変数はクラスを定義した直下で定義することができます。

その後、Positionedのtopとleftにそれぞれ変数をあてがいましょう。

ここまでのコードはこんな感じになります。

// MyWindowのステートを管理するクラス
class _MyWindowState extends State<MyWindow> {

  Offset pos = Offset(50,50);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Positioned(
            left: pos.dx,
            top: pos.dy,
            child: Draggable(
              feedback: Container(
              width:200,
              height:100,
              color: Colors.blue[100],
              ),
              child: Container(
              width:200,
              height:100,
              color: Colors.blue,
              ),
              childWhenDragging: Container(),
            ),
          ),
        ],
      ),
    );
  }
}

これで読み込み直すと上左が端っこから50pxズレた位置に表示されるはずです。

変数はOffset型で作って置くとdxとdyの変数が用意されているので活用しましょう。

そしてその変数をpositionedのleftとtopにそれぞれ渡しています。

後はドラッグしてマウスクリックを離したときに変数を更新すれば 座標が更新されて自由にウィジェットを配置出来るような気がします。

ドラッグ中にマウスクリックを離したときに発動するイベントが

「onDraggableCanceled」というものが用意されているので その中で変数を更新することで自由にドラッグで移動できるようになります。

最終的にはこんなコードになります。

// MyWindowのステートを管理するクラス
class _MyWindowState extends State<MyWindow> {
  Offset pos = Offset(50, 50);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Positioned(
            left: pos.dx,
            top: pos.dy,
            child: Draggable(
              feedback: Container(
                width: 200,
                height: 100,
                color: Colors.blue[100],
              ),
              child: Container(
                width: 200,
                height: 100,
                color: Colors.blue,
              ),
              childWhenDragging: Container(),
              // 追加部分
              onDraggableCanceled: (view, offset) {
                setState(() {
                  pos = offset;
                });
              },
            ),
          ),
        ],
      ),
    );
  }
}

onDraggableCanceledは引数にviewとoffsetを渡す必要があります。

そしてその中でsetState関数を呼び出してクラス変数を更新します。

引数にoffsetを受け取っているのでクラス変数で用意したoffset型にそのまま渡せば 離したときにその座標を渡しウィジェットの位置を更新してくれて 自由にウィジェットを移動させるという仕組みが完成します。

理想としてはグリッドに吸着する仕組みのON/OFFがほしいですが、 先にグリッドの仕組みを実装する必要があるのでまた実装できたら記事にしようと思います。

ウィジェットを自由配置にするためのまとめ

今回覚えたのはDraggableとPositionedウィジェットですね。

DraggableをPositionedの子ウィジェットにして 自由配置を出来るようにして座標を記録するための変数を作り onDraggableCanceledで離したときに変数を更新すればOKです。

このようにウィジェットでウィジェットをラップすることで そのウィジェットが持たない機能をカバーすることができるので この仕組を知っていれば色々と応用が効きそうですね。

後はウィジェットがどのようなイベントを持っているかを理解する必要があるので とりあえずやりたいなと思ったことを片っ端から調べて そのウィジェットについて知識と経験を深めていく地道な修行が必要そうです。