Android - 手把手教你寫出一個支持嵌套滑動的View 1. 說說嵌套滑動 2. 準備工作 3. 支持單指滑動 4. 支持多指滑動 5. 支持Fling滑動 6. 總結

  嵌套滑動機制,想必大家都不陌生,當我們在使用CoordinatorLayout + AppBarLayout框架設計界面,嵌套滑動就顯得尤爲地重要。CoordinatorLayout成爲協調佈局,目的是協調多個佈局的聯動,聯動就會涉及多個View在滑動時候相互的響應,簡單來說,就是一個View在滑動的時候,另一個View可能需要對應的滑動。那麼這種聯動是怎麼實現的呢?換句話說,View是怎麼知道其他View在滑動呢?有人可能說,是Behavior在進行協調。Behavior畢竟是設計CoordinatorLayout實現出來的東西,不能用於任何View,也就是說,Behavior衆多方法的回調還得依賴View的某些底層機制來實現,那麼這個底層機制是什麼呢?那就是嵌套滑動機制。
  回過頭來看一下標題,本文目的是介紹怎麼自定義一個可以產生嵌套滑動的View。那麼既然官方提供了衆多可以支持嵌套滑動的View,爲啥我們還要自己定義呢?自然是官方的不能滿足我們的要求,這也是從我工作中得來教訓。最近,我在負責界面的改版,新界面的交互逼得我不得不使用CoordinatorLayout + AppBarLayout進行開發。當我在開發某一個模塊時,發現需要使用一個支持嵌套滑動的View。最初的想法是使用NestedScrollView套一下,但是 NestedScrollView會把Child給攤平,性能問題自然就會出現了。所以爲了追求極致,就自己定義一個可以支持嵌套滑動的View。
  在閱讀本文之前,需要準備知識:

  1. CoordinatorLayout 的實現原理。
  2. 嵌套滑動實現的原理。

  本文不會深入分析上面兩部分知識,所以我默認大家都瞭解,有興趣的同學可以參考如下文章:

  1. Android 源碼分析 - 嵌套滑動機制的實現原理
  2. CoordinatorLayout 學習(一) - CoordinatorLayout的基本使用
  3. CoordinatorLayout 學習(二) - RecyclerView和AppBarLayout的聯動分析
  4. 從一次真實經歷中說說使用嵌套滑動過程中常見的坑

