Android嵌套滑動邏輯淺析

問題分析

嵌套滑動一直是Android中比較棘手的問題, 根本緣由是Android的事件分發機制致使的.致使嵌套滑動難處理的關鍵緣由在於當子控件消費了事件, 那麼父控件就不會再有機會處理這個事件了, 因此一旦內部的滑動控件消費了滑動操做, 外部的滑動控件就再也沒機會響應這個滑動操做了.html

嵌套滑動

不過這個問題終於在LOLLIPOP(SDK21)以後終於有了官方的解決方法, 就是嵌套滑動機制. 在分析具體的代碼邏輯以前, 下面先簡單介紹下嵌套滑動的一些基本知識.
嵌套滑動機制能夠理解爲一個約定, 原生的支持嵌套滑動的控件都是依據這個約定來實現嵌套滑動的, 例如CoordinatorLayout, 因此若是你自定義的控件也遵照這個約定, 那麼就能夠跟原生的控件進行嵌套滑動了.java

基本原理

嵌套滑動的基本原理是在子控件接收到滑動一段距離的請求時, 先詢問父控件是否要滑動, 若是滑動了父控件就通知子控件它消耗了一部分滑動距離, 子控件就只處理剩下的滑動距離, 而後子控件滑動完畢後再把剩餘的滑動距離傳給父控件.
經過這樣的嵌套滑動機制, 在一次滑動操做過程當中android

父控件和子控件都有機會對滑動操做做出響應, 尤爲父控件可以分別在子控件處理滑動距離以前和以後對滑動距離進行響應.數組

這解決了事件分發機制缺點引發的問題.bash

版本之別

在看具體的代碼以前先說下嵌套滑動相關方法的一些我認爲值得注意的地方.ide

LOLLIPOP(SDK21)以後

爲何說這個是官方的解決方法? 由於ui

嵌套滑動的相關邏輯做爲普通方法直接寫進了最新的(SDK21以後)ViewViewGroup類.this

普通方法是指這個方法不是繼承自接口或者其餘類, 例如View#dispatchNestedScroll, 能夠看到官方標註了Added in API level 21標示, 也就是說這是在SDK21版本以後添加進去的一個普通方法.spa

向前兼容

而SDK21以前的版本翻譯

官方在android.support.v4兼容包中提供了兩個接口NestedScrollingChildNestedScrollingParent, 還有兩個輔助類NestedScrollingChildHelperNestedScrollingParentHelper來幫助控件實現嵌套滑動.

這個兼容的原理很簡單

兩個接口NestedScrollingChildNestedScrollingParent分別定義上面提到的ViewViewParent新增的普通方法

在嵌套滑動中會要求控件要麼是繼承於SDK21以後的ViewViewGroup, 要麼實現了這兩個接口, 這是控件可以進行嵌套滑動的前提條件.
那麼怎麼知道調用的方法是控件自有的方法, 仍是接口的方法? 在代碼中是經過ViewCompatViewParentCompat類來實現.

ViewCompatViewParentCompat經過當前的Build.VERSION.SDK_INT來判斷當前版本, 而後選擇不一樣的實現類, 這樣就能夠根據版本選擇調用的方法.

例如若是版本是SDK21以前, 那麼就會判斷控件是否實現了接口, 而後調用接口的方法, 若是是SDK21以後, 那麼就能夠直接調用對應的方法.

輔助類

除了接口兼容包還提供了NestedScrollingChildHelperNestedScrollingParentHelper兩個輔助類, 這兩個輔助類實際上就是對應ViewViewParent中新增的普通方法, 代碼就不貼了, 簡單對比下就能夠發現, 對應方法實現的邏輯基本同樣, 因此

只要在接口方法內對應調用輔助類的方法就能夠兼容嵌套滑動了.

例如在NestedScrollingChild#startNestedScroll方法中調用NestedScrollingChildHelper#startNestedScroll.
題外話: 這裏實際用了代理模式來讓SDK21以前的控件具備了新增的方法.

默認處理邏輯

