#やりたいこと
【イメージ画像】
RecyclerViewにて、タイトルにあるような数種類のクリックで異なる処理を行わせています。難しいように見えて意外とそうでもないので、ぜひ試してみてください。
#クリックを検知させる
※RecyclerView本体が完成している前提で記事を書いています。
##SimpleOnItemTouchListener
今回はSimpleOnItemTouchListener
というコールバックのインターフェースを使ってクリック処理を記述します。このインターフェースはRecyclerView
のインターフェースの一つである、OnItemTouchListener
の互換性が高いバージョンです。RecyclerViewのタッチイベントを、スクロール動作と見なされる前に割り込むことが出来ます。このインターフェースを使うことで、非常に簡単にクリックの処理を作り出せます。
最初にSimpleOnItemTouchListener
を拡張するクラスを作りましょう。以下のようになります。
class PracticeRecyclerItemClickListener() : RecyclerView.SimpleOnItemTouchListener() {
次に、インターフェースに定義されている、クリックを検出するためのメソッドを実装します。それは、onInterceptTouchEvent
と呼ばれるもので、RecyclerView自体やその子となるビューに設定されているイベントの前に割り込んで、タッチイベントを監視・引継ぎを行います。先ほど作ったクラスの中で、オーバーライドして下さい。
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
Log.d("PracticeRecyclerItem", ".onInterceptTouchEvent called")
return super.onInterceptTouchEvent(rv, e)
}
取り敢えず本当にクリックを監視しているかをログで確認する為に、二行目でLogを設定してあります。
##addOnItemTouchListener
RecyclerView
のアダプターを設定する前に、先ほど記述した処理のリスナーをMainActivity
に設定します。具体的には以下の二行目の様です。
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addOnItemTouchListener(PracticeRecyclerItemClickListener())
recycler_view.adapter = PracticeRecyclerViewAdapter(testList)
recycler_view
はビューのRecyclerView
のidです。addOnItemTouchListener
はRecyclerView.OnItemTouchListener
のタッチイベントをインターセプトするコールバックメソッドです。
##テスト
この記述によって、簡易的なクリック処理が完成したはずです。アプリを起動して、ログを確認してみてください。
20XX-XX-XX XX:XX:XX.XXX XXXXX-XXXXX/com.e.practicerecyclerviewclick D/PracticeRecyclerItem: .onInterceptTouchEvent called
上手くいけば、クリックする度に↑のようにログに出力されるはずです。このように、SimpleOnItemTouchListener
とaddOnItemTouchListener
を使えば簡単にクリックを検知することが出来ます。ここからは、クリックしたときにどういった処理を行わせるかについて書いていこうと思います。
#クリックに処理を付ける
##GestureDetector
アンドロイドAPIのGestureDetector
を使って、クリックされた時の処理を作っていきます。このクラスは、さまざまなモーションやイベントを検出し、独自のコールバックメソッドからユーザーに通知してくれます。今回やりたいことは、前半でやったようにRecyclerView
の機能を使ってクリックを検知→それをGestureDetector
に渡して、普通のタップやダブルタップなどの違いに応じて処理を作るといった事です。GestureDetector
を使うためにまずはコンストラクタを追加します。具体的には以下のようです。
class PracticeRecyclerItemClickListener(context: Context, recyclerView: RecyclerView, private val listener: PracticeRecyclerItemClickListener.OnRecyclerClickListener)
: RecyclerView.SimpleOnItemTouchListener() {
そして、クラスの中にインターフェースを定義します。
interface OnRecyclerClickListener{
fun onItemClick(view: View, position: Int)
fun onDoubleClick(view: View, position: Int)
fun onItemLongClick(view: View, position: Int)
}
このコールバックインターフェースを用いて、後ほどMainActivityにそれぞれのメソッドを実装します。今回は上記3つのメソッドを追加しようと思います。次に、onInterceptTouchEvent
メソッドを以下のように変更します。
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
val result = gestureDetector.onTouchEvent(e)
return result
}
GestureDetector
が検知する全てのタップイベントを、result
という変数がキャッチしてくれます。そしてその結果を返すことで、RecyclerView
がタップされた時GestureDetector
のクリックイベントを起動することが出来ます。最後に、GestureDetector
を実体化した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))
}
}
override fun onDoubleTap(e: MotionEvent): Boolean {
val childView = recyclerView.findChildViewUnder(e.x, e.y)
if (childView != null) {
listener.onDoubleClick(childView, recyclerView.getChildAdapterPosition(childView))
}
return true
}
})
onSingleTapUp
とonLongPress
、onDoubleTap
をオーバーライドするだけでgestureDetector
は殆ど完成です。タイトルにある3種の操作に対応できます。あとはそのそれぞれに以下の同じような処理を書いていくだけです。
val childView = recyclerView.findChildViewUnder(e.x, e.y)
if (childView != null) {
listener.onLongPress(childView, recyclerView.getChildAdapterPosition(childView))
}
この部分で、クリックされたRecyclerView
内の子ビューを取得し、それ自身とそのポジションを先ほど定義したインターフェースOnRecyclerClickListener
内のメソッドに渡しています。このように、非常に簡単に数種類のクリックに対応する処理を作ることが出来ます。また、GestureDetector
にはまだ有用なメソッドが定義されているので、以下の公式リファレンスを参照して、今回と同じように実装してみてください。
https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener.html
###Tips: GestureDetectorのメソッドにおける返り値(飛ばしてもおk)
ここで、onSingleTapUp
とonDoubleTap
はBoolean
の型の返り値を持つのに、onLongPress
はなぜ何も返さないのか気になった人がいるかもしれません。基本的に前者の二つのメソッドは、タップされるとtrue
を返します。記事を辿ってもらうと分かりますが、その後true
という真偽値は、gestureDetector
を経由してresult
に渡され、onInterceptTouchEvent
の返り値になります。このメソッドは、返り値がtrue
の時、RecyclerView
やその子ビューに定義されているイベント全てに割り込んで、イベントを挿入します。これが意味するところは、onInterceptTouchEvent
の返り値がtrue
になると、スクロールをはじめとしたRecyclerView
の操作が出来なくなるということです。シングルタップやダブルタップではそこまで困る話ではないのですが、長押しの時は話が別です。長押しを意図せず、画面に指を置いたまましばらく経っても、画面のスクロールは出来るようにしなければなりません。しかし、他のメソッドと同じようにtrue
を返してしまうと、そうはいかず、スクロールが出来なくなってしまいます。したがって、onLongPress
メソッドに関しては、そういった問題の為に敢えて返り値を返さないことで、スクロール等のビューの処理と共存できるように設計されています。
##MainActivityに処理を書く!
インターフェースOnRecyclerClickListener
に定義したメソッドを実装して処理を書いていきます。今回は次のようにして、インターフェースを実装します。
class MainActivity : AppCompatActivity(), PracticeRecyclerItemClickListener.OnRecyclerClickListener{
そしてMainActivity
の中で、
override fun onItemClick(view: View, position: Int) {
Toast.makeText(this, "普通のタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "普通のタップ")
}
override fun onItemLongClick(view: View, position: Int) {
Toast.makeText(this, "長押しタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "長押しのタップ")
}
override fun onDoubleClick(view: View, position: Int) {
Toast.makeText(this, "ダブルタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "ダブルタップ")
}
のようにしてオーバーライドします。今回はログとトーストに呼び出されたことを示す処理を書きましたが、色々な応用の仕方があると思います。これにて必要なコーディングは終了です。あとはアプリを起動すれば思惑通り、タップに仕方によって異なる処理が可能なはずです。
【イメージ画像】
#今回のコード全容
class PracticeRecyclerItemClickListener(context: Context, recyclerView: RecyclerView, private val listener: PracticeRecyclerItemClickListener.OnRecyclerClickListener)
: RecyclerView.SimpleOnItemTouchListener() {
interface OnRecyclerClickListener{
fun onItemClick(view: View, position: Int)
fun onDoubleClick(view: View, position: Int)
fun onItemLongClick(view: View, position: Int)
}
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)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
val childView = recyclerView.findChildViewUnder(e.x, e.y)
if (childView != null) {
listener.onDoubleClick(childView, recyclerView.getChildAdapterPosition(childView))
}
return super.onDoubleTap(e)
}
})
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
val result = gestureDetector.onTouchEvent(e)
return result
}
}
class MainActivity : AppCompatActivity(), PracticeRecyclerItemClickListener.OnRecyclerClickListener{
private val practiceRecyclerViewAdapter = PracticeRecyclerViewAdapter(ArrayList())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testList: List<String> = listOf("一行目", "二行目", "三行目", "四行目", "五行目")
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addOnItemTouchListener(PracticeRecyclerItemClickListener(this, recycler_view, this))
recycler_view.adapter = PracticeRecyclerViewAdapter(testList)
}
override fun onItemClick(view: View, position: Int) {
Toast.makeText(this, "普通のタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "普通のタップ")
}
override fun onItemLongClick(view: View, position: Int) {
Toast.makeText(this, "長押しタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "長押しのタップ")
}
override fun onDoubleClick(view: View, position: Int) {
Toast.makeText(this, "ダブルタップ", Toast.LENGTH_SHORT).show()
Log.d("MainActivty", "ダブルタップ")
}
}
RecyclerView
本体についてはかなり適当なリストを挿入しただけなので気にしないでください。