1. 說說嵌套滑動

  嵌套滑動機制在API 21 之後就跟View之後綁定之後,Google爸爸在官方庫裏面提供了支持嵌套滑動的View,而這部分View可以分爲兩類:

  1. 產生嵌套滑動事件的View:這類View前提上是自己本身可以滑動,如果自己都不能滑動,那嵌套滑動什麼的都是白扯。比如說,RecyclerView,NestedScrollView之類,主要是實現NestedScrollingChild、NestedScrollingChild2、NestedScrollingChild3這三個接口的View。(至於這三個接口有啥區別,後文我會分析)
  2. 處理嵌套滑動事件的View:這類View都會實現NestedScrollingParent、NestedScrollingParent2、NestedScrollingParent3這三個接口中的任意一個。比如說比如CoordinatorLayout、NestedScrollView、SwipeRefreshLayout之類。

  通常來說,在嵌套滑動機制中,這兩類的View都是成對出現的,一般是產生嵌套滑動事件的View作爲處理嵌套滑動事件的View的子View,從另一個方面來說,處理嵌套滑動事件的View一般都是ViewGroup,而產生嵌套滑動事件的View可能是任意View的子類。同時,這兩類View如果只出現一個,嵌套滑動也會失效。
  從上面舉的例子中,我們可以發現,NestedScrollView同時實現了NestedScrollingChild3、NestedScrollingParent3這兩個接口,那麼就表示這個View同時可以產生嵌套滑動和處理嵌套滑動。這也是爲什麼現在有一個NestedScrollView 套RecyclerView的實現方案。而我本人不推薦此方案,因爲NestedScrollView會攤平內部所有的Child,這就意味着RecyclerView會衆多特性就失效。這也是本文寫作的原因,本文的目的是給大家介紹怎樣自定義一個產生嵌套滑動事件View
  在正式介紹之前,我先給大家分析一下NestedScrollingChildX、NestedScrollingParentX之間的區別。
  NestedScrollingChildX之間的區別,直接來看他們的類圖關係:


  我來分析這圖中的重點:

  1. NestedScrollingChild:這個接口主要定義了嵌套滑動需要的幾個關鍵方法,包括preScroll、scroll、preFling、fling等方法。
  2. NestedScrollingChild2:這個接口是NestedScrollingChild的子接口,在原有的方法基礎上增加type參數,用來判斷TOUCH和非TOUCH的情況,用來區分手指是否還在屏幕上
  3. NestedScrollingChild3:這個接口是NestedScrollingChild2的子接口,主要是重載了dispatchNestedScroll,在原有的接觸上增加了一個consumed 參數。

  我相信大家能區分出來1和2之間的區別,但是3就增加了一個consumed 參數,這是爲何呢?很明顯,這個是用來標記父View消費了多少距離,這個有啥作用呢?主要是在調用了dispatchNestedScroll之後,如果還有未消費的距離,子View就可以停掉滑動。這樣能解決很多奇怪的問題,比如說,我們在Fling RecyclerView到邊界時,觸發了加載更多,理論上應當停掉Fling,但事實上當使用RecyclerView的嵌套滑動時,加載更多完成時會繼續Fling,這就是Fling沒有停掉的原因。不過,這個問題的解決方案需要NestedScrollingChild3 配合NestedScrollingParent3纔會有效。
  我們繼續來看NestedScrollingParentX之間的類圖關係:


  他們之間的區別跟NestedScrollingChild之間的類似,這裏就不贅述了。不過大家需要注意的是,儘量都實現NestedScrollingChild3和NestedScrollingParent3,因爲這兩個接口方法是最全的,同時最好是將全部的方法都是實現一遍,因爲在某些手機可能會拋出AbstractMethodError異常,特別是在21以下的手機上。

2. 準備工作

  前面對嵌套滑動介紹的差不多了,現在我來介紹怎麼定義一個嵌套滑動的View。步驟主要分爲4步:

  1. 指定的View實現了NestedScrollingChild3接口,同時實現相關方法,同時使用NestedScrollingChildHelper來分發嵌套滑動。並且調用setNestedScrollingEnabled,設置爲true,表示該View能夠產生嵌套滑動的事件,這一點非常的重要。
  2. 在第一步的基礎上,先支持單指的嵌套滑動。
  3. 在第二步的基礎上,實現多指的嵌套滑動。
  4. 在第三步的基礎上,實現Fling的嵌套滑動。

  注意,本文使用的是CoordinatorLayout 來處理嵌套滑動
  我們先來看看具體的效果:

  圖中的CustomNestedViewGroup就是本文要實現的View。同時介於第一步比較簡單,本文就不介紹具體的操作。
  本文源碼地址:NestedScrollActivity,有興趣的同學可以參考一下。本文的實現代碼主要參考於NestedScrollView

3. 支持單指滑動

  單指滑動非常的簡單,無非就是在ACTION_MOVE的時機上來觸發滑動而已。但是這種事情看上去簡單,實際上在開發過程中有很多的細節得需要我們注意,正所謂書上得來終覺淺,絕知此事要躬行。不親身去嘗試着寫,理論永遠是理論。
  好了,廢話扯得有點多,我們正式開始介紹吧。需要一個View支持單指滑動,基本框架就是要重寫onInterceptTouchEventonTouchEvent這兩個方法(當然你是繼承View類,便不用重寫onInterceptTouchEvent方法)。所以,我們分別來看一下這個兩個方法的實現。

