Flutter2.2でグリッドスナップを実装する

ウィジェットの自由配置とグリッドの表示非表示まで実装できました。

続いてはグリッドの吸着をしたいのですが、どうやるんだろうと考えてました。

ペイントソフトなんかじゃ気軽に使っているけど 実際に作ろうとしたら結構面倒くさいですね。

グリッド吸着の基本

そもそもどうやって吸着しているかを考えてみましょう。

グリッドの描写と吸着については別機能で作っているのは確定ですね。

描写は単なる矩形の描写。

吸着は見えない判定といった感じでしょうか。

例えば16x16のグリッドが敷き詰められていた場合、 ウィジェットを掴んで離したときの左上座標とグリッドの1つの四角の左上との差が8以下だったら その四角の左上にウィジェットの位置をずらして適用といった感じですね。

グリッドのサイズを参照できるようにしとかないとダメ

今回はMVVMで作っているためグリッドのサイズはViewModelを参照すれば一発なので ユーザー側でグリッドのサイズの変更が容易になりますね。

グリッド間隔と吸着計算するためのサイズは統一する必要があるので 用意するのはOffset型の変数1つか2つで済みそうです。

動作はウィジェットを移動して完了する際に発動

現状ウィジェットを動かしたら座標を適用する処理だけになっているので その処理をしている手前でグリッドのどこにいるのかを調べる処理を書いてみます。

その前にまずはグリッドのサイズを設定してその値を返すgetを作りましょう。

  // グリッドのサイズ
  double _gridSize = 16.0;
  double get gridSize => _gridSize;
  set gridSize(double size) {
    _gridSize = size;
  }

グリッドサイズは一旦16x16にしました。 16も別ファイルでconst化しておくのが良いけど一旦リテラルでいきます。

縦横は同じサイズなので1つにしています。

グリッドサイズを規定のサイズ以外に変更する必要がなければconst化してもよさそうです。

これでグリッドのサイズの取得と変更がViewからできるようになりました。

ウィジェットの左上がどこのグリッドに存在しているか調べる

2Dゲームを作っていたときの知識が役立ちそうです。

16px間隔で2次元の線が引かれているので矩形の左上の座標を起点として どのグリッドに存在しているのかは簡単に割り出すことが出来ます。

「起点 / グリッドサイズ」でグリッドを点ではなくマス目座標で特定することができます。

例えばX130 y300で1つのグリッドサイズが16だった場合、 左から8個目、上から18個目のグリッドに矩形の左上が存在することがわかります。

もっと詳しく書くと……

X130を16で割ると8.125。

つまり左から8個目のグリッドのちょっとだけ右にずれた位置にいます。

Y300を16で割ると18.75。

つまり上から18個目のほぼ19個目のグリッド手前あたりにいます。

この場合グリッド吸着ルールとしては16x16だと間隔が結構狭いので グリッド内に左上の位置が入っていればそのグリッドの左上に吸着するようにすればよさそうです。

一旦確定で吸着する仕組みを設けてみましょう。

  // ウィンドウ自由配置用のセットメソッド
  void setWindowPosition(Offset vp) {
    // 矩形の左上がどこのグリッドにいるか調べる(小数点切り捨て)
    double gx = (vp.dx / gridSize).floor() as double;
    double gy = ((vp.dy - appBar.preferredSize.height) / gridSize).floor() as double;

    _pos = Offset(gx * gridSize, gy * gridSize);
    notifyListeners();
  }

このときなんで半分から先に行ったらグリッドの真ん中に吸着されるんだろうとおもってたのですが そう言えばグリッドサイズ32x32にしたの忘れてました。

ってことでグリッドサイズを32にしたらちゃんとグリッドの左上に吸着されるようになりました。

グリッド吸着、意外と簡単でしたね。

計算はひとまとめにしたほうがいいかもなので以下のようにしたほうがきれいですかね。

  // ウィンドウ自由配置用のセットメソッド
  void setWindowPosition(Offset vp) {
    // 矩形の左上がどこのグリッドにいるか調べる(小数点切り捨て)
    double gx = ((vp.dx / gridSize).floor()) * gridSize as double;
    double gy = (((vp.dy - appBar.preferredSize.height) / gridSize).floor()) * gridSize as double;

    _pos = Offset(gx, gy);
    notifyListeners();
  }

1行が長くなってちょっと見づらいですが、関数に値を渡すときはなるべくシンプルに渡したいのが理想です。

今回は短いですがここまで。