Android NestedScrolling全面解析 - 帶你實現一個支持嵌套滑動的下拉刷新(上篇)

轉載請注明出處 : http://www.lxweimin.com/p/f09762df81a5 謝謝

自從Lollipop開始,谷歌霸霸給我們帶來了一套全新的嵌套滑動機制 - NestedScrolling來實現一些普通情況下不容易辦到的滑動效果。Lollipop及以上版本的所有View都已經支持了這套機制,Lollipop之前版本可以通過Support包進行向前兼容。
那么我們先提出來三個問題:

  1. 什么是NestedScrolling?
  2. 怎么運作的?
  3. 我們怎么去使用?

讓我們帶著問題一起來深入了解下這神奇的嵌套滑動。
首先,什么是NestedScrolling呢,它和我們已熟知的dispatchTouchEvent不太一樣。
我們先來看傳統的事件分發,它是由父View發起,一旦父View需要自己做滑動效果就要攔截掉事件并通過自己的onTouch進行消耗,這樣子View就再沒有機會接手此事件,如果自己不攔截交給子View消耗,那么不使用特殊手段的話父View也沒法再處理此事件。
NestedScrolling不一樣,它是由子View發起的,它的過程是這樣的:

場景一:

  • 子View:爸爸,我準備在x軸方向滑動50px,有什么吩咐沒
  • 父View:好的,沒什么吩咐的,你滑吧。
  • 子View:遵命!滑動ing...... 爸爸,我滑完了,總共滑了50px。
  • 父View:好的,記得每次都要提前匯報!

場景二:

  • 子View:爸爸,我準備在x軸方向滑動50px,有什么吩咐沒
  • 父View:你x軸的50px我要全部沒收,你別動了
  • 子View:納尼 w(?Д?)w 好吧誰讓你是爸爸...
ni men dong le me

經過如上的場景分析,其實可以看出來NestedScrolling就是父View和子View之間的一套滑動交互機制。簡單點來說就是要求子View在準備滑動之前將滑動的細節信息傳遞給父View,父View可以決定是否部分或者全部消耗掉這次滑動,并使用消耗掉的值在子View滑動之前做自己想做的事情,子View會在父View處理完后收到剩余的沒有被父View消耗掉的值,然后再根據這個值進行滑動。滑動完成之后如果子View沒有完全消耗掉這個剩余的值就再告知一下父View,我滑完了,但是還有剩余的值你還要不要?
仔細想想就能發現,這套機制很有意思,那么我們直接進入問題2:怎么辦到的?我們看一下Lollipop及以上版本的View源碼就可以看到它多了這么幾個方法:

public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);

而ViewGroup中多了這些方法:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();

新東西還挺多的,看著有點暈

先不著急去理解這些方法,開篇時就在說Lollipop及以上版本的所有View都已經支持了NestedScrolling,Lollipop之前版本可以通過Support包進行向前兼容,那怎么向前兼容呢?
這就需要Support包里的以下4個類出場了

  • NestedScrollingParent
  • NestedScrollingParentHelper
  • NestedScrollingChild
  • NestedScrollingChildHelper

其中NestedScrollingParentNestedScrollingChild都是接口,分別對應ViewGroup和View中新增的方法,在Lollipop以下版本中,我們需要手動添加這兩個接口的實現。
那怎么實現接口中辣么多的方法呢?這就需要上面的Helper類登場了,Helper類中已經寫好了實現,只需要調用就可以了。

for example

我們隨便找個方法

    @Override
    public boolean startNestedScroll(int axes) {
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

是不是很簡單,谷歌霸霸都替我們寫好實現了,贊美霸霸。
那,那我們還需要做什么?


kong qi zhong mi man zhe

我們當然還有事情要做,一開始我們就說了這是一套機制,谷歌霸霸只是把基礎的東西給我們鋪墊好了,這些方法雖然實現好了但是空擺在這還是沒什么效果的,具體還需要我們去在合適的時機調用,那什么時候是合適的時機呢?還記得一開始的場景分析么?我們來把分析的結論結合著這些方法走一遍。

  1. 首先子View需要找到一個支持NestedScrollingParent的父View,告知父View我準備開始和你一起處理滑動事件了,一般情況下都是在onTouchEvent的ACTION_DOWN中調用public boolean startNestedScroll(int axes),然后父View就會被回調public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)。這里就產生了四個問題:
    1 int axes,int nestedScrollAxes這兩個參數代表的是什么意思?
    2 為什么子View一調用startNestedScroll父View就會收到onStartNestedScroll和onStartNestedScroll回調?
    3 onStartNestedScroll和onNestedScrollAccepted有什么區別?
    4 這些方法的返回值代表什么意思?</br>
    問題1其實很簡單,這個參數是一個常量,代表滑動的方向,比如ViewCompat.SCROLL_AXIS_VERTICAL就是代表縱向滑動。
    問題2更簡單,因為這塊是谷歌霸霸替我們寫好了的╮(╯_╰)╭,不信我們打開NestedScrollingChildHelper的startNestedScroll方法看一下:
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

