駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒2年目駆け出しSEぱかぱかの備忘録です。

【Android】RecyclerViewのセルタップイベントを検知する

こんにちは!ぱかぱかです!
お客様都合で今日もお休みを頂いているのですが、これでついに13連休も幕を閉じます…寂しいですね…
正月休みは小・中・高の友達にそれぞれ会ったり美味しいものを食べたりアプリを作ったり実家の猫と戯れたり、なかなか充実した時間になりました。
仕事が始まってもメリハリを付けていい時間の使い方をしていきたいですね。

今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、RecyclerViewのセルをタップした際のイベントを検知する処理についてまとめていきたいと思います。

前回の記事

radish-se.hatenablog.com

内部ストレージに保存した画像をリスト上で表示できるようになった!

今回のゴール

行った店リスト(RecyclerView)のセルをタップした際にタップイベントを検知できるようにする。
次回以降でシングルタップ時に行った店の詳細画面に飛べるようにしたい。
ロングタップ時でメニュー表示も。

リスナーの実装

RecyclerViewのタップ処理実装方法は調べてみると色々なパターンがありそうでした。

実装方法の候補
・SimpleOnItemTouchListener
→単純なタップやロングタップに対応できる

・ItemTouchHelper
→ドラッグやスワイプに対応できる

・自前でリスナーのインターフェースを作りViewHolderに適用
→単純なタップ等

今回はSimpleOnItemTouchListenerを使ってみたいと思います。
今後はスワイプで削除とかもやりたいのでItemTouchHelperも使いたい…(実装した後に気づいた…)
両方併用することも可能そうなので今度試してみます。

SimpleOnItemTouchListenerの継承

SimpleOnItemTouchListenerというRecyclerViewのインターフェースを継承したリスナークラスを作成し、クリック処理を実装していきます。
RecyclerView.SimpleOnItemTouchListener  |  Android Developers

// SimpleOnItemTouchListenerを継承したリスナークラス
class RecyclerItemClickListener() : RecyclerView.SimpleOnItemTouchListener() {

}

onInterceptTouchEventの実装

onInterceptTouchEventはタップイベントを監視するメソッドです。
RecyclerView.SimpleOnItemTouchListener  |  Android Developers

実装することで、MotionEventを検知した際にRecyclerViewのスクロール動作に割り込んで実装したタップイベントを先に処理できるようになります。

// SimpleOnItemTouchListenerを継承したリスナークラス
class RecyclerItemClickListener() : RecyclerView.SimpleOnItemTouchListener() {

    override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean {
       // タップイベント検知時の処理
    }
}

インターフェースの作成

リスナークラス内に実装するインターフェースを準備します。
今回はシングルタップ時とロングタップ時のメソッドを作ることにします。

// SimpleOnItemTouchListenerを継承したリスナークラス
class RecyclerItemClickListener() : RecyclerView.SimpleOnItemTouchListener() {

    interface OnRecyclerClickListener {
        // シングルタップ用
        fun onItemClick(view: View, position: Int)
        // ロングタップ用
        fun onItemLongClick(view: View, position: Int)
    }

    override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean {
       // タップイベント検知時の処理
    }
}

GestureDetectorでタップイベントの種類を判別

検知したタップイベントをGestureDetectorに渡すことでシングルタップやダブルタップなどを判別できるようにします。
GestureDetector  |  Android Developers

参考までに、GestureDetector.SimpleOnGestureListenerのメソッドには以下のように様々な種類があります。

メソッド名 検知する操作
onContextClick コンテキストクリック(コンテキストメニューを表示するクリック、長押しなど)
onDoubleTap ダブルタップ
onDoubleTapEvent ダブルタップの開始、移動、終了(DOWN, MOVE, UP)
onDown 押下(DOWN)
onFling サッとなぞる
onLongPress ロングタップ
onScroll 押下して画面上を移動
onShowPress 押下して少し待つ
onSingleTapConfirmed シングルタップ
onSingleTapUp 押下して指を離した時(UP)