(1). onInterceptTouchEvent方法

  重寫onInterceptTouchEvent方法的目的是在合適的時機攔截事件,表示我們的View需要消費後續的事件。我們直接來看onInterceptTouchEvent方法的實現:

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        val action = ev.actionMasked
        if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
            return true
        }

        when (action) {
            MotionEvent.ACTION_DOWN -> {
                mLastMotionY = ev.y.toInt()
                // 開始建立嵌套滑動傳遞鏈
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)

            }
            MotionEvent.ACTION_MOVE -> {
                val y = ev.y.toInt()
                val deltaY = abs(y - mLastMotionY)
                if (deltaY > mTouchSlop) {
                    mIsBeingDragged = true
                    mNestedYOffset = 0
                    mLastMotionY = y
                    parent?.let {
                        parent.requestDisallowInterceptTouchEvent(true)
                    }
                }
            }
            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                mIsBeingDragged = false
                // 切斷嵌套滑動的傳遞鏈
                stopNestedScroll(ViewCompat.TYPE_TOUCH)
            }
        }
        return mIsBeingDragged
    }

  onInterceptTouchEvent的實現非常簡單,我在這裏重點的分析幾點:

  1. 我們在ACTION_DOWN調用了startNestedScroll方法,表示建立起嵌套滑動的傳遞鏈,需要特別注意的是,這裏的Type傳遞的是ViewCompat.TYPE_TOUCH,主要是爲了區分後續的Fling滑動;其次,我們在ACTION_CANCELACTION_UP調用了stopNestedScroll,表示切斷嵌套滑動的傳遞鏈。
  2. ACTION_MOVE裏面嘗試設置mIsBeingDragged,從而攔截事件進行消費。

  從onInterceptTouchEvent方法,我們可以看出一個特點,這個方法不會消費move事件,而只是在這個時機設置某些狀態值,比如說:

mIsBeingDragged :用來表示當前是否需要消費時機,我們可以看到,只要滑動距離超過mTouchSlop ,就要進行消費。
mLastMotionY:用來記錄上一次event的Y座標,主要用於計算當前event相比於上一次event,產生多少的滑動距離。
mNestedYOffset:用以記錄產生的滑動距離,被父View消費了多少。這個變量怎麼來理解呢?在我們這個案例中,假設View產生了100px的滑動距離,如果View和AppBarLayout整體上移了50px的距離,那麼mNestedYOffset就爲50。這個變量非常的重要,後續計算mLastMotionY,以及UP的時候Fling的初速度,都需要它。

  既然onInterceptTouchEvent方法不消費事件,那麼在哪裏消費事件呢?自然是onTouchEvent方法。

