BehaviorScrollView 幫你解決各類嵌套滾動問題

一、簡介

以前在仿寫豆瓣詳情頁,以及平常的一些涉及嵌套滾動的需求時,每次都須要新增自定義 View 來實現,而在 touch 事件的攔截和處理,滾動和 fling 的處理上,又有着很大的共性,爲了減小以後處理相似需求的重複勞動,也爲了更進一步學習 Android 提供的嵌套滾動框架,因而打造了 BehaviorScrollView 來解決嵌套滾動的共性問題。java

BehaviorScrollView 內部實現了對 touch 事件、嵌套滾動和 fling 的攔截和處理的通用邏輯,實現了 NestedScrollingParent3NestedScrollingChild3 接口,支持多級嵌套(Demo 中會有一個四層嵌套的示例),支持水平和垂直方向滾動,外部能夠經過實現 NestedScrollBehavior 接口來支持不一樣的嵌套滾動需求。git

二、嵌套滾動處理流程

在講 BehaviorScrollViewNestedScrollBehavior 怎麼使用以前,仍是有必要介紹下 Android 是怎麼處理嵌套滾動的,這部分的文章就不少了,這裏只作簡要介紹。github

  1. 分發 touch 事件,父一級的 View 儘量不攔截事件,一直向下分發,直到找處處理事件的 Child
  2. Child 處理 touch 事件,在手指滑動時產生滾動量,開始滾動自身內容
  3. Child 在處理滾動量以前,要告訴父 View 本身要開始滾動了 pre-scroll,並一級一級的向上分發
  4. Child 此時須要計算下父級 View 還有多少滾動量沒有消耗,而後開始滾動本身,並計算本身消耗了多少滾動量
  5. Child 處理完本身後,將滾動量的消耗狀況向父 View 分發 after-scroll

咱們平時要處理的嵌套滾動問題也是 GrandparentParentChild 三種角色中的一個或多個的組合,接下來分別介紹下這三種角色分別要處理那些問題。數組

Grandparent 只須要處理子 View 的嵌套滾動事件,實現 NestedScrollingParent (後綴的 二、3 是爲了兼容更多狀況進行的擴展)接口,而後根據自身需求在滾動前 pre-scroll 或 滾動後 after-scroll,執行本身的操做。這種類型的 View 有不少,好比 NestedScrollViewCoordinatorLayoutSwipeRefreshLayout 等,咱們平時須要的大多數嵌套滾動需求只須要處理子 View 分發的滾動,也是這種狀況。框架

Child 通常只負責處理 touch 事件,並將產生的滾動量向父 View 分發,實現 NestedScrollingChild 接口,在本身處理滾動前分發 pre-scroll,本身處理後分發 after-scroll。這種 View 都是些可以產生滾動的 View,好比 RecyclerViewNestedScrollView 等。ide

Parent 相對比較複雜,即負責接收子 View 的嵌套滾動事件,還須要將其分發給本身的父 View,實現 NestedScrollingParentNestedScrollingChild 接口(即當兒子有當爹),一般狀況下還須要處理 touch 事件和 fling、動畫等。常見的有 NestedScrollViewSwipeRefreshLayout 等,本文介紹的 BehaviorScrollView 就屬於此類。函數

同方向嵌套滾動最核心的問題是 優先級 問題,手指滑動時父 View 能夠處理,子 View 也能夠處理,那到底須要交給誰呢。好比常見的 SwipeRefreshLayout 嵌套 RecyclerView。在嵌套滾動的流程中,Parent 收到 Childpre-scroll 時,須要決定本身是否要處理,還要決定是先分發給 Grandparent 而後本身處理,仍是先本身處理,再分發給 Grandparent佈局

固然,BehaviorScrollView 是不會幫你決定這些優先級的,它負責處理優先級以外的滾動量計算和分發,以及通用的 touch 事件、fling 和動畫的處理,從而是咱們可以更加方便地處理優先級問題。學習

三、使用和原理

BehaviorScrollView 的使用主要是經過 setupBehavior 方法設置不一樣的 NestedScrollBehavior,從而實現不一樣的優先級策略。這裏就從 NestedScrollBehavior 開始,介紹它在嵌套滾動各個階段發揮的做用。動畫

interface NestedScrollBehavior {
    /** * 當前的可滾動方向 */
    @ViewCompat.ScrollAxis
    val scrollAxis: Int

    val prevView: View?
    val midView: View
    val nextView: View?