雖然ViewViewGroup(SDK21以後)自己就具備嵌套滑動的相關方法, 可是默認狀況是是不會被調用, 由於ViewViewGroup自己不支持滑動, 因此

自己不支持滑動的控件即便有嵌套滑動的相關方法也不能進行嵌套滑動.

上面已經說到要讓控件支持嵌套滑動

  • 首先要控件類具備嵌套滑動的相關方法, 要麼僅支持SDK21以後版本, 要麼實現對應的接口, 爲了兼容低版本, 更經常使用到的是後者.

  • 由於默認的狀況是不會支持滑動的, 因此控件要在合適的位置主動調起嵌套滑動的方法.

接下來經過分析相對簡單的支持嵌套滑動的容器NestedScrollView來了解下怎樣主動調起嵌套滑動的方法, 以及嵌套滑動的具體邏輯.

相關方法

先簡單看看相關方法的做用, 更具體的說明建議看源碼註釋中的方法說明.
注意 : 下文分析用內控件表示兩層嵌套中的子控件, 外控件表示嵌套中的父控件.

NestedScrollingChild

startNestedScroll : 起始方法, 主要做用是找到接收滑動距離信息的外控件.
dispatchNestedPreScroll : 在內控件處理滑動前把滑動信息分發給外控件.
dispatchNestedScroll : 在內控件處理完滑動後把剩下的滑動距離信息分發給外控件.
stopNestedScroll : 結束方法, 主要做用就是清空嵌套滑動的相關狀態
setNestedScrollingEnabledisNestedScrollingEnabled : 一對get&set方法, 用來判斷控件是否支持嵌套滑動.
dispatchNestedPreFlingdispatchNestedFling : 跟Scroll的對應方法做用相似, 不過度發的不是滑動信息而是Fling信息.(這個Fling好難翻譯.. =。=)本文主要關注滑動的處理, 因此後續不分析這兩個方法.

從方法名就能夠看出

內控件是嵌套滑動的發起者.

NestedScrollingParent

由於內控件是發起者, 因此外控件的大部分方法都是被內控件的對應方法回調的.
onStartNestedScroll : 對應startNestedScroll, 內控件經過調用外控件的這個方法來肯定外控件是否接收滑動信息.
onNestedScrollAccepted : 當外控件肯定接收滑動信息後該方法被回調, 可讓外控件針對嵌套滑動作一些前期工做.
onNestedPreScroll : 關鍵方法, 接收內控件處理滑動前的滑動距離信息, 在這裏外控件能夠優先響應滑動操做, 消耗部分或者所有滑動距離.
onNestedScroll : 關鍵方法, 接收內控件處理完滑動後的滑動距離信息, 在這裏外控件能夠選擇是否處理剩餘的滑動距離.
onStopNestedScroll : 對應stopNestedScroll, 用來作一些收尾工做.
getNestedScrollAxes : 返回嵌套滑動的方向, 區分橫向滑動和豎向滑動, 做用不大
onNestedPreFlingonNestedFling : 同上略

外控件經過onNestedPreScrollonNestedScroll來接收內控件響應滑動先後的滑動距離信息.

再次指出, 這兩個方法是實現嵌套滑動效果的關鍵方法.

從NestedScrollView看嵌套機制

說完上面一大通, 終於能夠開始分析源碼來了解嵌套滑動機制起做用的具體邏輯了.
NestedScrollView簡單地說就是支持嵌套滑動的ScrollView, 內部邏輯簡單, 並且它既能夠是內控件, 也能夠是外控件, 因此選擇分析它來了解嵌套滑動機制.

注意 : 由於NestedScrollingChildHelperNestedScrollingParent這兩個輔助類的實現跟ViewViewGroup中的對應方法是同樣的, 並且ViewViewGroup的源碼沒有使用兼容類, 因此下面分析相關方法的時候源碼都使用ViewViewGroup中的代碼.

上面已經說了嵌套滑動是從startNestedScroll開始, 因此先看看哪裏調用了這個方法, 在源碼裏一搜就能知道有兩個地方調用了這個方法.

  • onInterceptTouchEventACTION_DOWN的狀況

  • onTouchEventACTION_DOWN的狀況