(2). onTouchEvent方法

  當View內部沒有child消費的事件,,或者被onInterceptTouchEvent攔截的事件,都會傳遞到onTouchEvent方法裏面。而onTouchEvent方法的作用自然是消費事件,要觸發View內部內容進行滑動,就是在該方法裏面實現。
  我們直接來看onTouchEvent方法的實現:

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val action = event.actionMasked
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0
        }
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                mLastMotionY = event.y.toInt()
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
            }
            MotionEvent.ACTION_MOVE -> {
                val y = event.y.toInt()
                var deltaY = mLastMotionY - y
                if (!mIsBeingDragged && abs(deltaY) > mTouchSlop) {
                    parent?.let {
                        requestDisallowInterceptTouchEvent(true)
                    }
                    mIsBeingDragged = true
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop
                    } else {
                        deltaY += mTouchSlop
                    }
                }
                if (mIsBeingDragged) {
                    // 1. 在內部內容滑動之前,先調用dispatchNestedPreScroll,讓父View進行滑動。
                    if (dispatchNestedPreScroll(
                            0,
                            deltaY,
                            mScrollConsumed,
                            mScrollOffset,
                            ViewCompat.TYPE_TOUCH
                        )
                    ) {
                        // 更新剩下的滑動距離
                        deltaY -= mScrollConsumed[1]
                        // 更新父View滑動的距離
                        mNestedYOffset += mScrollOffset[1]
                    }
                    // 更新上一次event的Y座標
                    mLastMotionY = y - mScrollOffset[1]

                    val oldScrollY = scrollY
                    val range = getScrollRange()
                    // 2. 觸發內容的滑動
                    overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, true)
                    val scrollDeltaY = scrollY - oldScrollY
                    val unconsumedY = deltaY - scrollDeltaY
                    mScrollConsumed[1] = 0
                    // 3. 當內容滑動完成,如果滑動距離還未消費,那麼就調用dispatchNestedScroll方法,詢問父View是否還消費
                    dispatchNestedScroll(
                        0,
                        scrollDeltaY,
                        0,
                        unconsumedY,
                        mScrollOffset,
                        ViewCompat.TYPE_TOUCH,
                        mScrollConsumed
                    )
                    // 再次更新相關信息
                    mLastMotionY -= mScrollOffset[1]
                    mNestedYOffset += mScrollOffset[1]
                }
            }
            MotionEvent.ACTION_UP -> {
                endDrag()
            }
            MotionEvent.ACTION_CANCEL -> {
                endDrag()
            }
        }

        return true
    }

  onTouchEvent方法代碼比較長,但是重點都是在move裏面,我們分開來看:

  1. down事件都是一些基本實現,比如說更新mLastMotionY,還有就是調用startNestedScroll方法。
  2. up和cancel都是調用了endDrag,這個方法裏面只做兩件事,重置mIsBeingDragged,同時還調用了stopNestedScroll

  而move事件的實現就比較複雜了,我將其分爲三步:

  1. 根據mLastMotionY計算出來本次產生了多少滑動距離,然後就是調用dispatchNestedPreScroll方法。目的就是,在內部滑動之前,先詢問父View是否要消費距離。其中mScrollConsumed裏面記錄的父View消費的距離,同時mScrollOffset表示我們的View在屏幕滑動的距離,主要是根據getLocationInWindow來計算的。父View滑動完成,自然就是就是更新某些狀態值,比如說:deltaY 、mNestedYOffset 、mLastMotionY 。可能有人會有疑問,爲啥還有更新mLastMotionY呢?因爲我們的View在屏幕中更新了位置,所記錄的上一次event的Y座標自然也要更新,不然下一次event計算的滑動距離會有誤差。
  2. 調用overScrollBy方法,用來滑動View內部的內容。這裏,大家可能又有疑問了,爲啥要調用overScrollBy,而不是調用scrollBy或者scrollYo呢?舉一個例子,如果滑動距離還剩下100px,但是View其實只能滑動50px,此時不能直接滑動100px,所以這裏需要裁剪滑動距離,如果直接調用scrollBy或者scrollYo,我們需要自己計算裁剪距離,但是overScrollBy方法內部會根據scrollRange來進行裁剪,所以第調用overScrollBy是爲了我們自己不需要寫裁剪的代碼。
  3. 當View自己滑動完成,調用dispatchNestedScroll,詢問父View是否需要消費剩下的距離。如果消費了,自然要更新mLastMotionYmNestedYOffset

  在嵌套滑動流程中,特別是move事件中需要觸發嵌套滑動時,這個流程固定不變的,即:


  看上去還是比較簡單的,但是有些前提大家必須知道,在這裏,我再次強調一遍:

  1. 調用setNestedScrollingEnabled方法,設置爲true。
  2. 在調用dispatchNestedPreScrolldispatchNestedScroll之前,必須先調用startNestedScroll,並且傳遞的Type必須是一致的。
  3. 滑動完成之後,需要調用stopNestedScroll方法來切斷傳遞鏈。

  Type一共有兩個,分別是:

  1. TYPE_TOUCH:表示手指在屏幕上產生的嵌套滑動事件。
  2. TYPE_NON_TOUCH:表示手指未在屏幕上產生的嵌套滑動事件,比如說Fling滑動。

  單指滑動的實現就介紹在這裏,整體上來說還是比較簡單的。完整代碼大家可以在KotlinDemo找到,commit message 爲【新增自定義NestedScrollViewGroup的Demo,並且完成CustomNestedScrollView的move事件處理】

