LoginSignup
9
6

More than 3 years have passed since last update.

【Kotlin】RecyclerViewでクリック処理+長押しやダブルタップ時の処理を作る

Last updated at Posted at 2020-07-13

やりたいこと

【イメージ画像】
gif.gif
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です。addOnItemTouchListenerRecyclerView.OnItemTouchListenerのタッチイベントをインターセプトするコールバックメソッドです。

テスト

 この記述によって、簡易的なクリック処理が完成したはずです。アプリを起動して、ログを確認してみてください。

20XX-XX-XX XX:XX:XX.XXX XXXXX-XXXXX/com.e.practicerecyclerviewclick D/PracticeRecyclerItem: .onInterceptTouchEvent called

上手くいけば、クリックする度に↑のようにログに出力されるはずです。このように、SimpleOnItemTouchListeneraddOnItemTouchListenerを使えば簡単にクリックを検知することが出来ます。ここからは、クリックしたときにどういった処理を行わせるかについて書いていこうと思います。

クリックに処理を付ける

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
        }
    })

onSingleTapUponLongPressonDoubleTapをオーバーライドするだけで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)

 ここで、onSingleTapUponDoubleTapBooleanの型の返り値を持つのに、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", "ダブルタップ")
    }

のようにしてオーバーライドします。今回はログとトーストに呼び出されたことを示す処理を書きましたが、色々な応用の仕方があると思います。これにて必要なコーディングは終了です。あとはアプリを起動すれば思惑通り、タップに仕方によって異なる処理が可能なはずです。
【イメージ画像】
gif.gif

今回のコード全容

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本体についてはかなり適当なリストを挿入しただけなので気にしないでください。

 

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6