    /** * 在 layout 以後的回調 * * @param v */
    fun afterLayout(v: BehavioralScrollView) {
        // do nothing
    }

    /** * 在 [v] dispatchTouchEvent 時是否處理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 處理,會在 dispatchTouchEvent 中直接返回 true,false -> 直接返回 false,null -> 不關心,會執行默認邏輯 */
    fun handleDispatchTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null

    /** * 在 [v] onTouchEvent 時是否處理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 處理,會直接返回 true,false -> 不處理,會直接返回 false,null -> 不關心,會執行默認邏輯 */
    fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null

    /** * 在 onNestedPreScroll 時,是否優先本身處理 * * @param v * @param scroll 滾動量 * @param type 滾動類型 * @return true -> 本身優先,false -> 本身不優先,null -> 不處理 onNestedPreScroll */
    fun handleNestedPreScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null

    /** * 在 onNestedScroll 時,是否優先本身處理 * * @param v * @param scroll 滾動量 * @param type 滾動類型 * @return true -> 本身優先,false -> 本身不優先,null -> 不處理 onNestedPreScroll */
    fun handleNestedScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null

    /** * 在須要 [v] 自身滾動時,是否須要處理 * * @param v * @param scroll 滾動量 * @param type 滾動類型 * @return 是否處理自身滾動,true -> 處理,false -> 不處理,null -> 不關心,會執行默認自身滾動 */
    fun handleScrollSelf(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null

}
複製代碼

3.1 佈局

NestedScrollBehavior 提供的 scrollAxis 決定了 BehavioralScrollView 要處理的滾動方向,同時也會決定佈局的方向。

BehavioralScrollView 的子 View 是由 NestedScrollBehavior 提供的 prevViewmidViewnextView,會在 onLayout 時造成水平或垂直線性佈局。具體來講,BehavioralScrollView 是繼承自 FrameLayout 的,在垂直佈局的狀況下,midView 的位置不變,prevView 會移動它的上面,nextView 移動到其下面,從而使得 BehavioralScrollView 有一個能夠上下滾動的範圍。佈局完成後會計算滾動範圍,從 preView.topnextView.bottom,而且回調 NestedScrollBehavior.afterLayout 方法。

private fun layoutVertical() {
    // midView 位置不變
    val t = midView?.top ?: 0
    val b = midView?.bottom ?: 0
    // prevView 移動到 midView 之上,bottom 和 midView 的 top 對齊
    prevView?.also {
        it.offsetTopAndBottom(t - it.bottom)
        minScroll = it.top
    }
    // nextView 移動到 midView 之下,top 和 midView 的 bottom 對齊
    nextView?.also {
        it.offsetTopAndBottom(b - it.top)
        maxScroll = it.bottom - height
    }
}
複製代碼

這裏爲何用三個 View 而不是兩個或着更多呢?一方面在我涉及到的場合下,三個 View 足夠用了,實在不夠還能夠嵌套,另外一方面,三個 View 可以比較方便地控制一些邊界條件。好比在垂直滾動狀況下,會在 scrollY == 0 的邊界處作一些判斷,調整嵌套滾動的優先級策略,判斷 scrollY 是大於 0 仍是小於 0,從而判斷是 nextView 滾動出來仍是 prevView 滾動出來了。若是增長到了四個以上,這種邊界的判斷就會變得很麻煩。

3.2 Touch 事件處理

首先看下 dispatchTouchEvent,會先回調 NestedScrollBehavior.handleDispatchTouchEvent,返回非空的值表示 NestedScrollBehavior 已經處理了,會直接返回,空的話會在 ACTION_DOWN 時復位一些標誌位,無特殊處理。

這裏回調給 NestedScrollBehavior 是爲了能夠儘早拿到 touch 事件,這裏一般會在 ACTION_UP 擡手時作一些動畫或復位。

override fun dispatchTouchEvent(e: MotionEvent): Boolean {
    // behavior 優先處理,不處理走默認邏輯
    behavior?.handleDispatchTouchEvent(this, e)?.also {
        log("handleDispatchTouchEvent $it")
        return it
    }
    // 在 down 時復位一些標誌位,停掉 scroller 的動畫
    if (e.action == MotionEvent.ACTION_DOWN) {
        lastScrollDir = 0
        state = NestedScrollState.NONE
        scroller.abortAnimation()
    }
    return super.dispatchTouchEvent(e)
}
複製代碼

onInterceptTouchEvent 沒有回調給 NestedScrollBehavior,這裏就不貼代碼了,主要邏輯是隻有手指在滾動方向上發生了滑動,且觸點位置沒有能夠處理嵌套滑動的 NestedScrollingChild 纔去攔截事件本身處理。

onTouchEvent 也會優先分發給 NestedScrollBehavior.handleTouchEvent 處理,默認會 ACTION_MOVE 時計算滾動量,並經過 dispatchScrollInternal(這個方法後面再講)進行分發,在 ACTION_UP 時進行 fling 的處理。

override fun onTouchEvent(e: MotionEvent): Boolean {
    // behavior 優先處理,不處理時本身處理 touch 事件
    behavior?.handleTouchEvent(this, e)?.also {
        return it
    }
    when (e.action) {
        MotionEvent.ACTION_DOWN -> {
            // ...
        }
        MotionEvent.ACTION_MOVE -> {
            // ...
            dispatchScrollInternal(dx, dy, ViewCompat.TYPE_TOUCH)
        }
        MotionEvent.ACTION_UP -> {
            // ...
            if (!dispatchNestedPreFling(vx, vy)) {
                dispatchNestedFling(vx, vy, true)
                fling(vx, vy)
            }
        }
    }
    // ...
}
複製代碼

3.3 嵌套滾動事件處理

BehavioralScrollView 實現的 NestedScrollingParent3NestedScrollingChild3 的大多數方法都不須要咱們作什麼特殊處理,用 NestedScrollingParentHelperNestedScrollingChildHelper 兩個幫助類就能解決,能夠多參考 NestedScrollView。這裏主要介紹做爲 Grandparent 角色的 onNestedPreScrollonNestedScroll 兩個方法,顧名思義,對應上面說的 pre-scrollafter-scroll 兩個時機。

onNestedPreScroll 會有兩個重載的方法,第二個比第一個多了 NestedScrollType 參數用以區分滾動是不是 touch 事件產生的,這裏統一回調到 dispatchNestedPreScrollInternal 處理。

代碼邏輯比較簡單,首先時回調 NestedScrollBehavior.handleNestedPreScrollFirst 判斷處理的優先級,返回值有三種狀況:

  1. null:表示不處理 pre-scroll,這時會直接調用 dispatchNestedPreScroll 將滾動量分發給父 View
  2. true:表示本身優先處理,這時會先調用 handleScrollSelf(這個方法後面再講)本身處理,而後計算未消耗的滾動量,再 dispatchNestedPreScroll 分發給父 View
  3. false:表示父 View 優先處理,這時會先 dispatchNestedPreScroll 分發給父 View,而後計算未消耗的滾動量,再 handleScrollSelf 本身處理
/** * 分發 pre scroll 的滾動量 */
private fun dispatchNestedPreScrollInternal( dx: Int, dy: Int, consumed: IntArray, type: Int = ViewCompat.TYPE_TOUCH ) {
    when (scrollAxis) {
        ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
            // ...
        }
        ViewCompat.SCROLL_AXIS_VERTICAL ->{
            val handleFirst = behavior?.handleNestedPreScrollFirst(this, dy, type)
            when (handleFirst) {
                true -> {
                    val selfConsumed = handleScrollSelf(dy, type)
                    dispatchNestedPreScroll(dx, dy - selfConsumed, consumed, null, type)
                    consumed[1] += selfConsumed
                }
                false -> {
                    dispatchNestedPreScroll(dx, dy, consumed, null, type)
                    val selfConsumed = handleScrollSelf(dy - consumed[1], type)
                    consumed[1] += selfConsumed
                }
                null -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
            }
        }
        else -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
    }
}
複製代碼

onNestedScroll 會有三個重載方法,依次增長了 NestedScrollType 和父 View 用於記錄消耗滾動量的數組 consumed,這裏會統一回調給 dispatchNestedScrollInternal 處理。

處理邏輯和 dispatchNestedPreScrollInternal 相似,先回調 NestedScrollBehavior.handleNestedScrollFirst 獲得優先級,再進行分發和處理,這裏再也不贅述。