4. 支持多指滑動

  如果要支持多指滑動,首先要引入新的action含義,如下:

  1. ACTION_POINTER_DOWN:表示非第一個手指落在屏幕中。
  2. ACTION_POINTER_UP: 表示非最後一個手指離開屏幕。
  3. ACTION_UP:表示最後一個手指離開屏幕
  4. ACTION_MOVE:表示任意一個手指在滑動。
  5. ACTION_DOWN:表示第一個手指落在屏幕中。

  同時,從整體上來看,我們需要定義一個變量,表示最近一個落在屏幕中的手指;還有就是,我們需要在down和up時,實時的更新這個記錄的最近手指。最後就是,在獲取滑動座標時,需要傳入手指Id,不能像以前直接getY來獲取。

(1).使用手指Id

  前面已經提到了,此時獲取座標不能直接調用getY方法,我們來看一下怎麼獲取,這裏只看onTouchEvent方法:

    override fun onTouchEvent(event: MotionEvent): Boolean {
        // ······
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                mActivePointerId = event.getPointerId(0)
                mLastMotionY = event.y.toInt()
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
            }
            MotionEvent.ACTION_MOVE -> {
                val pointerIndex = event.findPointerIndex(mActivePointerId)
                if (pointerIndex == -1) {
                    return true
                }

                val y = event.getY(pointerIndex).toInt()
                var deltaY = mLastMotionY - y
                //······
            }
            // ······
        }

        return true
    }

  我們發現在ACTION_DOWNACTION_MOVE中,都有一個mActivePointerId,用來表示最近活躍的手指Id,可以通過這個id,找到一個index,然後event可以通過index,獲取對應的座標。同時,我們還發現一個小細節,就是在ACTION_DOWN裏面初始化了mActivePointerId了,這一點大家要注意。

(2). 更新手指Id

  除去初始化手指Id和使用手指Id,還有必不可少的步驟就是:更新手指Id。更新時機體現在如下幾個地方:

  1. ACTION_CANCEL、ACTION_UP
  2. ACTION_POINTER_DOWN
  3. ACTION_POINTER_UP

  我們直接看代碼:

    override fun onTouchEvent(event: MotionEvent): Boolean {
        // ······
        when (action) {
            // ······
            MotionEvent.ACTION_UP -> {
                mActivePointerId = INVALID_POINTER
                // // ······
            }
            MotionEvent.ACTION_CANCEL -> {
                mActivePointerId = INVALID_POINTER
                endDrag()
            }
            MotionEvent.ACTION_POINTER_DOWN -> {
                val newPointerIndex = event.actionIndex
                mLastMotionY = event.getY(newPointerIndex).toInt()
                mActivePointerId = event.getPointerId(newPointerIndex)
            }
            MotionEvent.ACTION_POINTER_UP -> {
                onSecondaryPointerUp(event)
            }
        }
        // ······
        return true
    }

  從代碼中來看,三個地方的區別如下:

  1. ACTION_CANCEL、ACTION_UP:重置mActivePointerId
  2. ACTION_POINTER_DOWN:更新mActivePointerId,因爲這個時機表示一個手指落入屏幕,所以直接更新爲當前手指。
  3. ACTION_POINTER_UP:調用onSecondaryPointerUp方法,嘗試更新mActivePointerId。分爲兩種情況來看待:如果離開屏幕的手指Id不是mActivePointerId記錄的,那麼就直接忽略;如果是mActivePointerId記錄的,就跟據pointerIndex來判斷,將mActivePointerId更新到第一個手指,還是其他手指。細節大家可以看onSecondaryPointerUp方法實現。

  總的來說,多指滑動的實現比較簡單,畢竟已經有單指滑動的基礎。完整代碼大家可以在KotlinDemo找到,commit message 爲【支持多指滑動】

5. 支持Fling滑動

  其實支持了單指滑動和多指滑動,就能滿足大部分的要求,Fling滑動算是比較特別的需求了,大概只有列表類View才需要支持。不過,我們也來看看怎麼實現。
  要想一個View支持Fling,我們需要準備兩個東西:

  1. 需要計算手指離開View時,滑動的速度。這個速度可以作爲Fling滑動的初速度。
  2. 在初速度基礎上,來實現Fling滑動。

