【Kotlin/Android Studio】ItemTouchHelperの使い方!RecyclerViewでスワイプ処理を実装する方法
この記事からわかること
- Android Studio/KotlinでRecyclerViewにスワイプ処理を実装する方法
- ItemTouchHelper.SimpleCallbackの使い方
- スワイプの背景色やアイコンの設定
- 右スワイプと左スワイプで処理を切り分ける方法
- スワイプしたUIが残る場合の解消法
- スワイプ時に文字列を表示させる
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
参考文献:公式リファレンス:ItemTouchHelper.SimpleCallback
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
RecyclerViewでスワイプ処理
AndroidのRecyclerViewではリストアイテムをスワイプした際にアイテムを削除したりといったアクションを追加することが可能です。
それを実現させるのがItemTouchHelper.SimpleCallback
クラスです。このクラスはリストアイテムのドラッグやスワイプといった仕組みを提供しています。各アクションが実行された際には適当なコールバックメソッドが呼び出されるようになっており、任意の処理を実行させることが可能になっています。
abstract class ItemTouchHelper.SimpleCallback : ItemTouchHelper.Callback
実装の流れ
実装方法は少しややこしいので流れと方法を1つずつ見ながら実装していきたいと思います。とりあえずスワイプ時に削除する処理が走るようにしていきます。
- ItemTouchHelperを継承したクラスの作成
- スワイプの方向を指定
- コールバックメソッド内を実装
- Adapter側にDeleteメソッドを用意
ItemTouchHelperを継承したクラスの作成
RecyclerViewにスワイプアクションを追加するにはItemTouchHelper.SimpleCallback
クラスを継承させます。クラス名は何でも良いですがここではSwipeToCallback
クラスを用意して引数として対象のRecyclerViewのAdapterを受け取れるようにしておきます。
class SwipeToCallback(private val adapter: MyListAdapter) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.ACTION_STATE_IDLE, // ドラッグの方向を指定
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // スワイプの方向を指定
) {
// 〜〜〜〜〜〜〜〜〜〜〜〜〜〜
}
スワイプの方向を指定
許可するスワイプの方向はItemTouchHelper.SimpleCallback
クラスの引数から指定します。1つ目の引数にはドラッグ方向を、2つ目の引数にはスワイプ方向を指定します。
ItemTouchHelper.SimpleCallback(
ItemTouchHelper.ACTION_STATE_IDLE, // サポートしない
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // 左と右のスワイプをサポート
)
指定するのは以下の項目です。また複数の値を指定したいときはor
を使用します。
指定値(フラグ) | 概要 |
---|---|
ItemTouchHelper.UP | 上方向 |
ItemTouchHelper.DOWN | 下方向 |
ItemTouchHelper.START | Viewの開始方向 |
ItemTouchHelper.END | Viewの終了方向 |
ItemTouchHelper.LEFT | 左方向 |
ItemTouchHelper.RIGHT | 右方向 |
ItemTouchHelper.ACTION_STATE_IDLE | サポートしない(厳密にはアイドル状態を示すフラグ) |
その他の状態を示すフラグや詳細な内容は公式サイトを参考にしてください。
コールバックメソッド内を実装
スワイプ方向の指定ができたらスワイプ時に実行させたい処理を記述します。onMove
メソッドはドラッグ時の処理なのでここでは何も記述しません。onSwiped
メソッド内に実行したい処理を記述します。
class SwipeToCallback(private val adapter: MyListAdapter) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.ACTION_STATE_IDLE, // ドラッグの方向を指定
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // スワイプの方向を指定
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
// ドラッグアンドドロップの処理を実装
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// スワイプしたアイテムを削除する処理を実装
val position = viewHolder.adapterPosition
adapter.deleteItem(position)
}
}
onSwiped
の引数viewHolder
のadapterPosition
からスワイプされたリストアイテムインデックスを参照できます。少し順番が逆になりますが次でアダプター側(今回はMyListAdapter)のdeleteItem
メソッドを用意します。
Adapter側にDeleteメソッドを用意
Adapterクラスに先ほど呼び出していた削除するためのメソッドを用意します。ここでは引数として受け取ったインデックス番号を元にデータソースの削除とRecyclerViewの見た目上の削除(notifyItemRemoved
)を行なっています。
fun deleteItem(position: Int) {
if (position < 0 || position >= _userList.size) {
return
}
_userList.removeAt(position)
notifyItemRemoved(position)
}
RecyclerViewとの紐付け
RecyclerView
と紐づけるためには以下のように記述します。
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(view.context)
recyclerView.addItemDecoration(
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
)
val adapter = MyListAdapter(data)
val swipeToCallback = SwipeToCallback(adapter)
val itemTouchHelper = ItemTouchHelper(swipeToCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
recyclerView.adapter = adapter
これでスワイプ時にデータを削除するような処理の実装が完了しました。この状態ではスワイプ時の背景色などを設定していないためスワイプともにそのままデータが消えていく感じなります。
スワイプ時の背景色やアイコンを設置する
スワイプ時の背景色を変更したり、アイコンを設置するにはonChildDraw
メソッドをオーバーライドして記述します。
// スワイプ時の背景色とアイコンを描画
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
// 背景色を定義
val background: ColorDrawable = ColorDrawable(Color.RED)
// スワイプ時のアイコンを定義AppCompatResources
val deleteIcon: Drawable? = AppCompatResources.getDrawable(recyclerView.context,R.drawable.delete)
if (deleteIcon != null) {
// 余白調整
val itemView = viewHolder.itemView
val iconMargin = (itemView.height - deleteIcon.intrinsicHeight) / 2
val iconTop = itemView.top + (itemView.height - deleteIcon.intrinsicHeight) / 2
val iconBottom = iconTop + deleteIcon.intrinsicHeight
when {
dX > 0 -> { // 右方向へのスワイプ
val iconLeft = itemView.left + iconMargin
val iconRight = itemView.left + iconMargin + deleteIcon.intrinsicWidth
deleteIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
background.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
}
dX < 0 -> { // 左方向へのスワイプ
val iconLeft = itemView.right - iconMargin - deleteIcon.intrinsicWidth
val iconRight = itemView.right - iconMargin
deleteIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
}
else -> {
background.setBounds(0, 0, 0, 0)
}
}
// 背景の描画
background.draw(canvas)
// アイコンの描画
deleteIcon.draw(canvas)
}
}
スワイプ時に文字を表示させる
スワイプ時にアイコンではなく文字列を表示させるにはcanvas.drawText
メソッドを使用します。
dX > 0 -> { // 右方向へのスワイプ
background.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
background.draw(canvas)
val text = "右スワイプ"
val paint = Paint()
paint.color = Color.WHITE
paint.textSize = 40f
val textLeft = itemView.left + iconMargin
val centerY = itemView.top + itemView.height / 2
val textHeight = (paint.descent() + paint.ascent()) / 2
val textY = centerY - textHeight
canvas.drawText(text, textLeft.toFloat(), textY, paint)
}
右スワイプと左スワイプで処理を切り分ける
右スワイプと左スワイプで処理を切り分けたい場合はonSwiped
メソッドのdirection
の値から識別して分岐させることができます。
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (direction == ItemTouchHelper.LEFT) {
// 左スワイプ時に実行したい処理
} else if (direction == ItemTouchHelper.RIGHT) {
// 右スワイプ時に実行したい処理
}
}
スワイプしたUIが残る場合の解消法
スワイプ時に削除処理でなくアラートダイアログを挟んでから削除するように実装した場合、「キャンセル」をクリックしてもスワイプ時のUI(赤い背景色)が残ったままになってしまいました。
その場合はキャンセル時にアイテムを更新し、RecyclerViewに通知することで解消することができました。そのためにはonSwiped
メソッド内では以下のように記述します。ここでは呼び出すだけで肝心のアイテムを更新し、RecyclerViewに通知部分はAdapter側に実装します。
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(context)
.setTitle("削除しますか?")
.setPositiveButton("OK", { dialog, which ->
/// スワイプしたアイテムを削除する処理を実装
val position = viewHolder.adapterPosition
adapter.deleteItem(position)
})
.setNegativeButton("キャンセル", { dialog, which ->
// キャンセルされた場合はRecyclerViewを更新
val position = viewHolder.adapterPosition
adapter.updateItem(position)
})
.show()
}
Adapterでは以下のように対象行のアイテムを現状のデータのまま再度更新処理をかけてnotifyItemChanged
メソッド(変更されたことを通知して再描画する)を実行します。処理的には意味のないことですがこれでスワイプ時のUIをリセットすることができました。
public fun updateItem(position: Int) {
if (position < 0 || position >= _borrowerList.size) {
return
}
val item = _borrowerList[position]
viewModel.updateBorrower(item.id,item.name)
notifyItemChanged(position)
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。