/** * 分發 nested scroll 的滾動量 */
private fun dispatchNestedScrollInternal( dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int = ViewCompat.TYPE_TOUCH, consumed: IntArray = intArrayOf(0, 0) ) {
    when (scrollAxis) {
        ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
            // ...
        }
        ViewCompat.SCROLL_AXIS_VERTICAL -> {
            val handleFirst = behavior?.handleNestedScrollFirst(this, dyUnconsumed, type)
            when (handleFirst) {
                true -> {
                    val selfConsumed = handleScrollSelf(dyUnconsumed, type)
                    dispatchNestedScroll(dxConsumed, dyConsumed + selfConsumed, dxUnconsumed, dyUnconsumed - selfConsumed, null, type, consumed)
                    consumed[1] += selfConsumed
                }
                false -> {
                    dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
                    val selfConsumed = handleScrollSelf(dyUnconsumed - consumed[1], type)
                    consumed[1] += selfConsumed
                }
                null -> dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
            }
        }
    }
}
複製代碼

NestedScrollBehavior 對滾動量分發的優先級控制主要體如今 handleNestedPreScrollFirsthandleNestedScrollFirst 兩個方法,經過 BehavioralScrollView 當前狀態、滾動的距離、滾動類型和不一樣策略設置不一樣的優先級,從而知足不一樣嵌套滾動需求。

3.4 自身滾動的處理

dispatchScrollInternal 用來處理自身產生的來自 touch 事件或者 fling 的滾動量,這裏實際上是處於 Child 的角色,因此在自身處理的先後都要分發嵌套滾動事件,這裏複用了前面的 dispatchNestedPreScrollInternaldispatchNestedScrollInternal,在自身滾動時實現精細的優先級控制。

/** * 分發來自自身 touch 事件或 fling 的滾動量 * -> dispatchNestedPreScrollInternal * -> handleScrollSelf * -> dispatchNestedScrollInternal */
private fun dispatchScrollInternal(dx: Int, dy: Int, type: Int) {
    val consumed = IntArray(2)
    when (scrollAxis) {
        ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
            // ...
        }
        ViewCompat.SCROLL_AXIS_VERTICAL -> {
            var consumedY = 0
            dispatchNestedPreScrollInternal(dx, dy, consumed, type)
            consumedY += consumed[1]
            consumedY += handleScrollSelf(dy - consumedY, type)
            val consumedX = consumed[0]
            // 複用數組
            consumed[0] = 0
            consumed[1] = 0
            dispatchNestedScrollInternal(consumedX, consumedY, dx - consumedX, dy - consumedY, type, consumed)
        }
    }
}
複製代碼

handleScrollSelf 是真正到了自身滾動的時刻,會先回調 NestedScrollBehavior.handleScrollSelf 判斷是否處理該滾動量,一樣的有三種返回值:

  1. null 表示 NestedScrollBehavior 不作特殊處理,此時 BehavioralScrollView 會根據自身是否能夠滾動進行滾動,並返回消耗的滾動量
  2. true 表示處理,消耗全部的滾動量
  3. false 表示不處理,不消耗滾動量

handleScrollSelf 主要用於在 BehavioralScrollView 自身滾動時作特殊處理,好比下拉刷新等不但願 fling 的 ViewCompat.TYPE_NON_TOUCH 類型滾動形成自身的位移,有些彈性滾動的場合但願自身的滾動帶有阻尼效果等均可以在這裏處理。

/** * 處理自身滾動 */
private fun handleScrollSelf(scroll: Int, @ViewCompat.NestedScrollType type: Int): Int {
    // behavior 優先決定是否滾動自身
    val handle = behavior?.handleScrollSelf(this, scroll, type)
    val consumed = when(handle) {
        true -> scroll
        false -> 0
        else -> if (canScrollSelf(scroll)) {
            scrollBy(scroll, scroll)
            scroll
        } else {
            0
        }
    }
    return consumed
}
複製代碼

自身的滾動最終是經過 scrollBy 實現的,經過 getScrollByX/getScrollByY 實現了邊界控制。同時 scrollX/scrollY 在 0 處作了特殊處理,如 scrollY > 0 時,滾動範圍是 從 0 到 maxScroll,這和「3.1 佈局」中說的邊界處的特殊處理有關,須要在 scrollY 小於 0、等於 0 或大於 0 時使用不用的優先級策略。

override fun scrollBy(x: Int, y: Int) {
    val xx = getScrollByX(x)
    val yy = getScrollByY(y)
    super.scrollBy(xx, yy)
}

/** * 根據方向計算 y 軸的真正滾動量 */
private fun getScrollByY(dy: Int): Int {
    val newY = scrollY + dy
    return when {
        scrollAxis != ViewCompat.SCROLL_AXIS_VERTICAL -> scrollY
        scrollY > 0 -> newY.constrains(0, maxScroll)
        scrollY < 0 -> newY.constrains(minScroll, 0)
        else -> newY.constrains(minScroll, maxScroll)
    } - scrollY
}
複製代碼