由於ACTION_DOWN是滑動操做的開始事件, 因此當接收到這個事件的時候嘗試找對應的外控件. 只有找到了外控件纔有後續的嵌套滑動的邏輯發生.
關於NestedScrollView在這裏的實現其實有個奇怪的地方, 提出一個問題, 不感興趣的能夠直接跳過這段.

  • 既然內控件是發起者, 爲何要在onInterceptTouchEvent也調用startNestedScroll呢?

由於事件傳遞的時候會先執行外控件的onInterceptTouchEvent, 也就是說第一個執行startNestedScroll的是最外層的NestedScrollView, 即便它找到了對應的外控件後續若是有子控件消費了這個事件, 也就是說不執行onTouchEvent方法, 那麼找到外控件也沒用的, 不清楚設計者的意圖.

接着咱們看startNestedScroll是如何找對應的外控件的, 由於NestedScrollView#startNestedScroll調用了輔助方法的startNestedScroll, 因此下面直接貼View#startNestedScroll.

// View.javapublic 
boolean startNestedScroll(int axes) { 
    // ... 
    if (isNestedScrollingEnabled()) { 
        ViewParent p = getParent(); 
        View child = this; 
        while (p != null) { 
            try { 
                // 關鍵代碼 
                if (p.onStartNestedScroll(child, this, axes)) { 
                    mNestedScrollingParent = p; 
                    p.onNestedScrollAccepted(child, this, axes); 
                    return true; 
                }
            } catch (AbstractMethodError e) { 
                // ... 
            } 
            if (p instanceof View) { 
                child = (View) p; 
            } 
            p = p.getParent(); 
        } 
    } 
    return false;
}複製代碼

很是簡單的邏輯遍歷父控件, 調用父控件的onStartNestedScroll, 返回true表示找到了對應的外控件, 找到外控件後立刻調用onNestedScrollAccepted

從這裏能夠知道

外控件不必定是內控件的直接父控件, 但必定是最近的符合條件的外控件.

還能夠肯定了上面關於onStartNestedScroll的方法說明, 返回true表示接收內控件的滑動信息.對於NestedScrollView#onStartNestedScroll內部邏輯很簡單, 只要是豎直滑動方向就返回true, 因此能夠知道

NestedScrollView不支持橫向嵌套滑動.

接着被調用的是onNestedScrollAccepted, 看NestedScrollView#onNestedScrollAccepted

// NestedScrollView.java
@Overridepublic void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}複製代碼

輔助類的方法很簡單, 就是記錄當前的滑動方向, 在這裏NestedScrollView又調用startNestedScroll來找它本身的外控件, 這是爲了連續嵌套NestedScrollView, 不過這是NestedScrollView本身的實現, 無論它.

找到了外控件後ACTION_DOWN事件就沒嵌套滑動的事了, 要滑動確定會在onTouchEvent中處理ACTION_MOVE事件, 接着咱們看ACTION_MOVE事件是怎樣處理的.

// NestedScrollView#onTouchEvent
case MotionEvent.ACTION_MOVE: 
    // ... 
    final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); 
    int deltaY = mLastMotionY - y; 
    // 讓外控件先處理滑動距離 
    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 
        deltaY -= mScrollConsumed[1];// 消耗滑動距離 
        // ... 
    } 
    // ... 
    if (mIsBeingDragged) { 
        // ... 
        // 內控件處理滑動距離 
        if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 
                      0, true) && !hasNestedScrollingParent()) { 
            // ... 
        } 

        final int scrolledDeltaY = getScrollY() - oldY; 
        final int unconsumedY = deltaY - scrolledDeltaY; 
        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { 
            // ... 
        } 
        // ... 
    } 
    break;複製代碼

這部分是NestedScrollView可以處理嵌套滑動的關鍵代碼了, 其餘可以嵌套滑動的控件也應該在ACTION_MOVE中相似地處理滑動距離.

