RecyclerViewにドラッグドロップで並べ替え機能を追加する

あんまり使うことがなくて、使おうとする度にどうやるんだっけ？と調べるやつ

RecyclerViewはその内部でドラッグドロップで並べ替えをする機能を実装するのは非常に簡単です。

まずは、シンプルなリストを作ってみましょう

```kotlin
private class ItemViewHolder(val binding: ItemViewBinding) : ViewHolder(binding.root)
private class ItemAdapter(context: Context) :
    ListAdapter<Int, ItemViewHolder>(object : DiffUtil.ItemCallback<Int>() {
        override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean = oldItem == newItem
        override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean =
            oldItem == newItem
    }) {
    private val inflater = LayoutInflater.from(context)
    private val list: MutableList<Int> = MutableList(10) { it + 1 }

    init {
        submitList(list.toList())
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder =
        ItemViewHolder(ItemViewBinding.inflate(inflater, parent, false))

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.binding.text.text = "item: " + getItem(position).toString()
    }
}
```

レイアウトとかもシンプルなものなので省略、これで以下のようなリストを作ることができました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/9fab33d4-8f77-1aaa-2d4c-ca5d5c4bf90e.png" width="400">

次に、上記Adapterに項目の入れ替え、削除を行うメソッドを追加します。Adapterに実装する必要はないですが、あくまで説明用のサンプルとしてです。

```kotlin
fun swap(from: Int, to: Int) {
    list.add(to, list.removeAt(from))
    submitList(list.toList())
}

fun remove(target: Int) {
    list.removeAt(target)
    submitList(list.toList())
}
```

LinearLayoutManagerで縦一列に並べている場合、通常onMoveは前後に動く度にコールされるので単純にfrom/toでswapしても同じですが、単純にfrom/toを入れ替えるのではなく、fromからtoの間を順次移動させてtoにfromの項目を入れる用にしています。

上記メソッドを呼び出すようにItemTouchHelperを実装します。

```kotlin
val helper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    ItemTouchHelper.START or ItemTouchHelper.END
) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean {
        adapter.swap(viewHolder.adapterPosition, target.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        adapter.remove(viewHolder.adapterPosition)
    }
})
helper.attachToRecyclerView(binding.recyclerView)
```

第一引数がドラッグの方向で、第二引数がスワイプ方向ですね。
ドラッグ（onMove）が発生したときはtargetと位置を入れ替える操作を、スワイプ(onSwipe）が発生したときは削除の動作をそれぞれつないでいるわけです。

こうすると、以下のようにドラッグドロップで並べ替え、スワイプで削除が実装できます

|drag&drop|swipe|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/b5b9ba7d-b6ca-fc76-a482-30d338a9bc4c.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/5d1045fd-2ff3-b509-23d9-a6a9dd32a396.gif)|

超簡単です。

## GridLayoutManager

グリッドレイアウトの場合、ドラッグ方向は上下左右になりますね。

```kotlin
val helper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN or
    ItemTouchHelper.START or ItemTouchHelper.END, 0
) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean {
        adapter.swap(viewHolder.adapterPosition, target.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        adapter.remove(viewHolder.adapterPosition)
    }
})
helper.attachToRecyclerView(binding.recyclerView)
```

また、並べ替えのロジックによって振る舞いが結構違います。
前項目と同様に、from/toの間を詰めて移動させる場合

```kotlin:1
fun swap(from: Int, to: Int) {
    list.add(to, list.removeAt(from))
    submitList(list.toList())
}
```

from/toを単純に入れ替える場合

```kotlin:2
fun swap(from: Int, to: Int) {
    Collections.swap(list, from, to)
    submitList(list.toList())
}
```

それぞれ、以下のような動作になります。どちらがいいか、また別のロジックが良いかはアプリの特性によって決めれば良いと思います。

|1|2|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/f42bf63c-cb46-4083-1a55-341345b215c5.gif)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d3bb0bb3-fe31-2e7d-296b-71f4eeba3ee7.gif)|

以上です。