美文网首页
关于在RecyclerView中的Banner滑动冲突问题

关于在RecyclerView中的Banner滑动冲突问题

作者: 寻水的鱼Chock | 来源:发表于2022-11-06 11:54 被阅读0次
前言

本文主要是为了记录一下在RecyclerView中添加同向滑动的Banner后导致Banner无法切换的问题,此问题的解决方案可能不是最优解,也可能不是你期望的结果,但能够提供一种解决思路,希望对你有用。
(本文以上下滑动的RecyclerView与上下滑动切换的BannerViewPager滑动冲突为示例)

问题复现

View的布局结构是一个全屏的androidx.recyclerview.widget.RecyclerView,使用androidx.recyclerview.widget.LinearLayoutManager。其中Item布局xml如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clTaskRoot"
        android:layout_width="match_parent"
        android:tag="disable_vertical_scroll"
        android:layout_height="wrap_content">

        <com.zhpan.bannerview.BannerViewPager
            android:id="@+id/banner"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:bvp_indicator_checked_color="@color/color_FFFFFF"
            app:bvp_indicator_normal_color="@color/color_FFFFFF_50"
            app:bvp_indicator_style="round_rect"
            app:bvp_interval="5000"
            app:bvp_scroll_duration="600" />

        <com.ned.mysterybox.view.HorizontalIndicatorView
            android:id="@+id/indicator"
            android:layout_width="wrap_content"
            android:layout_height="2dp"
            android:layout_marginTop="@dimen/dp_8"
            android:visibility="gone"
            app:layout_constraintTop_toBottomOf="@id/banner"
            app:layout_constraintStart_toStartOf="@id/banner"
            app:layout_constraintEnd_toEndOf="@id/banner"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

BannerViewPager滑动方向设置为上下滑动setOrientation(ViewPager2.ORIENTATION_VERTICAL)
(注意这边设置了一个tag = disable_vertical_scroll 后面会用到)

运行后出现的问题是,当点击BannerViewPager向上或者向下滑动Banner切换时,RecyclerView消耗了所有的滑动事件,并没有按照预期的Banner切换,而是变成了RecyclerView的上下滑动。

解决方案

其实很容易发现,出现此问题的原因就是Touch事件分发与预期不一致。

思路分析

如果是自己开发的自定义View,可以调整自己Touch事件处理的逻辑代码进行修复,但本次因为使用的是第三方库的原因,代码逻辑已经基本固定,不容易再进行二次开发(也不是不行,可能不是太容易)。那换个思路,在RecyclerView中进行拦截?

通过分析RecyclerView的源码可以发现,其其实可以通过设置addOnItemTouchListener实现对Touch事件的拦截。

OnItemTouchListener接口有三个函数 onInterceptTouchEventonTouchEventonRequestDisallowInterceptTouchEvent,很好理解,这些函数名跟ViewGroup中相应的函数名一致,功能也类似。

接下来要做的就是2件事,事件发现事件处理
1)事件发现
回到我们最初预期的效果,当我们滑动Banner时Banner处理,滑动其他Item时RecyclerView滑动处理。
所以可以认为,当Touch事件的MotionEvent.ACTION_DOWN在Banner区域时,Banner消耗所有的滑动事件,在其他View区域时,不进行处理。

通过将MotionEvent事件的x,y坐标传入findChildViewUnder可以找到 RecyclerView中所对应的View。这里我使用tag标记,判断此View是否就是目标View(也可通过ID或者class名确认),返回True,意味着接下来的所有Touch事件均将被拦截。

override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
   if (e.action != MotionEvent.ACTION_DOWN) return false
    val view = rv.findChildViewUnder(e.x, e.y)
    if (view?.tag == "disable_vertical_scroll") {
        ......
        //找到实际的Banner View
        targetView = view.findViewById<BannerViewPager<*>>(R.id.banner)
        targetView?.requestDisallowInterceptTouchEvent(true)
        return true
    }
    return false
}

2)事件处理
到这里其实就很好处理了, 就是对一连串的Touch事件转换为滑动或者点击动作,并响应。

