背景:2个同向滑动的RecyclerView相互嵌套,进行滑动时发生滑动冲突。例如,Viewpager2内部包含了另一个ViewPager2或一个横向滑动的recyclerView。这时,在横向滑动时,滑动的可能是外部的ViewPager2,也可能是内部的RecyclerView。

滑动冲突的原因外部ViewPager2和内部RecyclerView的滑动阈值不同造成滑动冲突。ViewPager2的滑动阈值较高,内部RecyclerView的滑动阈值较低。具体来说,在触摸事件分发时,ViewPager2会先尝试对事件进行拦截,若滑动的距离大于了ViewPager2的滑动阈值,则ViewPager2会对此事件进行拦截,即给之前处理事件的子视图发一个Cancel通知,然后自己去处理后继的所有事件。比如,对于包含有RecyclerView的ViewPager2来说,由于滑动距离的增长快慢不同,会导致不同的视图处理滑动事件;1、滑动距离增长缓慢,当滑动距离超过了RecyclerView的滑动阈值且低于ViewPager2的滑动阈值时,RecyclerView会处理事件,VewPager2不会处理事件,且RecclerView在发生滑动时,会通知父视图不要拦截之后的事件;2、当滑动距离突增,超过了ViewPager2的滑动阈值时,ViewPager2会处理事件,并对事件进行拦截,这样就造成内部的RecyclerView无法处理事件。这就是滑动冲突产生的原因。

解决思路:当内部的RecyclerView可以滑动时,优先滑动内部的RecyclerView;内部视图不可以滑动时,则滑动外部的ViewPager2。为了实现此目标,针对实际种可能存在的2种不同情况,给出了不同的解决方案。

    视图嵌套场景一外部视图的滑动阈值大,内部视图的滑动阈值小的情况。例如,ViewPager2包含一个RecyclerView

    解决方法一:对外部视图的dispatchTouchEvent方法进行重写,在滑动距离首次超过自身的滑动阈值时,先发送一个滑动距离等于自身滑动阈值的事件,以使滑动阈值较小的内部视图可以优先处理事件,然后再发生原触摸事件。对于上述场景例子来说,因为外部的ViewPager2用final进行了修饰,不能被继承,进而不能重写dispatchTouchEvent方法,所以需要一个“壳”View,将ViewPager2放到此“壳”View内,并重写壳view的dispatchTouchEvent。壳View代码可以如下:

/**
 * 此视图内部只能包含一个RecyclerView
 */
class OutsideLinearLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
    LinearLayout(context, attrs, defStyleAttr) {

    //记录手指按下时的坐标
    var mInitialTouchX: Float = -1f
    var mInitialTouchY: Float = -1f

    //判断是否需要在分发原MotionEvent事件之前,分发一个滑动距离为滑动阈值的事件
    var splitFlag = false

    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, -1)

    constructor(context: Context?) : this(context, null)

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            if(it.action == MotionEvent.ACTION_DOWN){
                mInitialTouchX = it.x
                mInitialTouchY = it.y
                splitFlag = true
            }else if(it.action == MotionEvent.ACTION_MOVE){
                var recyclerView = searchRecyclerView(this)
                //获取ViewPager2的滑动阈值
                var touchSlop = getRecyclerViewTouchSlop(recyclerView!!)
                var isScroll = Math.abs(it.x - mInitialTouchX)>touchSlop||Math.abs(it.y - mInitialTouchY)>touchSlop
                //检查是否需要插入一个滑动距离为滑动阈值的触摸事件
                //需要满足的条件为:1、滑动距离里大于滑动阈值;2、自Down事件发生后,第一次大于滑动阈值
                if(isScroll&&splitFlag){
                    var newMotionEvent = MotionEvent.obtain(it)
                    var newX = it.x
                    if(Math.abs(it.x - mInitialTouchX)>touchSlop){
                        //使新MotionEvent的x与手指按下时的x之间的差为滑动阈值
                        if(it.x-mInitialTouchX>0){
                            newX = mInitialTouchX+touchSlop
                        }else newX = mInitialTouchX-touchSlop
                    }
                    var newY = it.y
                    if(Math.abs(it.y - mInitialTouchY)>touchSlop){
                        if(it.y-mInitialTouchY>0){
                            newY = mInitialTouchY+touchSlop
                        }else newY = mInitialTouchY-touchSlop
                    }
                    newMotionEvent.setLocation(newX, newY)
                    //发生新的MotionEvent
                    super.dispatchTouchEvent(newMotionEvent)
                    splitFlag = false
                }
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    /**
     * 获取RecyclerView的滑动阈值
     * @param recyclerView RecyclerView
     * @return Int 滑动阈值
     */
    fun getRecyclerViewTouchSlop(recyclerView: RecyclerView): Int {
        var clazz = RecyclerView::class.java
        var field = clazz.getDeclaredField("mTouchSlop")
        field.isAccessible = true
        var touchSlop = field.get(recyclerView) as Int
        return touchSlop
    }

    /**
     * 获取ViewGroup中的RecyclerView
     * @param vg ViewGroup
     * @return RecyclerView?
     */
    fun searchRecyclerView(vg: ViewGroup):RecyclerView?{
        for(i in 0 until vg.childCount){
            var view = vg.getChildAt(i)
            if(view is RecyclerView){
                return view
            }else if(view is ViewGroup){
                var recyclerView = searchRecyclerView(view)
                if(recyclerView!=null){
                    return recyclerView
                }
            }
        }
        return null
    }
}

    将ViewPager2放到壳View中:

