やりたいこと
上の図のように、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)
}
成果物は次のようになります。
行を長押しすると行が選択され、行をドラッグできるようになります!
ただし、以下のような問題があります。
- 行長押しで移動ができる、という事実があまり直感的でない
- __長押ししてもフィードバックがない__ので、いつドラッグできていつドラッグできないのかがよくわからない
ステップ 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
}
}
...
}
これだけです。
だいぶ見やすくなりました。ただ、まだ__「行長押しで移動できる、という事実があまり直感的でない」__という問題が解決されておらず、また長押しでいつも数秒待たされるのがストレスです。
ステップ 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
}
...
}
これだけです。
ハンドルをタッチすればすぐに行の移動ができますし、ハンドル以外のところでは長押しによる移動も可能です。
まとめ
教科書通りに ItemTouchHelper
を実装するだけでは UX 的に優しくないところを、少し工夫を加えてストレスのない挙動にすることができます。
ソースコードはここ にあります。git タグ (step1, step2, step3) がこの記事の (ステップ 1, ステップ 2, ステップ 3) に対応しているので、各ステップの実装を見たければどうぞ。