代碼不復雜,很容易看懂,同時也可以解答問題3和問題4,onStartNestedScroll可以理解是父View的一個驗證機制,父View可以在此方法中根據滑動方向等信息決定是否要和子View一起處理此次滑動,只有在onStartNestedScroll返回true的時候才會接著調用onNestedScrollAccepted,這個判斷是需要我們自己來處理的,所以NestedScrollingParentHelper并沒有實現此方法。如果這個方法返回false,那么while循環就會繼續尋找更上一級的父View讓其接手,這里我們可以看出,NestedScrolling的交互不是直接的父子關系一樣可以正常進行。至于onNestedScrollAccepted的作用就好說了,字面意思也可以理解出來父View接受了子View的邀請,可以在此方法中做一些初始化的操作。

  1. 然后每次子View在滑動前都需要將滑動細節傳遞給父View,一般情況下是在ACTION_MOVE中調用public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow),然后父View就會被回調public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)。這里依然帶著4個問題來分析:
    1 dx dy代表什么意思?
    2 int[] consumed代表什么意思?
    3 int[] offsetInWindow代表什么意思?
    4 返回值代表什么意思?</br>
    dx dy代表本次滑動 x y方向的距離,consumed這個數組就比較有意思了,需要子View創建并傳遞給父View,如果父View選擇要消耗掉滑動的值就需要通過此數組傳遞給子View。比如以下偽代碼表示父View要在x方向消耗10px,y方向消耗5px。
//子View中
int[] consumed = new int[2];
dispatchNestedPreScroll(50, -20, consumed, null);
//父View中@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
      consumed[0] = 10;
      consumed[1] = -5;
      //....
    }

注意,如果是相反方向,需要寫負值。
offsetInWindow這個數組也比較有意思,API文檔中對其描述的是

     * Optional. If not null, on return this will contain the offset
     * in local view coordinates of this view from before this operation
     * to after it completes. View implementations may use this to adjust
     * expected input coordinate tracking.

大致的意思是:這是一個可選的參數,可以傳null值,但如果不傳null,它將含有View從此方法調用之前到調用完成后的屏幕坐標偏移量,可以使用這個偏移量來調整預期的輸入坐標跟蹤。</br>
是不是不好理解,至于什么叫預期的輸入坐標跟蹤我也不知道,反正我小學英語水平只能翻成這樣┑( ̄Д  ̄)┍。其實不用過于糾結,我們知道這個參數保存著子View在這個方法調用前后的坐標偏移量就足夠了。
那返回值是怎么回事呢,我們來看一看NestedScrollingChildHelper的實現就明白了:

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

我們可以很清晰的看出這個方法的實現邏輯,先把view的屏幕坐標記錄下來,如果consumed傳的是null,會初始化一個臨時的mTempNestedScrollConsumed,然后去調用父View的onNestedPreScroll,完事之后再取一次View的屏幕坐標和之前記錄的相減把偏移量賦值到offsetInWindow,最后再檢查下父View有沒有消耗dx或dy,如果任意一項有消耗就返回true,否則返回false。</br>
那么返回true和false又有什么意義呢,目的是讓子View知道父View是否有消耗,因為子View有可能傳一個null的consumed,這樣就只能根據返回值來判斷父View是否有消耗。
父View處理完后,接下來子View就要進自己的滑動操作了,滑動完成后子View還需要調用public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)將自己的滑動結果再次傳遞給父View,父View對應的會被回調public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed),但這步操作有一個前提,就是父View沒有將滑動值全部消耗掉,因為父View全部消耗掉,子View就不應該再進行滑動了,這一步也就沒有必要了。這一步中幾個參數dxConsumed dxUnconsumed dyConsumed dyUnconsumed從字面意思就可以看出是x y方向消耗的和沒有消耗的值,因為子View進行自己的滑動操作時也是可以不全部消耗掉這些滑動值的,剩余的可以再次傳遞給父View,使父View在子View滑動結束后還可以根據子View剩余的值再次執行某些操作。
接下來就是隨著不停的滑動重復階段2這個過程。

  1. 隨著ACTION_UP或者ACTION_CANCEL的到來,子View需要調用public void stopNestedScroll()來告知父View本次NestedScrollig結束,父View對應的會被回調public void onStopNestedScroll(View target),可以在此方法中做一些對應停止的邏輯操作比如資源釋放等。但這一步還有一個意外情況,就是當子View ACTION_UP時可能伴隨著fling的產生,如果產生了fling,就需要子View在stopNestedScroll前調用public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY)public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed),父View對應的會被回調public boolean onNestedPreFling(View target, float velocityX, float velocityY)public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed),這點和之前的scroll處理邏輯是一樣的,返回值代表父View是否消耗掉了fling,參數consumed代表子View是否消耗掉了fling,fling不存在部分消耗,一旦被消耗就是指全部。

以上就是一次完整的NestedScrolling交互,Lollipop及以上版本的View大致就是這樣的處理邏輯,我制作了一張簡單的流程圖:

xi wang mei hua cuo....

最后,讓我們回歸到一開始就提出的第三個問題:我們怎么去使用?

NestedScrollingChild的話,Lollipop及以上版本可滑動的View如ScrollView、ListView已經按此交互流程為我們處理好了一切,如果你需要自定義View或者要兼容Lollipop以下版本就需要自己實現上述所有邏輯,當然更好的辦法是使用support包里為我們提供的,比如NestedScrollView,RecyclerView等。
NestedScrollingParent就需要我們自己去實現了,畢竟父View要實現什么酷炫的效果還是需要我們去定義的,當然,support包中也有一系列為我們準備好的Parent,就是design包中的CoordinatorLayout,下一章節,我將講述下怎么實現一個NestedScrollingParent的下拉刷新。

最后的最后,祝大家雞年大吉吧!o(////////)q

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容