<com.example.icemusic.view.ViewPager2LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:id="@+id/view_pager2_linear_layout"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/tabLayout">

            <androidx.viewpager2.widget.ViewPager2
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:id="@+id/viewPager">
            </androidx.viewpager2.widget.ViewPager2>

        </com.example.icemusic.view.ViewPager2LinearLayout>

    视图嵌套场景二外部视图的滑动阈值和内部视图的滑动阈值相同。例如,外部ViewPager2里又含有一个ViewPager2

    解决方法二:在上一种方案的基础上,将内部视图的滑动阈值减小。针对场景中的例子,可以通过反射将内部ViewPager2的滑动阈值减一。由于ViewPager2不能被继承,因此也需要一个壳View来执行此操作,壳View代码如下:

/**
 * 用来封装RecyclerView,其内部只能含有1个RecyclerView
 */
class SmallLinearLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
    LinearLayout(context, attrs, defStyleAttr) {
    val TAG = "SmallLinearLayout"

    //记录视图中RecyclerView的滑动阈值,注:普通的RecyclerView与ViewPager2内部自带的RecyclerView的滑动阈值不同
    var recyclerViewSlop = -1

    constructor(context: Context):this(context,null)

    constructor(context: Context,attributeSet: AttributeSet?):this(context,attributeSet!!,-1)


    private fun setRecyclerViewTouchSlop(recyclerView: RecyclerView, touchSlop: Int) {
        var clazz = RecyclerView::class.java
        var field = clazz.getDeclaredField("mTouchSlop")
        field.isAccessible = true
        field.set(recyclerView,touchSlop)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        if(ev!!.action == MotionEvent.ACTION_DOWN){
            var recyclerView = searchRecyclerView(this)
            recyclerView?.let {
                if(recyclerViewSlop==-1){
                    recyclerViewSlop = getRecyclerViewTouchSlop(it)
                }
                //减小reyclerView的滑动阈值
                setRecyclerViewTouchSlop(it,recyclerViewSlop-1)
                var slop = getRecyclerViewTouchSlop(recyclerView)
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    /**
     * 获取ViewGroup中的RecyclerView
     * @param vg ViewGroup
     * @return RecyclerView?
     */
    fun searchRecyclerView(vg:ViewGroup):RecyclerView?{
        for(i in 0 until vg.childCount){
            var view = vg.getChildAt(i)
            if(view is RecyclerView){
                return view
            }else if(view is ViewGroup){
                var recyclerView = searchRecyclerView(view)
                if(recyclerView!=null){
                    return recyclerView
                }
            }
        }
        return null
    }

    fun getRecyclerViewTouchSlop(recyclerView: RecyclerView): Int {
        var clazz = RecyclerView::class.java
        var field = clazz.getDeclaredField("mTouchSlop")
        field.isAccessible = true
        var touchSlop = field.get(recyclerView) as Int
        return touchSlop
    }
}

    将内部的ViewPager2方法此壳View里:

<com.example.icemusic.view.SmallLinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/ad_small_linear_layout"
            android:orientation="horizontal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/ad_view_pager2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:onPageChange="@{(pos)->obj.onPageSelected(pos)}" />
        </com.example.icemusic.view.SmallLinearLayout>

总结,针对2种不同场景下的滑动冲突问题,解决方法的主旨都是通过优先将触摸事件发送给内部View,然后在内部View不处理事件的情况下,外部View再进行处理。如果出现3或多个RecyclerView嵌套的情况,可以将上述2个壳view的代码组合使用。