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

# やりたいこと

![final.gif](https://qiita-image-store.s3.amazonaws.com/0/108030/2560fae3-d642-bd7b-d4a4-d57a9f82e47b.gif)

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

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

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

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


# ステップ 1

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

```kotlin
    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 に乗っけてやるとこれでオッケーです。

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

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

![reorder.gif](https://qiita-image-store.s3.amazonaws.com/0/108030/1d235dd5-449e-4401-ed37-665f194afa41.gif)

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

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

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

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

```kotlin
    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](https://qiita-image-store.s3.amazonaws.com/0/108030/0159d7f7-2fe0-c087-5f2a-7babadd0bed5.gif)

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

# ステップ 3

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

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

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

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

```kotlin
    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](https://qiita-image-store.s3.amazonaws.com/0/108030/e005338a-093e-95c6-a045-9ad29c9918fe.gif)

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

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

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