14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

and factoryAdvent Calendar 2019

Day 11

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

Last updated at Posted at 2019-12-11

本記事はand factory Advent Calendar 2019の11日目の記事になります。昨日はMatsuNaoPenさんのadbコマンドで端末を操作するでした。

#問題
sample.gif

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

<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をカスタマイズして凡そ縦方向に指をスライドさせた時にだけ反応するように修正します。

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

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

##ポイントその1:scaledTouchSlop

scaledTouchSlopとは公式ドキュメントによると

Distance in pixels a touch can wander before we think the user is scrolling

と書かれており、

ユーザーがスクロールを開始したとOSが認識する前にスクロールできるピクセル距離

ということだと解釈しました。要するにスクロールを開始した瞬間の指の移動距離のことだと思います。

##ポイントその2:onInterceptTouchEvent
onInterceptTouchEventでは子Viewで発生したイベントを親Viewが傍受します。更にそのタッチイベントを親Viewイベント奪う場合はtrue、親Viewがイベントを奪わず子Viewにイベントを流す場合はfalseを返却します。今回のケースでは子Viewが横方向RecyclerView、親ViewがSwipeRefreshLayoutになります。

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

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

14
2
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
14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?