1. NestedScrolling嵌套滾動機制

什么是嵌套滾動?(如下圖):

嵌套滾動(知乎效果).gif
  1. 一般情況下,如果我們界面有多個布局: 包括可滾動布局(ScrollView口叙、ListView澳腹、RecyclerView等)和不可滾動布局(普通的View). 當我們滾動該滾動布局時,該布局內(nèi)會做相應(yīng)的滾動,其他的不可滾動布局并不會有相關(guān)的變動晚唇。這是因為:滾動View在處理Touch事件時巫财,攔截了該Touch事件進行處理,那么布局內(nèi)后續(xù)的Touch事件都不會交給其他布局(父布局/同級View)哩陕,會一直下發(fā)到這個滾動View平项。
  2. 但是,我們從嵌套滾動可以看出: 當滾動ListView時,Toolbar、Bottombar悍及、FloatingActionBar會先隱藏后闽瓢,再執(zhí)行ListView的滾動(或者是ListView滾動過程中隱藏其他不可滾動View)。顯然這是因為: 滾動View布局內(nèi)發(fā)生Touch事件時心赶,滾動View先不處理這個Touch事件扣讼,先把它交給本身的父布局,父布局再把這個Touch事件交給與滾動View同級的非滾動View去處理(隱藏/改變顏色等等)缨叫,等到其他View處理完成后椭符,父布局不再需要滾動View內(nèi)的Touch事件時,滾動View就自己去處理剩下的Touch事件耻姥。

如何實現(xiàn)嵌套滾動

實現(xiàn)嵌套滾動機制主要依賴四個類:

1. NestedScrollingChild //滾動列表需要實現(xiàn)NestedScrollingChild接口销钝,以支持將滾動事件分發(fā)給父ViewGroup 
2. NestedScrollingParent //相應(yīng)的,父ViewGroup需要實現(xiàn)NestedScrollingParent接口琐簇,以支持將滾動事件進一步的分發(fā)給各個子View
3. NestedScrollingChildHelper //進行嵌套滾動的輔助類
4. NestedScrollingParentHelper //進行嵌套滾動的輔助類

一般實現(xiàn)NestedScrollingChild接口的滾動列表會把滾動事件委托給NestedScrollingChildHelper輔助類來處理蒸健。例如:RecyclerView實現(xiàn)了NestedScrollingChild接口,它內(nèi)部就會把滾動相關(guān)事件委托給NestedScrollingChildHelper對象來處理婉商,如下所示:

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

@Override
public void stopNestedScroll() {
    getScrollingChildHelper().stopNestedScroll();
}
    
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow) {
    return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

//NestedScrollingChild的方法有很多似忧,更多的可參見源碼
//...

當我們滾動RecyclerView時,RecyclerView首先會通過startNestedScroll方法通知父ViewGroup(“我馬上要滾動了丈秩,是否有兄弟節(jié)點要一起滾動盯捌?”),父ViewGroup會進一步把滾動事件分發(fā)給所有子View(實際是分發(fā)給和子View綁定的Behavior)癣籽,感興趣的子View會特別關(guān)注挽唉,即Behavior.onStartNestedScroll方法返回true。

1. RecyclerView會在Down事件時調(diào)用startNestedScroll方法

我們看下NestedScrollingChildHelper.startNestedScroll方法的實現(xiàn):

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //該循環(huán)主要是尋找到能夠協(xié)調(diào)處理滾動事件的父View筷狼,即實現(xiàn)NestedScrollingParent接口的父ViewGroup
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                //記錄協(xié)調(diào)處理滾動事件的父View
                mNestedScrollingParent = p;
                //ViewParentCompat是一個和父ViewGroup交互的兼容類,如果在Android5.0以上匠童,就用View自帶的方法埂材,否則若實現(xiàn)了NestedScrollingParent接口,則調(diào)用接口方法汤求。
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

上述方法會找到能夠協(xié)調(diào)處理滾動事件的父ViewGroup俏险,然后調(diào)用它的onStartNestedScroll方法

2. 調(diào)用父ViewGroup的onStartNestedScroll方法

因為CoordinatorLayout實現(xiàn)了NestedScrollingParent接口严拒,所以我們看下CoordinatorLayout.onStartNestedScroll方法:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;
    final int childCount = getChildCount();
    //詢問每一個子View是否對滾動列表的滾動事件感興趣?
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        //獲取和子View綁定的Behavior
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
            handled |= accepted;
            //做一下標注竖独,作為判斷后續(xù)是否接收滾動事件的標記
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

上述方法會遍歷每一個子View裤唠,詢問它們是否對滾動列表的滾動事件感興趣,若Behavior.onStartNestedScroll方法返回true莹痢,則表示感興趣种蘸,那么滾動列表后續(xù)的滾動事件都會分發(fā)到該子View的Behavior

因此竞膳,我們可以在自定義的Behavior.onStartNestedScroll方法中根據(jù)實際情況決定是否對滾動事件感興趣航瞭。

假設(shè)CoordinatorLayout的某個子View對RecyclerView的滾動事件感興趣(Behavior.onStartNestedScroll方法返回true)
-> CoordinatorLayout.onStartNestedScroll返回true
-> RecyclerView.startNestedScroll返回true
-> RecyclerView就會把用戶的滾動事件源源不斷的分發(fā)給之前找到的父ViewGroup
-> 父ViewGroup則進一步分發(fā)給感興趣的子View
-> 感興趣的子View處理完滾動事件后,若用戶的滾動距離沒有被消費完
-> RecyclerView才有機會處理滾動事件(例如:用戶一次性滾動了10px坦辟,其中某個View消費了8px刊侯,那么RecyclerView就只能滾動2px了)

3.RecyclerView會在Move事件時進行事件分發(fā)(先交給父布局,再自己處理)

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        final int action = e.getActionMasked();
        ...
        switch (action) {
            //other  case...
            case MotionEvent.ACTION_MOVE: {
                //1.先算出滾動距離...
                //2.事件分發(fā)給父ViewGroup處理
                // dispatchNestedPreScroll返回true锉走,說明 父ViewGroup消耗了一定距離滨彻,消耗掉的距離存儲在mScrollConsumed,滾動的距離要減去父ViewGroup消耗的距離
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }
                //3.計算出本身處理的距離...
                //4.RecyclerView本身處理這些滾動事件(scrollByInternal)
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            }break;
            //other  case...
        }
    }