3.5 fling 和動畫

fling 和動畫都是經過 Scroller 處理的,fling 須要 VelocityTracker 幫助類在 touch 事件中記錄手指移動速度。

這裏須要介紹 BehavioralScrollView 保存當前狀態的一個屬性 NestedScrollState,方便嵌套滾動事件的優先級判斷。

/** * 用於描述 [BehavioralScrollView] 正處於的嵌套滾動狀態,和滾動類型 [ViewCompat.NestedScrollType] 共同描述滾動量 */
@IntDef(NestedScrollState.NONE, NestedScrollState.DRAGGING, NestedScrollState.ANIMATION, NestedScrollState.FLING)
@Retention(AnnotationRetention.SOURCE)
annotation class NestedScrollState {
    companion object {
        /** * 無狀態 */
        const val NONE = 0
        /** * 正在拖拽 */
        const val DRAGGING = 1
        /** * 正在動畫,動畫產生的滾動不會被分發 */
        const val ANIMATION = 2
        /** * 正在 fling */
        const val FLING = 3
    }
}
複製代碼

fling 和動畫最終都會回調到 computeScroll 中處理,不一樣的是動畫產生的滾動不須要進行分發(由於動畫不是 touch 事件產生的,而是外部明確調用的),而 fling 的須要 dispatchScrollInternal 進行分發。

override fun computeScroll() {
    when {
        scroller.computeScrollOffset() -> {
            val dx = (scroller.currX - lastX).toInt()
            val dy = (scroller.currY - lastY).toInt()
            lastX = scroller.currX.toFloat()
            lastY = scroller.currY.toFloat()
            // 不分發來自動畫的滾動
            if (state == NestedScrollState.ANIMATION) {
                scrollBy(dx, dy)
            } else {
                dispatchScrollInternal(dx, dy, ViewCompat.TYPE_NON_TOUCH)
            }
            invalidate()
        }
        // ...
    }
}
複製代碼

四、示例

BehavioralScrollView 已經處理了共性的東西,個性化的部分是 NestedScrollBehavior 實現的,所以這裏的示例可能不具有通用性。當有特殊須要是,能夠很方便地自定義 NestedScrollBehavior 實現,這也正是 BehavioralScrollView 但願達到的效果。

這裏以底部浮層 BottomSheetBehavior 爲例大體介紹下 NestedScrollBehavior 的使用。

構造 BottomSheetBehavior 須要知道內容視圖 contentView 以及浮層彈出的範圍和初始位置。

class BottomSheetBehavior(
    /**
     * 浮層的內容視圖
     */
    contentView: View,
    /**
     * 初始位置,最低高度 [POSITION_MIN]、中間高度 [POSITION_MID] 或最大高度 [POSITION_MAX]
     */
    private val initPosition: Int,
    /**
     * 內容視圖的最低顯示高度
     */
    private val minHeight: Int,
    /**
     * 內容視圖中間停留的顯示高度,默認等於最低高度
     */
    private val midHeight: Int = minHeight
)
複製代碼

因爲滾動範圍是由 prevViewmidViewnextView 肯定的,頂部的空白區域須要設置 prevView 進行佔位,經過 topMargin 控制其高度,從而控制滾動的範圍,midView 設置爲 contentView,這裏不須要 nextView 設爲 null

/** * 用於控制滾動範圍 */
override val prevView: View? = Space(contentView.context).also {
    val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    lp.topMargin = minHeight
    it.layoutParams = lp
}

override val midView: View = contentView
override val nextView: View? = null
複製代碼

afterLayout 時計算中間高度時 scrollY 的值,並在第一次 layout 後直接滾動指定的初始位置。

override fun afterLayout(v: BehavioralScrollView) {
        // 計算中間高度時的 scrollY
        midScroll = v.minScroll + midHeight - minHeight
        // 第一次 layout 滾動到初始位置
        if (firstLayout) {
            firstLayout = false
            v.scrollTo(
                v.scrollX,
                when (initPosition) {
                    POSITION_MIN -> v.minScroll
                    POSITION_MAX -> v.maxScroll
                    else -> midScroll
                }
            )
        }
    }
複製代碼

簡單畫了下佈局的示意圖