(1). VelocityTracker

  要計算手指離開的屏幕時滑動的速度,這個非常簡單,官方已經提供對應的工具了,那就是VelocityTracker,我們將對應的Event傳入到這個工具類裏面就可以計算出我們想要的速度。
  不過,在這裏我需要強調一個事,那就是由於涉及到到嵌套滑動,那麼View就會屏幕中改變位置,直接傳入Event會導致我們計算的速度不是正確的。舉一個例子,假設上一個move event的Y座標是500,同時這個move事件產生了100px的滑動距離,將該View上移了100px,那麼下一個event的Y座標也是500,實際上是沒有改變的,因爲event.getY是相對於View的座標,手指相對於View的位置沒有變,所以event的座標沒有變。這也是爲什麼前面,我們需要實時更新mLastMotionY,就是爲了避免下一次計算的滑動距離不正確;同時還定義了一個mNestedYOffset,用來記錄的是,本次完整事件鏈中(down->move->up),該View在屏幕中移動了多少距離。
  我們直接來看實現,還是onTouchEvent方法:

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val action = event.actionMasked
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0
        }
        val vtev = MotionEvent.obtain(event)
        vtev.offsetLocation(0f, mNestedYOffset.toFloat())
        // ······

        mVelocityTracker?.let {
            it.addMovement(vtev)
        }
        vtev.recycle()

        return true
    }

  這裏爲了解決上面所說速度計算不對,在調用addMovement之前,調整event的座標,調整所依賴的就是mNestedYOffset
  只要我們理解到這一點就行,其他的就不需要過多的分析了。

(2). OverScroller

  要想實現Fling滑動,我們需要使用到OverScrollerOverScroller的作用是,擁有一個初速度,然後不斷輪詢產生滑動的距離,我們可以這個滑動距離,用來滑動我們想要的東西,這裏就是兩部分:父View和子View的內容。
  我們來看一下實現:

    override fun onTouchEvent(event: MotionEvent): Boolean {
            // ······
            MotionEvent.ACTION_UP -> {
                mActivePointerId = INVALID_POINTER
                val velocityTracker = mVelocityTracker
                velocityTracker?.computeCurrentVelocity(1000, mMaximumVelocity.toFloat())
                // 向上滑動速度爲負,向下滑動速度爲正
                val initVelocity = velocityTracker?.getYVelocity(mActivePointerId)?.toInt() ?: 0
                if (abs(initVelocity) > mMinimumVelocity) {
                    if (!dispatchNestedPreFling(0F, -initVelocity.toFloat())) {
                        dispatchNestedFling(0F, -initVelocity.toFloat(), true)
                        fling(-initVelocity.toFloat())
                    }
                }
                endDrag()
            }
            // ······
        return true
    }

  fling的觸發是在up裏面方法,但是我們從代碼實現上可以看出來,在正式調用fling方法之前,還通過嵌套滑動的方法--dispatchNestedPreFling,將fling的滑動分發到父View,主要的目的是爲了詢問父View是否消費fling事件。如果返回的是false,那麼表示父View不消費fling,那就是子View自己消費了,消費的邏輯主要是體現在fling發現裏面。不過在看fling方法之前,我們先關注另一個點,那就是endDrag:

    private fun endDrag() {
        mIsBeingDragged = false
        stopNestedScroll(ViewCompat.TYPE_TOUCH)
    }

  在這個方法裏面,我們關注的是,這個通過stopNestedScroll切斷了type爲TYPE_TOUCH的傳遞鏈,這是爲了後面能夠重新建立TYPE_NON_TOUCH的傳遞鏈做準備。
  我們回過頭來看繼續看fling方法:

    private fun fling(velocityY: Float) {
        mOverScroller.fling(0, scrollY, 0, velocityY.toInt(), 0, 0, Int.MIN_VALUE, Int.MAX_VALUE)
        runAnimatedScroll()
    }

  這裏通過OvserScrolled的fling方法開始觸發fling滑動,同時還調用了runAnimatedScroll方法:

    private fun runAnimatedScroll() {
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)
        mLastScrollY = scrollY
        ViewCompat.postInvalidateOnAnimation(this)
    }

  這個方法裏面主要是做了三件事:

  1. 建立TYPE_NON_TOUCH的傳遞鏈。
  2. 記錄scrollY,用以計算OverScroller產生的滑動距離。
  3. 調用postInvalidateOnAnimation方法,從而觸發輪詢,回調computeScroll方法。

  fling真實的滑動實現邏輯都在computeScroll方法裏面,我們看一下:

    override fun computeScroll() {
        if (mOverScroller.isFinished) {
            return
        }
        mOverScroller.computeScrollOffset()
        val y = mOverScroller.currY
        var deltaY = y - mLastScrollY
        mLastScrollY = y
        mScrollConsumed[1] = 0
        dispatchNestedPreScroll(0, deltaY, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)
        deltaY -= mScrollConsumed[1]
        val range = getScrollRange()
        if (deltaY != 0) {
            val oldScrollY = scrollY
            overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, false)
            val consumedY = scrollY - oldScrollY
            deltaY -= consumedY
            mScrollConsumed[1] = 0
            dispatchNestedScroll(
                0,
                consumedY,
                0,
                deltaY,
                null,
                ViewCompat.TYPE_NON_TOUCH,
                mScrollConsumed
            )
            deltaY -= mScrollConsumed[1]
        }
        if (deltaY != 0) {
            abortAnimateScroll()
        }
        if (!mOverScroller.isFinished) {
            ViewCompat.postInvalidateOnAnimation(this)
        } else {
            abortAnimateScroll()
        }
    }

  從代碼實現上來看,整體框架跟move的實現比較類似。首先,先計算產生的滑動距離,然後通過dispatchNestedPreScroll詢問父View是否消費滑動距離,然後更新滑動距離,調用overScrollBy方法,自己消費滑動距離;當自己消費完成,再調用dispatchNestedScroll詢問父View是否消費。
  這裏有一個小細節,當到最後滑動距離沒有消費完成,表示當前已經滾到邊界了此時需要停掉Fling滑動。
  fling 的整個邏輯就是這樣,整體來說還是比較清晰的。完整代碼大家可以在KotlinDemo找到,commit message 爲【支持Fling滑動】

