Android NestedScroll筆記

  1. NestedScrollingParent和NestedScrollingChild這套協議的目的是爲了增強(或者說反轉)Android自上而下的MotionEvent傳遞流,這條流的傳遞方向是死的,一條路走到天黑不回頭,單向的好處是簡單,壞處就是反方向的體系內通信基本不可能了。
  2. 對於某些應用場景,希望在MotionEvent傳遞到下級以後,還有機會可以回饋給上級一些信息,就從原來的自上而下變成了自下而上,而如果要對Android原來的那套View的dispatch機制進行修改,顯然代價太大,收益卻不大(這個需求並不是一個common的需求),既然不能修改全局機制,那麼通過接口增強功能的方式就成了首選。
  3. 上面說到了,既然是自下而上,那麼顯然需要一個下家,一個上家,而兩者必然需要進行交互通信,通信就需要協議,因爲兩者的地位是不對等的,會有兩套協議,即NestedScrollingParent(上家)和NestedScrollingChild(下家). 嚴格的說,其實通信是單向的,主要通信協議是NestedScrollingParent(單向通信就是被動的回調), NestedScrollingChild是對體系外部開放的通信協議。說實話,這個”Nested”前綴起的不太好,它其實只代表了一部分應用場景(比如ScrollView嵌套ScrollView),但是卻沒有反映出來反向通信這個本質(當然了,這裏的通信內容也挺侷限的,只能溝通關於Scroll的信息).
  4. 先看NestedScrollingParent協議中的方法(其方法全部都是on前綴,都是回調,這和上家的定位有關,上家被動的等待下家的觸發回調,因爲下家是事件源,下面這些回調本質就是消息的傳遞):

    • onStartNestedScroll(View child, View target, int nestedScrollAxes): 該方法在下家觸發自己的Scroll時會被調用,即下家告訴上家自己這邊要開始scroll了,看上家對這一輪scroll是否感興趣,如果感興趣,纔會傳輸這一輪scroll的信息,其實跟View的dispatch機制挺像的,先確認你要不要
    • onNestedScrollAccepted(View child, View target, int nestedScrollAxes),在上面的onStartNestedScroll返回true後,會被回調,給予一次初始化配置的機會。
    • onStopNestedScroll(View target),下家告訴上家這一輪Scroll的結束
    • onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed),每次Scroll都會回調,注意Consumed和Unconsumed,Consumed代表被下家已經消費的scroll距離,而Unconsumed則是下家”吃剩的”,纔會讓上家有機會處理,這裏可以看到,在這個函數中上家基本不可能攔截下家對scroll的處理,上家能做的就是根據下家處理完scroll的信息做一些自己的操作
    • onNestedPreScroll(View target, int dx, int dy, int[] consumed),剛剛纔說了onNestedScroll中我們沒有機會阻攔下家,pre則給了你阻攔的機會,其會在下家能處理自己的Scroll前被調用,關鍵點是consumed這個數組,上家將自己消費的scroll距離填入該數組,就變成了”下家吃上家剩的”
    • onNestedFling/onNestedPreFling基本同scroll,只不過換成了fling的。
    • getNestedScrollAxes
  5. NestedScrollingChild的接口函數基本和NestedScrollingParent是對應的,一個主動一個被動:

    • setNestedScrollingEnabled(boolean enabled)/isNestedScrollingEnabled(),一個setter一個getter,表示該接口實現者是否支持NestedScroll特性,注意,如果是flying中返回false,會觸發stopNestedScroll()
    • startNestedScroll(int axes),該函數的實現者View需要遵從下面的約定:
      1. 一旦View這邊開始一輪scroll,那麼就需要調用此函數。該函數返回true,代表找到一個對這一輪scroll感興趣的上家後面的聯動纔有可能,否則要等到下一輪scroll。
      2. 每一次(注意不是輪)*scroll都需要須要觸發dispatchNestedPreScroll(),如果該函數返回true, 代表上家對這次scroll進行了消費,下家需要根據上家的消費值進行scroll距離的修正,當然了,你自己實現的時候,真不管也沒人攔你,只要達到你的目的就行*
      3. dispatchNestedScroll()需要在下家消費了scroll信息後調用,將自己消費和沒有消費的scroll信息都傳過去,讓上家能對下家消費的情況作出響應
    • stopNestedScroll(), 這一輪scroll結束了
    • hasNestedScrollingParent(),有上家願意和你一塊玩麼?
    • dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)/dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow), 上面startNestedScroll(…)中都介紹過了。
    • dispatchNestedFling/dispatchNestedPreFling,基本同scroll。
  6. 對實現了NestedScrollingParent和NestedScrollingChild的NestedScrollView的一些簡單分析(NestedScrollView實現兩個接口表明了其可攻可受,也可以理解爲其可以作爲一個反向傳遞過程中的中樞,而不是一個終點):
    • 按照Google的建議,上面兩個接口的實現全部delegate給了NestedScrollingParentHelper和NestedScrollingChildHelper,NestedScrollView的實現中基本都是相應helper的方法進行轉發。
    • 上面NestedScrollingParentHelper和NestedScrollingChildHelper的構造參數都填的是NestedScrollView自己,這樣才能從NestedScrollView向上不斷的檢查parent,找到上家。
    • NestedScrollView的實現基本等同於ScrollView,主要多的就是對NestedScroll的支持,其Scroll的原理和ScrollView是一樣的,都是在ComputeScroll()中結合內部的Scroller獲取當前的scrollX/Y然後ScrollTo, computeScroll() -> overScrollByCompat() -> onOverScrolled() -> super.scrollTo(scrollX, scrollY)
    • 來看看NestedScrollingChild的幾個關鍵函數的調用點:
      1. startNestedScroll()在onInterceptTouchEvent()/onTouchEvent()中**可以開始(注意,是可以開始,即該輪交互判斷爲Scroll的條件已經滿足了,但是呢,還沒有真正的開始scroll一段距離)**scroll時都會被調用,因爲是垂直滑動,會傳遞SCROLL_AXIS_VERTICAL.
      2. dispatchNestedPreScroll()的調用時機,是在onTouchEvent()中對ActionMove的處理,認爲當前確實交互滑動了一定的距離,這裏會本次華東的距離傳遞到該回調中,爲了遵循約定,在其返回true的情況下(即上家消費了一定的距離),會提取出上家消費的距離值,然後對這次的MotionEvent進行offsetLocation來將上家消費的距離反映給下家
      3. dispatchNestedScroll()和dispatchNestedPreScroll()在同一部分,不過其所在位置是在下家已經自己處理了scroll距離之後。和前面的pre對比
    • 從上面可以看到,NestedScrollView實現了通過調用自己實現的NestedScrollingChild的方法將自己在scroll/fling中的一舉一動都傳遞了出去,並且還能根據調用函數的參數進行聯動
    • 跟蹤下真正的實現體NestedScrollingChildHelper:
      1. startNestedScroll(…), 這一步很關鍵,沿着View體系不斷向上尋找對這次NestedScroll感興趣的上家(一定是View的parent),並保存爲mNestedScrollingParent,這樣後面的一系列NestedScroll消息纔有傳遞的目的地
        • while循環向上不斷取view的parent,
        • 對每一級parent,先調用ViewParentCompat.onStartNestedScroll(…), 如果parent實現了NestedScrollingParent接口,那麼會調用其實現的onStartNestedScroll(..)方法,返回true代表該parent對這輪NestedScroll感興趣,終於找到了上家,將其保存到mNestedScrollingParent留待後面的信息投遞
        • 調用ViewParentCompat.onNestedScrollAccepted(…)來回調該parent的onNestedScrollAccepted(),也符合了該函數的語義。
      2. dispatchNestedScroll(),每次調用都會檢查isNestedScrollingEnabled()以及是否上家還在.
        • 核心還是通過ViewParentCompat.onNestedScroll(…)觸發上家的onNestedScroll()
      3. dispatchNestedPreScroll(),基本同上,offsetInWindow的部分不介紹了。
      4. stopNestedScroll(),除了觸發上家的onStopNestedScroll(),還會將mNestedScrollingParent重置爲null,代表着這一輪NestedScroll的結束
      5. 從上面的分析可以看出,NestedScrollingChildHelper本身沒有做很複雜的工作,其意義在於將查找上家,保存上家以及在流程中觸發上家的回調等比較common的操作全部獨立了出來,模塊化,類似於很多庫中的DragHelper等輔助類的定位
  7. 上面將NestedScrollView**作爲下家進行了分析,不過因爲其也實現了NestedScrollingParent,因此也可以作爲上家進行分析(即被動的接受某級子View的NestedScroll消息)**:

    • 因爲上家的方法都是被動回調,因此在NestedScrollView中基本不會有主動調用的地方。
    • onStartNestedScroll(View child, View target, int nestedScrollAxes)的實現很簡單,檢查下nestedScrollAxes是不是包含了ViewCompat.SCROLL_AXIS_VERTICAL, 即對包含了垂直Scroll的NestedScroll信息感興趣,可以和該下家進行合作
    • onNestedScrollAccepted(…):
      1. 調用了mParentHelper的onNestedScrollAccepted(),因爲上家的定位是被動的,因此NestedScrollingParentHelper中基本沒有什麼實現(),只是保存了NestedScrollAxe.
      2. startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL),這一點充分顯示出NestedScrollView的中樞角色,及其現在扮演的是上家,但是通過調用此方法,繼續將NestedScroll消息向上傳遞,對於接受了這個消息的parent來說,其又成了下家
      3. onNestedScroll():
        • 實現NestedScroll關鍵的一點: scrollBy(0, dyUnconsumed), 下家沒有消費掉全部的滑動距離,那麼多出來的可以由上家(也就是)來進行消費,不過後面會緊接着計算NestedScrollView實際可以消費的距離,unConsumed的距離則通過,進一步調用dispatchNestedScroll(..)再次向上傳遞
      4. onNestedPreScroll(): 空函數,這應該和其定位有關:NestedScrollView先保證子View對Scroll的處理,然後纔會處理剩下的,其沒有對子View處理進行攔截的需求和必要
  8. 總結來看,NestedScrollingChild和NestedScrollingParent組成了Scroll/Fling反向傳遞的通道,並且可以通過一個View同時實現兩者來達到形成一個反向傳遞鏈的效果,其本質很簡單,只不過對與實現者的嵌入程度比較高,很難通過直接繼承的方式來實現(NestedScrollView沒有繼承ScrollView一部分原因就是這個)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章