今回は一旦、シングルタップロングタップを実装したいと思います。
GestureDetectorをインスタンス化し、シングルタップを検知するonSingleTapConfirmedとロングタップを検知するonLongPressを実装します。

class RecyclerItemClickListener(
    context: Context,
    recyclerView: RecyclerView,
    private val listener: OnRecyclerClickListener
) : RecyclerView.SimpleOnItemTouchListener() {

...

// GestureDetectorのインスタンス化
private val gestureDetector =
        GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {

            // シングルタップ
            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
                val childView = recyclerView.findChildViewUnder(e.x, e.y)

                if (childView != null) {
                    listener.onItemClick(
                        childView,
                        recyclerView.getChildAdapterPosition(childView)
                    )
                }
                return true
            }

            // ロングタップ
            override fun onLongPress(e: MotionEvent) {
                val childView = recyclerView.findChildViewUnder(e.x, e.y)

                if (childView != null) {
                    listener.onItemLongClick(
                        childView,
                        recyclerView.getChildAdapterPosition(childView)
                    )
                }
                super.onLongPress(e)
            }
        })
...
}

RecyclerItemClickListenerのコンストラクタ引数にContext, RecyclerView, OnRecyclerClickListenerを追加しています。

各overrideメソッドではクリックされたRecyclerView内の子ビューがある場合はそれを取得し、先ほど作成したOnRecyclerClickListenerインターフェースのメソッドに子ビューとそのポジションを渡します。

返り値がtrueの時、RecyclerViewやその子ビューに定義されているイベントを止めてタップイベントが挿入されます。

ただしonLongPressでは検知した後にRecyclerViewのスクロールができなくなてしまっては困るので、trueを返さないようにしています。

onTouchEventの呼び出し

onInterceptTouchEventの中でGestureDetectorのonTouchEventを呼び出すようにします。

class RecyclerItemClickListener(
    context: Context,
    recyclerView: RecyclerView,
    private val listener: OnRecyclerClickListener
) : RecyclerView.SimpleOnItemTouchListener() {


    interface OnRecyclerClickListener {
...
    }

private val gestureDetector =
        GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {
...
        })

    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(e)
    }
}

これでタップイベントが以下の流れで処理されるようになります。

タップイベント発生
onInterceptTouchEventで検知
GestureDetector#onTouchEvent呼び出し
→該当するタップイベントのoverrideメソッド呼び出し
OnRecyclerClickListenerインターフェースのメソッド呼び出し

というわけで最後はOnRecyclerClickListenerインターフェースのメソッドを実装するだけです。

インターフェースの実装

シングルタップ時に行った店の詳細画面に遷移するようにしたいのですが、その実装は次回の記事に回したいと思います。

今回はタップ時にログが出力されることを確かめます。
行った店リストを表示するVisitedRestaurantFragmentでインターフェースRecyclerItemClickListener.OnRecyclerClickListenerを継承し、それぞれログを出力するようメソッドを実装します。

class VisitedRestaurantFragment : Fragment(),
    RecyclerItemClickListener.OnRecyclerClickListener {
...

    // シングルタップ時の処理
    override fun onItemClick(view: View, position: Int) {
        Log.d("VisitedRestaurantFragment", "Single tap position: $position")
    }

    // ロングタップ時の処理
    override fun onItemLongClick(view: View, position: Int) {
        Log.d("VisitedRestaurantFragment", "Long tap position: $position")
    }
}

いざ実践

以下のような状態の行った店リストでアイテムをタップしてみます。

(毎回データを消して入れ直しているので、その時私が食べたい物がバレますね…)

1つ目(position: 0)のモスバーガーをシングルタップ、2つ目(position: 1)の丸亀製麺をロングタップしてみます。
(開発者向けオプションでタップ位置を表示した状態で動画にとってみたのですが、シングルタップ・ロングタップが全然伝わらなさそうなので掲載は諦めました…)

するとログに以下のように出力されました。
タップの種類を検知できていますね。

次回はタップ時に行った店詳細画面に遷移したいと思います!
ではまた!