RecyclerView+ViewPager+RecyclerView 嵌套滑动实战

为了简化描述,本文使用 VP 指代 ViewPager,RV 指代 RecyclerView,OuterRv 指代最外围的 RecyclerView,innerRV 指代 ViewPager 内部的 RecyclerView数组

1、场景

外层 RV,内部一个楼层嵌套 VP,VP 里面的 Fragment 又使用 RV 布局,主要应用场景是各大零售 APP 首页底部的商品楼层。由于 RV 自己就处理了嵌套滑动,所以 VP 左右滑事件不收影响,在实际使用时,咱们也常常使用 纵向 RV 嵌套横向滑动 RV 这样的应用模式。bash

1.1 问题

VP 在 RV 中做为一个楼层应用时主要有两个问题:ide

  1. 须要主动设置 VP 的高度。若是你使用 MATCH_PARENT 或者WRAP_CONTENT,你根本看不到 VP 楼层,所以其高度计算时被设置为 0。
  2. VP 内部使用了可上下滑动的控件,如 RV,ScrollView 等时,由于内部的可上下滑动控件须要消耗上下滑动这个事件,而外部 RV 也须要消耗该事件,那到底谁去消耗呢?也由此引起滑动冲突问题。

2、实战

2.1 解决问题一

首先能够明确的一点是:ViewPager 是固定高度的。若是是根据内部 RV 的高度来变化,首先你须要复写 RV 的 onMeasure方法来获取内部子 View 的高度,很麻烦,损耗性能,其次有些场景很差计算子View高度,很不方便。布局

那么设置 ViewPager 高度为多少呢?性能

天下 APP 一大抄,页面布局思路类似:底部的商品楼层上面会有一个 Tab 切用来标识当前 VP 在哪一页。Tab 切上滑时可以吸顶,内部商品列表能够滑动。个人计算思路是:优化

VP高度 = 屏幕高度 - statusBar高度 - titleBar 高度 - 底部NavigationBar 高度
复制代码

表面看上去是吸顶,其实就是 OuterRV 划不动了,已经到底部了。this

2.2 解决问题二

嵌套滑动之前的解决方式能够参考:spa

Android 仿京东,淘宝RecyclerView嵌套ViewPager嵌套RecyclerView商品展现code

文章思路不错可是有一个最大缺点就是一直向上滑以后,确实会吸顶,可是要触发一个 ACTION_UP 事件(手指移开),才能将滑动事件传递给 InnerRV,也就是说,这个滑动事件,要不 OuterRV 处理要不 InnerRV 处理。但京东、每日优鲜嵌套滑动却很天然。接口

个人解决思路主要依靠的是 NestedScrollingChildNestedScrollingParent。RV 实现了 NestedScrollingChild2

public interface NestedScrollingChild2 extends NestedScrollingChild 
复制代码

在开始代码实战以前,须要去理解一下这两个支持嵌套滑动的接口。

2.3 撸代码

先上菜:

2.3.1 OuterRecyclerView

class NestedOuterRecyclerView : RecyclerView, NestedScrollingParent2{

    private val mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
    private var mNestedScrollingTarget: View? = null
    private var mNestedScrollingChildView : View? = null
    private var maxHeight : Int = -1
    private var childLocation = IntArray(2)
    private val isDebug = false

    init {
        maxHeight = ScreenUtil.getStatusBarHeight() + 100
    }

    constructor(context: Context?) : super(context!!) {}

    constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {}

    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context!!, attrs, defStyle)

    override fun onNestedScrollAccepted(child: View, target: View, nestedScrollAxes: Int, type: Int) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type)
        mNestedScrollingTarget = target
        mNestedScrollingChildView = child
        if(isDebug) {
            Log.e("dc", "Outer --> onNestedScrollAccepted 》》")
        }
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {

        mNestedScrollingChildView?.let {
            if (target !is NestedInnerRecyclerView){
                return
            }
            it.getLocationOnScreen(childLocation)
            // 若是是向上的
            if (dy >= 0){
                // ViewPager当前所处位置没有在顶端,交由父类去滑动
                if (childLocation[1] > (maxHeight + 5)) {
                    consumed[0] = 0
                    consumed[1] = dy
                    scrollBy(0, dy)
                }
            }
            // 若是是向下的
            else{
                if (childLocation[1] > (maxHeight + 5)){
                    if (!target.canScrollVertically(-1)){
                        consumed[0] = 0
                        consumed[1] = dy
                        scrollBy(0, dy)
                    }
                }else{
                    if (!target.canScrollVertically(-1)){
                        consumed[0] = 0
                        consumed[1] = dy
                        scrollBy(0, dy)
                    }
                }
            }
            if(isDebug) {
                Log.e("dc", "Outer --> onNestedPreScroll 》》【dx=$dx】【dy=$dy】【location[0]=${childLocation[0]}}】【location[1]=${childLocation[1]}】【maxHeight=${maxHeight}】")
            }
        }
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        if(isDebug) {
            Log.e("dc", "Outer --> onStopNestedScroll 》》")
        }
        mNestedScrollingParentHelper.onStopNestedScroll(target, type)
    }

    override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
        if(isDebug) {
            Log.e("dc", "Outer --> onStartNestedScroll 》》")
        }
        return true
    }

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        if(isDebug) {
            Log.e("dc", "Outer --> onNestedScroll 》》【dxConsumed=$dxConsumed】【dyConsumed=$dyConsumed】【dxUnconsumed=$dxUnconsumed】【dyUnconsumed=$dyUnconsumed】")
        }
    }
}
复制代码

