32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

アンドロイドで RecyclerView のアイテムを「優しく」ドラッグ操作する

Last updated at Posted at 2018-12-05

やりたいこと

final.gif

上の図のように、RecyclerView 上のアイテムを

  • ViewHolder の右側に配置されたハンドルをドラッグして位置変更
  • ViewHolder 自体を長押しからドラッグして位置変更

できるようにしたいです。当然、このドラッグ実装が RecyclerView のスクロール自体を邪魔してはいけません。

以下、サンプルコードは全て Kotlin です。

ステップ 1

まず、教科書通りに ItemTouchHelper を実装します。

    private val itemTouchHelper by lazy {
        // 1. drag 方向の引数に上下左右全て指定している。左右も指定した方が自然なドラッグを実現できる。
        val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) {

            override fun onMove(recyclerView: RecyclerView,
                                viewHolder: RecyclerView.ViewHolder,
                                target: RecyclerView.ViewHolder): Boolean {
                val adapter = recyclerView.adapter as MainRecyclerViewAdapter
                val from = viewHolder.adapterPosition
                val to = target.adapterPosition
                // 2. モデルの変更。 MainRecyclerViewAdapter でのカスタム実装。
                adapter.moveItem(from, to)
                // 3. Adapter に変更を通知。これを呼ばないと、Drop が完了しない。
                adapter.notifyItemMoved(from, to)

                return true
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                // 4. 横方向の swipe 用のコードブロック。ここでは無視。
            }
        }

        ItemTouchHelper(simpleItemTouchCallback)
    }

で、この ItemTouchHelper インスタンスを RecyclerView に乗っけてやるとこれでオッケーです。

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        itemTouchHelper.attachToRecyclerView(recyclerView)
    }

成果物は次のようになります。

reorder.gif

行を長押しすると行が選択され、行をドラッグできるようになります!

ただし、以下のような問題があります。

  • 行長押しで移動ができる、という事実があまり直感的でない
  • __長押ししてもフィードバックがない__ので、いつドラッグできていつドラッグできないのかがよくわからない

ステップ 2

「長押ししてもフィードバックがない」という問題は、行が選択されている間、行をハイライトすることで簡単に解決できます。ハイライトの仕方は色々ありますが、今回は半透明にすることにします。

    private val itemTouchHelper by lazy {
        val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) {
            ...
            // 1. 行が選択された時に、このコールバックが呼ばれる。ここで行をハイライトする。
            override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
                super.onSelectedChanged(viewHolder, actionState)

                if (actionState == ACTION_STATE_DRAG) {
                    viewHolder?.itemView?.alpha = 0.5f
                }
            }

            // 2. 行が選択解除された時 (ドロップされた時) このコールバックが呼ばれる。ハイライトを解除する。
            override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
                super.clearView(recyclerView, viewHolder)

                viewHolder?.itemView?.alpha = 1.0f
            }
        }
        ...
    }

これだけです。

highlight.gif

だいぶ見やすくなりました。ただ、まだ__「行長押しで移動できる、という事実があまり直感的でない」__という問題が解決されておらず、また長押しでいつも数秒待たされるのがストレスです。

ステップ 3

上記の問題を解決するために、行の右側にハンドルを配置し、ハンドルをタッチしたらすぐに行が選択されるようにします。これ自体もかなり簡単にできます。

Activity 側で、itemTouchHelper.startDrag(...) メソッドを呼べる準備をしておきます。これが呼ばれると、ViewHolder インスタンスがドラッグ用の選択状態になります。

    fun startDragging(viewHolder: RecyclerView.ViewHolder) {
        itemTouchHelper.startDrag(viewHolder)
    }

ハンドルアイコンを RecyclerView 用の xml レイアウトに配置して、handleView という id をつけたとします。

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder {
        ...
        // 1. handleView に `OnTouchListener` を実装。
        viewHolder.itemView.handleView.setOnTouchListener { view, event ->
            if (event.actionMasked == MotionEvent.ACTION_DOWN) {
                // 2. タッチダウンを検出したら、先ほど用意した `startDragging(...)` を呼びます。
                activity.startDragging(viewHolder)
            }
            return@setOnTouchListener true
        }
        ...
    }

これだけです。

handle.gif

ハンドルをタッチすればすぐに行の移動ができますし、ハンドル以外のところでは長押しによる移動も可能です。

まとめ

教科書通りに ItemTouchHelper を実装するだけでは UX 的に優しくないところを、少し工夫を加えてストレスのない挙動にすることができます。

ソースコードはここ にあります。git タグ (step1, step2, step3) がこの記事の (ステップ 1, ステップ 2, ステップ 3) に対応しているので、各ステップの実装を見たければどうぞ。

32
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?