横方向RecyclerViewとSwipeRefreshLayoutが競合してRecyclerViewがスクロールしづらい問題を解決する

本記事は[and factory Advent Calendar 2019](https://qiita.com/advent-calendar/2019/andfactory)の11日目の記事になります。昨日は[MatsuNaoPen](https://qiita.com/MatsuNaoPen)さんの[adbコマンドで端末を操作する](https://qiita.com/MatsuNaoPen)でした。

#問題
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222352/f687dc1a-a1f6-b3a6-4992-26cd042be844.gif)

僕が開発しているアプリには上記のような画面があります。なんら難しくないRecyclerViewをSwipeRefreshLayoutで囲んであげるだけのやつですね。

```xml
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
```
こんな感じのxmlになるかと思います。

---

ある日社内チェックで「なんかスクロールしづらいんですけど、、、」という指摘をされました。
確かに、斜め方向にスクロールすると横にスクロールしたりSwipeRefreshLayoutが反応したりと使いづらいなと感じたので修正してみました。

#原因はSwipeRefreshLayoutにあり
SwipeRefreshLayoutがスクロールに過敏に反応しすぎてしまうのが原因であることがわかりました。よってSwipeRefreshLayoutをカスタマイズして凡そ縦方向に指をスライドさせた時にだけ反応するように修正します。

#修正方法
コードはこちらです

```kotlin
class OnlyVerticalSwipeRefreshLayout(context: Context, attrs: AttributeSet) :
    SwipeRefreshLayout(context, attrs) {

    private var touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
    private var prevX: Float = 0.toFloat()
    private var declined: Boolean = false

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                prevX = MotionEvent.obtain(event).x
                declined = false // New action
            }

            MotionEvent.ACTION_MOVE -> {
                val eventX = event.x
                val xDiff = abs(eventX - prevX)
                if (declined || xDiff > touchSlop) {
                    declined = true // Memorize
                    return false
                }
            }
        }
        return super.onInterceptTouchEvent(event)
    }
}
```

##ポイントその１:scaledTouchSlop

[scaledTouchSlop](https://developer.android.com/reference/android/view/ViewConfiguration#getScaledTouchSlop())とは公式ドキュメントによると

``` text
Distance in pixels a touch can wander before we think the user is scrolling
```
と書かれており、

```text
ユーザーがスクロールを開始したとOSが認識する前にスクロールできるピクセル距離
```
ということだと解釈しました。要するに**スクロールを開始した瞬間の指の移動距離**のことだと思います。

##ポイントその２:onInterceptTouchEvent
[onInterceptTouchEvent](https://developer.android.com/training/gestures/viewgroup#intercept)では子Viewで発生したイベントを親Viewが傍受します。更にそのタッチイベントを親Viewイベント奪う場合はtrue、親Viewがイベントを奪わず子Viewにイベントを流す場合はfalseを返却します。今回のケースでは子Viewが横方向RecyclerView、親ViewがSwipeRefreshLayoutになります。


##ポイントその３:指の移動距離からSwipeRefreshLayoutがイベントを奪うかどうか判断する

```kotlin
when (event.action) {
    MotionEvent.ACTION_DOWN -> {
        prevX = MotionEvent.obtain(event).x
        declined = false // New action
    }

    MotionEvent.ACTION_MOVE -> {
        val eventX = event.x
        val xDiff = abs(eventX - prevX)
        if (declined || xDiff > touchSlop) {
            declined = true // Memorize
            return false
        }
    }
}
```
この部分になります。
ACTION_DOWNで最初のタップ位置を保持します。ACTION_MOVEでx方向の移動距離を計算して、移動距離がtouchSlopを超えた場合はRecyclerViewにイベントを流します。

---
最後にSwipeRefreshLayoutをOnlyVerticalSwipeRefreshLayoutに変更してあげれば修正完了です。

#参考サイト
https://stackoverflow.com/questions/34136178/swiperefreshlayout-blocking-horizontally-scrolled-recyclerview