2021年1月25日月曜日

Androidでパズドラ風な操作を実装する

Androidでパズドラ風のドラッグ操作を作る機会があったのでjavaでのソースコードを残しておく
Kotlinも書こうかと思ったけど、コピペしてAndroidStudioに貼ったら自動変換されるので良しとする
こういう動き


設計

※以降の説明で図a,図b,図c,図dって単語はこの画像の図番号を示す。


レイアウト

Viewを並べられればなんでもいいですけど、とりあえず4×4のGridLayout
画像は適当に標準で存在する画像を利用
レイアウトは記事のメインではないので、コードはURLだけで(ブログが冗長になるからね)


ドラッグ&ドロップの実装

まずは公式「ドラッグ&ドロップ」で知識を身に着ける
ただし、ここに書かれているドラッグを開始するView#startDragはAPI24から非推奨なのでView#startDragAndDropメソッドを使用すべき。
ドラッグされたViewの動作イベントはView.OnDragListenerでイベントで拾える。
ドラッグ契機となるのはImageViewのタッチなのでView.OnTouchListenerも実装。
よって、全体像とてはこんな感じ
public class MainActivity extends AppCompatActivity 
  implements View.OnTouchListener, View.OnDragListener {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // TODO:後で解説
  }

  @Override
  public boolean onTouch(View v, MotionEvent motionEvent) {
    // TODO:後で解説
    return true;
  }

  @Override
  public boolean onDrag(View v, DragEvent event) {
     // TODO:後で解説
     return true;
  }
}


OnCreateの実装

Viewのリスナーをここで設定しておく。
注意が必要なのはOnDragListener登録は全部のImageViewに仕込む必要がある点。
図cのように、ドラッグ状態のViewの上を通過契機でViewの位置入れ替えを行いたいのだが、ドラッグ状態のViewにのみOnDragListenerを設定しても、他のViewの上を通過したイベントは発火しない。これは、通過される側のViewのイベントであるため、通過されるView側にもリスナーを仕込む必要があった。
よって、実装は全部のViewにOnDragListenerを仕込む。
    @Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // 各Viewに、タッチ・ドラッグのイベントを設定していく
  GridLayout parent = findViewById(R.id.grid_layout);
  for (int i = 0; i < parent.getChildCount(); i++) {
    View v = parent.getChildAt(i);
    v.setOnTouchListener(this);
    v.setOnDragListener(this);
  }
}

OnTouchの実装

タッチされたときの動作として、触ったViewをドラッグ状態にする。
ドラッグ状態すると、右図のようにドラッグ中Viewが薄く表示、元のViewはそのまま表示される。⇒

仕様では、図bのように元のViewは非表示にする必要があるので、元のView側の透過値を0にして見えなくする。
ここで表示可否のVisibilityを用いてView.INVISIBLEやView.GONEで消すと、見えなくできるがドロップ時にドロップに「失敗」する。このとき、元の位置に戻るアニメーションが入り動作が不自然になる。以上の理由から透過で消す方法で実現させる
/** ドラッグしたViewはメンバ変数保持(ドラッグ処理で使用) */
private View mDragView;
@Override
public boolean onTouch(View v, MotionEvent motionEvent) {
  // 押下時に動作
  if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
    mDragView = v;

    // Viewをドラッグ状態にする。
    v.startDragAndDrop(null, new View.DragShadowBuilder(v), v, 0);
    v.setAlpha(0);
  }
  return true;
}

onDragの実装

ドラッグイベントは6種類あるが、使用するのは以下の2つ
  • 手を放し、ドラッグが終了したイベント(ACTION_DRAG_ENDED)
  • ドラッグ中に他のViewの上に乗ったイベント(ACTION_DRAG_LOCATION)
他のイベントは公式参照
@Override
public boolean onDrag(View v, DragEvent event) {
  switch (event.getAction()) {
    // 手を放し、ドラッグが終了した時の処理
    // ドラッグしているViewを表示させる。
    case DragEvent.ACTION_DRAG_ENDED:
      getMainExecutor().execute(() -> mDragView.setAlpha(1));
      break;

    // ドラッグ中他のViewの上に乗る時の処理
    // Viewの位置を入れ替える
    case DragEvent.ACTION_DRAG_LOCATION:
      getMainExecutor().execute(() -> swap(v, mDragView));
      break;
  }
  return true;
}
Viewの位置の入れ替え(swap)は、
LayoutParamを入れ替えればよいので以下のようになる。
private void swap(View v1, View v2) {
  // 同じViewなら入れ替える必要なし
  if (v1 == v2) return;

  GridLayout parent = findViewById(R.id.grid_layout);

  // レイアウトパラメータを抜き出して、入れ替えを行う
  GridLayout.LayoutParams p1, p2;
  p1 = (GridLayout.LayoutParams) v1.getLayoutParams();
  p2 =  (GridLayout.LayoutParams) v2.getLayoutParams();
  parent.removeView(v1);
  parent.removeView(v2);
  parent.addView(v1, p2);
  parent.addView(v2, p1);
}
     

完成!


最後に

最終的に、メンバ変数とかコードがバラバラになってるので、GitHubにまとめておく

0 件のコメント:

コメントを投稿