【Kotlin/Android Studio】ItemTouchHelperの使い方!RecyclerViewでスワイプ処理を実装する方法

この記事からわかること

  • Android Studio/KotlinRecyclerViewスワイプ処理実装する方法
  • ItemTouchHelper.SimpleCallback使い方
  • スワイプ背景色アイコン設定
  • 右スワイプ左スワイプ処理切り分ける方法
  • スワイプしたUI残る場合の解消
  • スワイプ時に文字列表示させる

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献:公式リファレンス:ItemTouchHelper.SimpleCallback

環境

RecyclerViewでスワイプ処理

AndroidのRecyclerViewではリストアイテムをスワイプした際にアイテムを削除したりといったアクションを追加することが可能です。

【Kotlin/Android Studio】ItemTouchHelperの使い方!RecyclerViewでスワイプ処理を実装する方法

それを実現させるのがItemTouchHelper.SimpleCallbackクラスです。このクラスはリストアイテムのドラッグやスワイプといった仕組みを提供しています。各アクションが実行された際には適当なコールバックメソッドが呼び出されるようになっており、任意の処理を実行させることが可能になっています。


abstract class ItemTouchHelper.SimpleCallback : ItemTouchHelper.Callback

実装の流れ

実装方法は少しややこしいので流れと方法を1つずつ見ながら実装していきたいと思います。とりあえずスワイプ時に削除する処理が走るようにしていきます。

  1. ItemTouchHelperを継承したクラスの作成
  2. スワイプの方向を指定
  3. コールバックメソッド内を実装
  4. 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 サポートしない(厳密にはアイドル状態を示すフラグ)

その他の状態を示すフラグや詳細な内容は公式サイトを参考にしてください。

公式リファレンス:ItemTouchHelper

コールバックメソッド内を実装

スワイプ方向の指定ができたらスワイプ時に実行させたい処理を記述します。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の引数viewHolderadapterPositionからスワイプされたリストアイテムインデックスを参照できます。少し順番が逆になりますが次でアダプター側(今回は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(赤い背景色)が残ったままになってしまいました。

【Kotlin/Android Studio】ItemTouchHelperの使い方!RecyclerViewでスワイプ処理を実装する方法

その場合はキャンセル時にアイテムを更新し、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)
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index