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

#やりたいこと
【イメージ画像】
![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658219/033139ce-1c28-3fc5-14ae-15f034676dc5.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です。`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", "ダブルタップ")
    }
```
のようにしてオーバーライドします。今回はログとトーストに呼び出されたことを示す処理を書きましたが、色々な応用の仕方があると思います。これにて必要なコーディングは終了です。あとはアプリを起動すれば思惑通り、タップに仕方によって異なる処理が可能なはずです。
【イメージ画像】
![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658219/033139ce-1c28-3fc5-14ae-15f034676dc5.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`本体についてはかなり適当なリストを挿入しただけなので気にしないでください。