3.1 調(diào)用RecyclerView的dispatchNestedPreScroll把事件分發(fā)給父ViewGroup處理:ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed)

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
       return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

//NestedScrollingChildHelper.dispatchNestedPreScroll方法的實現(xiàn)
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    //判斷之前是否找到協(xié)同處理的父ViewGroup
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        //dx和dy分別表示X和Y軸上的滾動距離
        if (dx != 0 || dy != 0) { 
            int startX = 0;
            int startY = 0;
            //offsetInWindow用于計算滾動前后挪蹭,滾動列表本身的偏移量
            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;
            //分發(fā)給父ViewGroup
            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;
}

方法的第3個參數(shù)是一個長度為2的一維數(shù)組亭饵,用于記錄父ViewGroup(其實是父ViewGroup的子View)消費的滾動長度,若滾動距離沒有用完嚣潜,則滾動列表處理剩下的滾動距離冬骚;第4個參數(shù)也是一個長度為2的一維數(shù)組,用于記錄滾動列表本身的偏移量懂算,該參數(shù)用于修復(fù)用戶Touch事件的坐標只冻,以保證下一次滾動距離的正確性。

3.2 父ViewGroup就會把滾動事件分發(fā)給感興趣的子View

    //ViewParentCompat.java
    public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            IMPL.onNestedPreScroll(parent, target, dx, dy, consumed);
        }
    }

CoordinatorLayout實現(xiàn)了NestedScrollingParent接口计技,所以我們看下CoordinatorLayout.onNestedPreScroll方法:

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    int xConsumed = 0;
    int yConsumed = 0;
    boolean accepted = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams)view.getLayoutParams();
        //若子View對滾動事件不感興趣喜德,則直接跳過
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }

        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            mTempIntPair[0] = mTempIntPair[1] = 0;
            //分發(fā)給每個子View的Behavior處理
            viewBehavior.onNestedPreScroll(this, view,target, dx, dy, mTempIntPair);
            //找出每個子View消費的最大滾動距離就是父ViewGroup消費的滾動距離
            xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0]): Math.min(xConsumed, mTempIntPair[0]);
            yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1]): Math.min(yConsumed, mTempIntPair[1]);
            accepted = true;
        }
    }
    //記錄父ViewGroup消費的滾動距離
    consumed[0] = xConsumed;
    consumed[1] = yConsumed;
    if (accepted) {
        //處理子View之間的依賴關(guān)系
        dispatchOnDependentViewChanged(true);
    }
}

CoordinatorLayout的處理很簡單,把滾動事件分發(fā)給各個子View的Behavior.onNestedPreScroll方法處理垮媒,并計算出最終消費的滾動距離舍悯。

因此,我們可以在RecyclerView滾動之前睡雇,重寫Behavior.onNestedPreScroll方法中處理CoordinatorLayout的子View的滾動事件萌衬,然后根據(jù)實際情況填寫消費的滾動距離。