handleDispatchTouchEvent 的 up 或 cancel 時,須要根據當前滾動位置和上次滾動的方向,決定動畫的目標位置。

override fun handleDispatchTouchEvent( v: BehavioralScrollView, e: MotionEvent ): Boolean? {
    if ((e.action == MotionEvent.ACTION_CANCEL || e.action == MotionEvent.ACTION_UP)
        && v.scrollY != 0) {
        // 在 up 或 cancel 時,根據當前滾動位置和上次滾動的方向,決定動畫的目標位置
        v.smoothScrollTo(
            if (v.scrollY > midScroll) {
                if (v.lastScrollDir > 0) { v.maxScroll } else { midScroll }
            } else {
                if (v.lastScrollDir > 0) { midScroll } else { v.minScroll }
            }
        )
        return true
    }
    return super.handleDispatchTouchEvent(v, e)
}
複製代碼

handleTouchEvent 須要在 down 在 prevView 時不進行處理,由於它只是個佔位的,這樣不會影響下層視圖對事件的處理。

override fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? {
    // down 事件觸點在 prevView 上時不作處理
    return if (e.action == MotionEvent.ACTION_DOWN && prevView?.isUnder(e.rawX, e.rawY) == true) {
        false
    } else {
        null
    }
}
複製代碼

嵌套滾動的優先級處理比較簡單,handleNestedPreScrollFirst 只在 contentView 沒有徹底展開,即 v.scrollY != 0 時處理,而 handleNestedScrollFirst 老是優先處理。

override fun handleNestedPreScrollFirst( v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int ): Boolean? {
    // 只要 contentView 沒有徹底展開,就在子 View 滾動前處理
    return if (v.scrollY != 0) { true } else { null }
}

override fun handleNestedScrollFirst( v: BehavioralScrollView, scroll: Int, type: Int ): Boolean? {
    return true
}
複製代碼

自身的滾動只處理 touch 類型的,其餘的過濾掉。

override fun handleScrollSelf( v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int ): Boolean? {
    // 只容許 touch 類型用於自身的滾動
    return if (type == ViewCompat.TYPE_NON_TOUCH) { true } else { null }
}
複製代碼

Demo 中還有其餘各類類型的 NestedScrollBehavior,如實現頂部 TabLayout 懸浮效果的 FloatingHeaderBehavior,兼容嵌套的下拉刷新 SwipeRefreshBehavior 等。這裏簡單說明下爲何 SwipeRefreshLayout 已經實現了 NestedScrollingParentNestedScrollingChild,卻沒法適用於嵌套滾動呢?

NestedScrollingChild.dispatchNestedScroll 缺乏 NestedScrollingChild3.dispatchNestedScroll 中的 consumed 參數,因此在向父 View 分發時,沒法得知父 View 消耗了多少滾動量,嵌套使用就會存在問題,來看下 SwipeRefreshLayout.onNestedScroll 方法。

public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) {
    // Dispatch up to the nested parent first
    dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
    // ...
    final int dy = dyUnconsumed + mParentOffsetInWindow[1];
    if (dy < 0 && !canChildScrollUp()) {
        mTotalUnconsumed += Math.abs(dy);
        moveSpinner(mTotalUnconsumed);
    }
}
複製代碼

它在 dispatchNestedScroll 以後是不知道父 View 有沒有消耗滾動量的,而函數中的 mParentOffsetInWindow 獲得的是 SwipeRefreshLayout 在屏幕上的位移,SwipeRefreshLayout 認爲的父 View 沒有消耗的滾動量等於 dyUnconsumed + mParentOffsetInWindow[1]

這樣看起來沒啥問題,但當父 View 消耗的滾動量不等於其子 View 在屏幕上的位移時(好比增長了阻尼效果,消耗了 n 的滾動量,卻只移動了 n/2)就會出問題,即便滾動量已經所有被外部消耗了,SwipeRefreshLayout 仍是有下拉效果:

因此爲了解決這種問題,就須要實現了 NestedScrollingChild3 的接口,下面是 BehavioralScrollView + SwipeRefreshBehavior 的效果:

五、結束

嵌套滾動的核心問題是優先級問題,咱們應該專一於優先級的策略而不是各類事件的處理和分發問題,這也真是 BehavioralScrollView 在嘗試作到的,但願這篇文章可以對你有所幫助,有不一樣思路的也歡迎相互探討。

github.com/funnywolfda…

相關文章
相關標籤/搜索