6. 總結

  到這裏,我對實現嵌套滑動的介紹就結束了。本文的重點介紹如何定義一個能夠產生嵌套滑動的View,並沒有介紹如何定義一個處理嵌套滑動的View。
  這個可以留給大家,有興趣的同學可以參考CoordinatorLayout的實現,自己實現一個。之前我定義過處理上下兩個RecyclerView的ViewGroup,這之間的聯動真是折騰人。類似於這種結構:


  其中黃色部分的ViewGroup就是需要我來定義。有興趣的同學嘗試這種ViewGroup怎麼來定義。
  我對本文的內容來做一個簡單的總結:

  1. 要定義一個產生嵌套滑動的View,實現需要實現NestedScrollingChild接口,並且調用setNestedScrollingEnabled方法,設置爲true,表示該View可以產生嵌套滑動的事件。
  2. 定義一個產生嵌套滑動的View,需要處理三個問題:單指滑動,多指滑動,Fling滑動。
  3. 單指滑動,需要在down時,調用startNestedScroll方法建立起嵌套滑動的傳遞鏈;在move時,計算產生的滑動距離,先調用dispatchNestedPreScroll方法,詢問父View消費滑動的距離,然後在自己消費滑動距離,最後在調用dispatchNestedScroll再次詢問父View消費滑動的距離。
  4. 多指滑動,需要在down時,定義一個活躍的手指Id;在move時,使用這個手指Id計算event的Y座標,從而正確的計算滑動距離;最後就是在合適的時機(up、cancel)正確的更新這個手指Id。
  5. Fling 滑動需要處理兩個問題:計算滑動速度和觸發Fling滑動。滑動速度可以使用VelocityTracker來計算,Fling滑動可以使用OverScroller來實現。但是Fling滑動需要跟手指滑動區分的是,Fling滑動建立的嵌套滑動傳遞鏈,type是TYPE_NON_TOUCH;而單指滑動是TYPE__TOUCH
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章