3.3 RecyclerView調(diào)用scrollByInternal事件分發(fā)給自己處理

假設(shè)RecyclerView的滾動距離沒有被CoordinatorLayout消費完它抱,那么接下來RecyclerView應(yīng)該處理這些滾動事件了秕豫。在RecyclerView的onTouchEvent方法中會調(diào)用scrollByInternal處理內(nèi)容滾動,關(guān)鍵代碼如下所示:

//x表示X軸上剩余的滾動距離
if (x != 0) {
    //交給具體的LayoutManager處理滾動事件,并且記錄下消費的和剩余的滾動量
    consumedX = mLayout.scrollHorizontallyBy(x, mRecycler,mState);
    unconsumedX = x - consumedX;
}
//y表示Y軸上剩余的滾動距離
if (y != 0) {
    //交給具體的LayoutManager處理滾動事件混移,并且記錄下消費的和剩余的滾動量
    consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
    unconsumedY = y - consumedY;
}

//...
//分發(fā)滾動列表本身對剩余滾動量的消費情況
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset);

如上所示祠墅,RecyclerView通過LayoutManager處理了剩余的滾動距離,如果onNestedPreScroll之后的剩余滾動量沒有被RecyclerView消耗完歌径,又可以分發(fā)給父ViewGroup毁嗦,父ViewGroup再分發(fā)給感興趣的子View的Behavior處理。這部分的代碼邏輯和onNestedPreScroll類似回铛,就不貼出了狗准,感興趣的可以直接看源碼。

因此勺届,我們可以在RecyclerView滾動時或滾動后驶俊,重寫Behavior.onNestedScroll方法處理CoordinatorLayout的子View的滾動事件,去消耗RecyclerView的滾動量

4. RecyclerView會在UP事件時stopNestedScroll

假設(shè)用戶結(jié)束滾動操作了免姿,即應(yīng)該結(jié)束一系列的滾動事件了饼酿,RecyclerView會在UP事件中調(diào)用stopNestedScroll方法,該方法和上面介紹的三個方法類似胚膊,都會先把事件分發(fā)給父ViewGroup故俐,然后父ViewGroup再把事件分到各個子View,最終觸發(fā)子View的Behavior.onStopNestedScroll方法紊婉,感興趣可以可接看源碼药版,此處不再貼出。

因此喻犁,我們可以在自定義的Behavior.onStopNestedScroll方法中檢測到滾動事件的結(jié)束槽片。

總結(jié):

整個嵌套滾動機制就介紹完了,可見跟我們直接打交道的就是CoordinatorLayout.Behavior類了肢础,通過重寫該類中的方法还栓,我們不僅可以監(jiān)聽滾動列表的滾動事件,還可以做很多其他的事情传轰。

下一篇會重點介紹:CoordinatorLayout.Behavior

摘抄總結(jié)自: Android CoordinatorLayout和Behavior

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剩盒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子慨蛙,更是在濱河造成了極大的恐慌辽聊,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件期贫,死亡現(xiàn)場離奇詭異跟匆,居然都是意外死亡,警方通過查閱死者的電腦和手機通砍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門贾铝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人埠帕,你說我怎么就攤上這事垢揩。” “怎么了敛瓷?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵叁巨,是天一觀的道長。 經(jīng)常有香客問我呐籽,道長锋勺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任狡蝶,我火速辦了婚禮庶橱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贪惹。我一直安慰自己苏章,他們只是感情好,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布奏瞬。 她就那樣靜靜地躺著枫绅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪硼端。 梳的紋絲不亂的頭發(fā)上并淋,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音珍昨,去河邊找鬼县耽。 笑死,一個胖子當著我的面吹牛镣典,可吹牛的內(nèi)容都是我干的兔毙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼骆撇,長吁一口氣:“原來是場噩夢啊……” “哼瞒御!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起神郊,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肴裙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涌乳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜻懦,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年夕晓,在試婚紗的時候發(fā)現(xiàn)自己被綠了宛乃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖征炼,靈堂內(nèi)的尸體忽然破棺而出析既,到底是詐尸還是另有隱情,我是刑警寧澤谆奥,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布眼坏,位于F島的核電站,受9級特大地震影響酸些,放射性物質(zhì)發(fā)生泄漏宰译。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一魄懂、第九天 我趴在偏房一處隱蔽的房頂上張望沿侈。 院中可真熱鬧,春花似錦市栗、人聲如沸缀拭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智厌。三九已至,卻和暖如春盲赊,著一層夾襖步出監(jiān)牢的瞬間铣鹏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工哀蘑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诚卸,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓绘迁,卻偏偏與公主長得像合溺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缀台,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容