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 數組裏提早消耗一丟丟。

相關文章
相關標籤/搜索