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も実装。
よって、全体像とてはこんな感じ
  1. public class MainActivity extends AppCompatActivity
  2. implements View.OnTouchListener, View.OnDragListener {
  3. @Override
  4. public void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. // TODO:後で解説
  8. }
  9.  
  10. @Override
  11. public boolean onTouch(View v, MotionEvent motionEvent) {
  12. // TODO:後で解説
  13. return true;
  14. }
  15.  
  16. @Override
  17. public boolean onDrag(View v, DragEvent event) {
  18. // TODO:後で解説
  19. return true;
  20. }
  21. }


OnCreateの実装

Viewのリスナーをここで設定しておく。
注意が必要なのはOnDragListener登録は全部のImageViewに仕込む必要がある点。
図cのように、ドラッグ状態のViewの上を通過契機でViewの位置入れ替えを行いたいのだが、ドラッグ状態のViewにのみOnDragListenerを設定しても、他のViewの上を通過したイベントは発火しない。これは、通過される側のViewのイベントであるため、通過されるView側にもリスナーを仕込む必要があった。
よって、実装は全部のViewにOnDragListenerを仕込む。
  1. @Override
  2. public void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5.  
  6. // 各Viewに、タッチ・ドラッグのイベントを設定していく
  7. GridLayout parent = findViewById(R.id.grid_layout);
  8. for (int i = 0; i < parent.getChildCount(); i++) {
  9. View v = parent.getChildAt(i);
  10. v.setOnTouchListener(this);
  11. v.setOnDragListener(this);
  12. }
  13. }

OnTouchの実装

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

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

onDragの実装

ドラッグイベントは6種類あるが、使用するのは以下の2つ
  • 手を放し、ドラッグが終了したイベント(ACTION_DRAG_ENDED)
  • ドラッグ中に他のViewの上に乗ったイベント(ACTION_DRAG_LOCATION)
他のイベントは公式参照
  1. @Override
  2. public boolean onDrag(View v, DragEvent event) {
  3. switch (event.getAction()) {
  4. // 手を放し、ドラッグが終了した時の処理
  5. // ドラッグしているViewを表示させる。
  6. case DragEvent.ACTION_DRAG_ENDED:
  7. getMainExecutor().execute(() -> mDragView.setAlpha(1));
  8. break;
  9.  
  10. // ドラッグ中他のViewの上に乗る時の処理
  11. // Viewの位置を入れ替える
  12. case DragEvent.ACTION_DRAG_LOCATION:
  13. getMainExecutor().execute(() -> swap(v, mDragView));
  14. break;
  15. }
  16. return true;
  17. }
Viewの位置の入れ替え(swap)は、
LayoutParamを入れ替えればよいので以下のようになる。
  1. private void swap(View v1, View v2) {
  2. // 同じViewなら入れ替える必要なし
  3. if (v1 == v2) return;
  4.  
  5. GridLayout parent = findViewById(R.id.grid_layout);
  6.  
  7. // レイアウトパラメータを抜き出して、入れ替えを行う
  8. GridLayout.LayoutParams p1, p2;
  9. p1 = (GridLayout.LayoutParams) v1.getLayoutParams();
  10. p2 = (GridLayout.LayoutParams) v2.getLayoutParams();
  11. parent.removeView(v1);
  12. parent.removeView(v2);
  13. parent.addView(v1, p2);
  14. parent.addView(v2, p1);
  15. }

完成!


最後に

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

0 件のコメント:

コメントを投稿