override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
    targetView?.dispatchTouchEvent(e)
    when (e.action) {
        MotionEvent.ACTION_DOWN -> {
            startX = e.x
            startY = e.y
        }
        MotionEvent.ACTION_MOVE -> {
            process(rv, e)
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            process(rv, e)
            startX = 0f
            startY = 0f
        }
    }
}

总结一下,实现步骤大致是:
1)RecyclerView通过设置OnItemTouchListener监听器实现对在RecyclerView上所有的Touch事件进行处理(判断是否拦截);
2)在OnItemTouchListener监听器的onInterceptTouchEvent函数中,通过RecyclerViewfindChildViewUnder函数判断是否是目标区域的滑动事件,是返回true表示进行拦截,否返回false表示不进行处理;
3)在OnItemTouchListener监听器的 onTouchEvent函数中对所有拦截的Touch事件进行处理,包括上下、左右滑动、点击等。

OK,到这里就已经基本结束,接着就是对一些细节进行优化处理,在文章后面贴上了完整代码。

如果本文对你有帮助就点个赞支持下吧~~~

完整代码
mRecyclerView.addOnItemTouchListener(VerticalScrollBannerTouchListener(mContext))
class VerticalScrollBannerTouchListener(val context: Context) : RecyclerView.OnItemTouchListener {

    var targetView: BannerViewPager<*>? = null
    var vRoot: View? = null
    val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop / 3
    var enable = false
    var startX = 0f
    var startY = 0f
    var processTime = 0L

    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
        if (e.action != MotionEvent.ACTION_DOWN) return false
        val view = rv.findChildViewUnder(e.x, e.y)
        if (view?.tag == "disable_vertical_scroll") {
            vRoot = view
            targetView = view.findViewById<BannerViewPager<*>>(R.id.banner)
            targetView?.requestDisallowInterceptTouchEvent(true)
            enable = true
            return true
        }
        enable = false
        targetView = null
        return false
    }

    override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
        targetView?.dispatchTouchEvent(e)//不能删
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                startX = e.x
                startY = e.y
            }
            MotionEvent.ACTION_MOVE -> {
                process(rv, e)
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                process(rv, e)
                startX = 0f
                startY = 0f
                if (enable) {
                    val ec = MotionEvent.obtain(e)
                    val xx = (rv.scaleX + (vRoot?.left ?: 0) + (targetView?.left ?: 0))
                    val xy = (rv.scaleY + (vRoot?.top ?: 0) + (targetView?.top ?: 0))
                    ec.offsetLocation(-xx, -xy)
                    ec.action = MotionEvent.ACTION_DOWN
                    targetView?.dispatchTouchEvent(ec)
                    ec.action = MotionEvent.ACTION_UP
                    targetView?.dispatchTouchEvent(ec)
                    ec.recycle()
                }
                enable = false
                targetView = null
            }
        }
    }

    override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
    }

    fun process(rv: RecyclerView, e: MotionEvent) {
        if (!enable) return
        val endX = e.x
        val endY = e.y
        var dy = startY - endY
        val disX = abs(endX - startX)
        val disY = abs(endY - startY)
        if (disY > disX) {
            dy = if (dy > 0) {
                0f.coerceAtLeast(dy - mTouchSlop)
            } else {
                0f.coerceAtMost(dy + mTouchSlop)
            }
            if (dy != 0f) {
                val size = targetView?.data?.size ?: 1
                val now = System.currentTimeMillis()
                if (size > 1 && now - processTime > 600) {
                    val p = targetView?.currentItem ?: 0
                    if (dy > 0) {
                        if (p + 1 >= size) {
                            targetView?.currentItem = 0
                        } else {
                            targetView?.currentItem = p + 1
                        }
                    } else {
                        if (p - 1 < 0) {
                            targetView?.currentItem = size - 1
                        } else {
                            targetView?.currentItem = p - 1
                        }
                    }
                    processTime = System.currentTimeMillis()
                }
                enable = false
            }
        }
    }
}
如果本文对你有帮助就点个赞支持下吧~~~

相关文章

网友评论

      本文标题:关于在RecyclerView中的Banner滑动冲突问题

      本文链接:https://www.haomeiwen.com/subject/whebtdtx.html