先計算出本次滑動距離deltaY, 這裏有個小細節

deltaY等於上一次的Y座標減去此次的Y座標, 這意味着在相關方法中接收到的滑動距離參數中, 滑動距離 > 0表示手指向下滑動, 反之表示手指向上滑動. 這是由於在屏幕中Y軸正方向是向下的.

獲得滑動距離deltaY後, 先把它傳給dispatchNestedPreScroll, 而後在結果返回true的時候, delta會減去mScrollConsumed[1].

接着看dispatchNestedPreScroll幹了什麼

// View.java
public boolean dispatchNestedPreScroll(int dx, int dy,
                     @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
    // ... 忽略狀態判斷 
    consumed[0] = 0; 
    consumed[1] = 0; 
    mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); 
    return consumed[0] != 0 || consumed[1] != 0; 
    // 其餘狀況返回false
}複製代碼

忽略條件判斷和offsetInWindow的相關處理, 先指出consumed就是上一步分析中的mScrollConsumed, dy就是deltaY.
由於dispatchNestedPreScroll的工做就是把滑動距離在內控件處理前分發給外控件, 因此這裏的關鍵代碼也很簡單, 就是直接把相關的參數傳給外控件的onNestedPreScroll, 而後只要外控件消耗了滑動距離(不論橫向仍是豎向), 就會返回true

因此

外控件若是想在內控件以前消耗滑動距離僅須要在onNestedPreScroll把消耗的值放到數組中返回給內控件.

onNestedPreScroll是決定外控件的嵌套滑動邏輯的關鍵方法, 在不一樣的控件中應該是根據須要有不一樣的實現的, 而在NestedScrollView中就是直接詢問它本身的外控件是否消耗滑動距離, 實現比較簡單就不貼代碼了.

在這裏提醒下, 在咱們本身修改嵌套滑動邏輯的時候須要注意滑動距離的正負號和內控件處理consumed數組的方式. 不過這些都是些數字遊戲, 不細說了.

好了, 如今外控件已經比內控件先處理了滑動距離了, 若是外控件沒有徹底消耗掉全部滑動距離, 這時該內控件處理剩下的滑動距離了, 不一樣的控件有不一樣的滑動實現, 在NestedScrollView中經過NestedScrollView#overScrollByCompat來進行滑動, 而且滑動結束後經過比對滑動先後的scrollY值獲得了內控件消耗的滑動距離, 而後獲得剩下的滑動距離, 最後傳給dispatchNestedScroll.

dispatchNestedScroll的邏輯跟dispatchNestedPreScroll幾乎同樣, 區別是它調用了外控件的onNestedScroll, 由於到這裏已是處理滑動距離最後的機會了, 因此onNestedScroll不會再影響內控件的處理邏輯了.

到這裏ACTION_MOVE事件就分析完畢了.

最後就是stopNestedScroll了, 代碼就不貼了, 調用這個方法基本是新的滑動操做開始前, 或者滑動操做結束/取消, 代碼邏輯就是進行一些變量的重置工做和調用onStopNestedScroll, 而onStopNestedScroll也相似.

整個嵌套滑動的基本邏輯就是這樣. 注意這裏雖然分析的是NestedScrollView, 但這表明了嵌套滑動的"約定"處理方式, 雖然不一樣的控件實際的實現會有不一樣不過應該遵循基本方法的調用順序, 確保參數的含義和參數的處理方式.

總結

  • 若是要支持嵌套滑動, 內控件和外控件要支持對應的方法, 爲了兼容低版本通常經過實現NestedScrollingChildNestedScrollingParent接口以及使用NestedScrollingChildHelperNestedScrollingParent輔助類.

  • 具體嵌套滑動邏輯主要是在onNestedPreScrollonNestedScroll方法中.

  • 父控件經過給數組賦值來把消耗的滑動距離傳遞給內控件.


感謝原創做者的獨到的剖析!!

本文轉載自:http://www.apkbus.com/blog-977752-79583.html

相關文章
相關標籤/搜索