2.3.2 InnerRecyclerView

class NestedInnerRecyclerView : RecyclerView {

    private var downX : Float = 0f
    private var downY : Float = 0f

    constructor(context: Context?) : super(context!!) {}

    constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {}

    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context!!, attrs, defStyle) {}

    override fun onTouchEvent(e: MotionEvent): Boolean {
        val x = e.x
        val y = e.y
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                downX = x
                downY = y
                // 必须加上这个,让 RecyclerView 也要处理滑动冲突才行
                parent.requestDisallowInterceptTouchEvent(true)
                Log.e("dc", "inner ACTION_DOWN 》》》")
            }
            MotionEvent.ACTION_MOVE -> {
                val dx: Float? = x.minus(downX)
                val dy: Float? = y.minus(downY)
                //经过距离差判断方向
                val orientation = getOrientation(dx ?: 0f, dy ?: 0f)
                when (orientation) {
                    "r", "l" -> {
                        // 要求左右滑动很大才能触发父类的左右滑动
                        dx?.let {
                            if (abs(dx) > 100){
                                parent.requestDisallowInterceptTouchEvent(false)
                                Log.e("dc", "inner ACTION_MOVE 》》》父类处理")
                                return false
                            }else{
                                parent.requestDisallowInterceptTouchEvent(true)
                            }
                        }
                    }
                    else -> {
                        parent.requestDisallowInterceptTouchEvent(true)
                        Log.e("dc", "inner ACTION_MOVE 》》》子类处理")
                    }
                }
            }
        }
        return super.onTouchEvent(e)
    }

    private fun getOrientation(dx: Float = 0f, dy: Float = 0f): String {
        return if (abs(dx) > abs(dy)) {
            //X轴移动
            if (dx > 0) "r" else "l"//右,左
        } else {
            //Y轴移动
            if (dy > 0) "b" else "t"//下//上
        }
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean {
        Log.e("dc", "Inner --> dispatchNestedScroll1[dxConsumed=$dxConsumed][dyConsumed=$dyConsumed][dxUnconsumed=$dxUnconsumed][dyUnconsumed=$dyUnconsumed][offsetInWindow=[$offsetInWindow]]")
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int): Boolean {
        Log.e("dc", "Inner --> dispatchNestedScroll2[dxConsumed=$dxConsumed][dyConsumed=$dyConsumed][dxUnconsumed=$dxUnconsumed][dyUnconsumed=$dyUnconsumed][offsetInWindow=[$offsetInWindow]][type=$type]")
        return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type)
    }

    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        Log.e("dc", "Inner --> dispatchNestedPreFling[velocityX=$velocityX][velocityY=$velocityY]")
        return super.dispatchNestedPreFling(velocityX, velocityY)
    }

    override fun onStartNestedScroll(child: View?, target: View?, nestedScrollAxes: Int): Boolean {
        Log.e("dc", "Inner --> onStartNestedScroll[nestedScrollAxes=$nestedScrollAxes]")
        return super.onStartNestedScroll(child, target, nestedScrollAxes)
    }

    override fun onStopNestedScroll(child: View?) {
        Log.e("dc", "Inner --> onStopNestedScroll")
        super.onStopNestedScroll(child)
    }
}
复制代码

2.4 讲解

若是理解了 NestedScrollingParent 以后,我这段代码就很简单啦,讲一个注意点:

  1. InnerRecyclerView.OnTouchEvent()
parent.requestDisallowInterceptTouchEvent(true)
复制代码

true:表明父类不拦截,交由子类优先处理,因为事件是一层一层传递的,理论上任何一环处理了事件,其余层就不会处理了,直到下一次事件的到来。一旦设置为 false,VP 不会再处理左右滑动事件了,由于代码设置了 InnerRV 须要处理滑动事件,因此须要在恰当时机从新交由 VP 处理。

3、优化

在实际操做中会发现滑动过于灵敏的问题,这里主要是手指移动多长,RV 就移动多少,解决思路是在onNestedPreScroll适当消耗一点滑动,好比在 consume 数组里提早消耗一丢丢。

相关文章
相关标签/搜索