今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、RecyclerViewのセルをタップした際のイベントを検知する処理についてまとめていきたいと思います。
リスナーの実装
RecyclerViewのタップ処理実装方法は調べてみると色々なパターンがありそうでした。
実装方法の候補
・SimpleOnItemTouchListener
→単純なタップやロングタップに対応できる
・ItemTouchHelper
→ドラッグやスワイプに対応できる
・自前でリスナーのインターフェースを作りViewHolderに適用
→単純なタップ等
今回はSimpleOnItemTouchListenerを使ってみたいと思います。
今後はスワイプで削除とかもやりたいのでItemTouchHelperも使いたい…(実装した後に気づいた…)
両方併用することも可能そうなので今度試してみます。
onInterceptTouchEventの実装
onInterceptTouchEventはタップイベントを監視するメソッドです。
RecyclerView.SimpleOnItemTouchListener | Android Developers
実装することで、MotionEventを検知した際にRecyclerViewのスクロール動作に割り込んで実装したタップイベントを先に処理できるようになります。
class RecyclerItemClickListener() : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean {
}
}
インターフェースの作成
リスナークラス内に実装するインターフェースを準備します。
今回はシングルタップ時とロングタップ時のメソッドを作ることにします。
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() {
...
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)の丸亀製麺をロングタップしてみます。
(開発者向けオプションでタップ位置を表示した状態で動画にとってみたのですが、シングルタップ・ロングタップが全然伝わらなさそうなので掲載は諦めました…)
するとログに以下のように出力されました。
タップの種類を検知できていますね。
次回はタップ時に行った店詳細画面に遷移したいと思います!
ではまた!