4AppBarLayout滑動(dòng)原理

4AppBarLayout滑動(dòng)原理

CoordinatorLayout的measure和layout里矗晃,其實(shí)介紹過一點(diǎn)AppBarLayout旗唁,這篇將重點(diǎn)講解AppBarLayout的滑動(dòng)原理以及behavior是如何影響onTouchEvent與onInterceptTouchEvent的畦浓。

基本原理

介紹AppBarLayout的mTotalScrollRange,mDownPreScrollRange检疫,mDownScrollRange讶请,滑動(dòng)的基本概念
mTotalScrollRange內(nèi)部可以滑動(dòng)的view的高度(包括上下margin)總和

官方介紹

先來看看google的介紹
AppBarLayout is a vertical LinearLayout which implements many of the features of material designs app bar concept, namely scrolling gestures.

Children should provide their desired scrolling behavior through setScrollFlags(int) and the associated layout xml attribute: app:layout_scrollFlags.

This view depends heavily on being used as a direct child within a CoordinatorLayout. If you use AppBarLayout within a different ViewGroup, most of it's functionality will not work.

AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. The binding is done through the AppBarLayout.ScrollingViewBehavior behavior class, meaning that you should set your scrolling view's behavior to be an instance of AppBarLayout.ScrollingViewBehavior. A string resource containing the full class name is available.

簡單的整理下,AppBarLayout是一個(gè)vertical的LinearLayout屎媳,實(shí)現(xiàn)了很多material的概念夺溢,主要是跟滑動(dòng)相關(guān)的。AppBarLayout的子view需要提供layout_scrollFlags參數(shù)烛谊。AppBarLayout和CoordinatorLayout強(qiáng)相關(guān)风响,一般作為CoordinatorLayout的子類,配套使用丹禀。
按我的理解状勤,AppBarLayout內(nèi)部有2種view,一種可滑出(屏幕)双泪,另一種不可滑出持搜,根據(jù)app:layout_scrollFlags區(qū)分。一般上邊放可滑出的下邊放不可滑出的攒读。

舉個(gè)例子如下朵诫,內(nèi)有個(gè)Toolbar、TextView薄扁,Toolbar寫了app:layout_scrollFlags="scroll"表示可滑動(dòng)剪返,Toolbar高200dp,TextView高100dp邓梅。Toolbar就是可滑出的脱盲,TextView就是不可滑出的。此時(shí)框高300(200+100)日缨,內(nèi)容300钱反,可滑動(dòng)范圍200

總高度300,可滑出部分高度200匣距,剩下100不可滑出

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

        <TextView
            android:background="#ff0000"
            android:layout_width="match_parent"
            android:layout_height="100dp"></TextView>

    </android.support.design.widget.AppBarLayout>

效果如下所示

這個(gè)跟ScrollView有所不同面哥,框的大小和內(nèi)容大小一樣,這樣上滑的時(shí)候毅待,底部必然會(huì)空出一部分(200)尚卫,ScrollView的實(shí)現(xiàn)是通過修改scrollY,而AppBarLayout的實(shí)現(xiàn)是直接修改top和bottom的尸红,其實(shí)就是把整個(gè)AppBarLayout內(nèi)部的東西往上平移吱涉。

down事件

來看看上圖的事件傳遞的順序刹泄,先看down。簡單來說怎爵,這個(gè)down事件被傳遞下來特石,一直無人處理,然后往上傳到CoordinatorLayout被處理鳖链。但實(shí)際上CoordinatorLayout本身無法處理事件(他只是個(gè)殼)姆蘸,內(nèi)部實(shí)際交由AppBarLayout的behavior處理。

總體分析

首先撒轮,down事件從CoordinatorLayout傳到AppBarLayout再到TextView乞旦,沒人處理贼穆,然后回傳回來到AppBarLayout的onTouchEvent,不處理题山,再回傳給CoordinatorLayout的onTouchEvent,這里主要看L10 performIntercept故痊,type為TYPE_ON_TOUCH顶瞳。

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        //此處會(huì)分發(fā)事件給behavior
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } else if (cancelSuper) {
            if (cancelEvent == null) {
                final long now = SystemClock.uptimeMillis();
                cancelEvent = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            }
            super.onTouchEvent(cancelEvent);
        }

        if (!handled && action == MotionEvent.ACTION_DOWN) {

        }

        if (cancelEvent != null) {
            cancelEvent.recycle();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            resetTouchBehaviors();
        }

        return handled;
    }

再看performIntercept,type為TYPE_ON_TOUCH,首先獲取topmostChildList愕秫,這是把child按照z軸排序慨菱,最上面的排前面,CoordinatorLayout跟FrameLayout類似戴甩,越后邊的child符喝,在z軸上越靠上。所以甜孤,這里topmostChildList就是FloatingActionButton协饲、AppBarLayout。然后在for循環(huán)里調(diào)用behavior的onTouchEvent缴川。此時(shí)AppBarLayout.Behavior的onTouchEvent會(huì)返回true(具體后邊分析)茉稠,所以intercepted就為true,mBehaviorTouchView就會(huì)設(shè)置為AppBarLayout把夸,然后performIntercept結(jié)束返回true而线。這個(gè)mBehaviorTouchView就相當(dāng)于一般的ViewGroup里的mFirstTouchTarget的作用。再回頭看上邊代碼恋日,performIntercept返回true了膀篮,那就能進(jìn)入L13,會(huì)調(diào)用mBehaviorTouchView.behavior.onTouchEvent,在這里把CoordinatorLayout的onTouchEvent岂膳,傳遞給了AppBarLayout.Behavior的onTouchEvent誓竿。
而L16也會(huì)返回true,那整個(gè)CoordinatorLayout的onTouchEvent就返回true了闷营,按照事件分發(fā)的規(guī)則烤黍,此時(shí)這個(gè)down事件被CoordinatorLayout消費(fèi)了知市。但是實(shí)際上down事件的處理者是AppBarLayout.Behavior。他們之間通過mBehaviorTouchView連接速蕊。

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();
            嫂丙。。规哲。

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }

            ...
        }

        topmostChildList.clear();

        return intercepted;
    }

AppBarLayout.Behavior的onTouchEvent為何返回true

上文說了“此時(shí)AppBarLayout.Behavior的onTouchEvent會(huì)返回true”,我們來具體分析下跟啤。來看AppBarLayout.Behavior的onTouchEvent。AppBarLayout.Behavior的onTouchEvent代碼在HeaderBehavior內(nèi)唉锌,看L12只要觸摸點(diǎn)在AppBarLayout內(nèi)隅肥,而且canDragView,那就返回true袄简,否則返回false腥放。在AppBarLayout內(nèi)明顯是滿足的,那就看canDragView绿语。

   @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();

                if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
                    mLastMotionY = y;
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    ensureVelocityTracker();
                } else {
                    return false;
                }
                break;
            }
            秃症。。吕粹。        
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }

下邊是AppBarLayout的canDragView种柑,此時(shí)mLastNestedScrollingChildRef為null,所以走的是L16匹耕,返回true聚请,那回頭看上邊的onTouchEvent也返回true。

    @Override
        boolean canDragView(AppBarLayout view) {
            if (mOnDragCallback != null) {
                // If there is a drag callback set, it's in control
                return mOnDragCallback.canDrag(view);
            }

            // Else we'll use the default behaviour of seeing if it can scroll down
            if (mLastNestedScrollingChildRef != null) {
                // If we have a reference to a scrolling view, check it
                final View scrollingView = mLastNestedScrollingChildRef.get();
                return scrollingView != null && scrollingView.isShown()
                        && !ViewCompat.canScrollVertically(scrollingView, -1);
            } else {
                // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
                return true;
            }
        }

ps

可以看出在CoordinatorLayout的onTouchEvent處理down事件的過程中稳其,調(diào)用了2次AppBarLayout.Behavior的onTouchEvent

MOVE事件

由上文可知down事件被CoordinatorLayout消費(fèi)驶赏,所以move事件不會(huì)走到CoordinatorLayout的onInterceptTouchEvent,而直接進(jìn)入onTouchEvent欢际。此時(shí)mBehaviorTouchView就是AppBarLayout母市。看L10损趋,直接進(jìn)入患久,然后把move事件發(fā)給了AppBarLayout.Behavior。

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        //此處會(huì)分發(fā)事件給behavior
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }
            浑槽。蒋失。。

        return handled;
    }

AppBarLayout.Behavior處理move事件的代碼比較簡單桐玻,判斷超過mTouchSlop就調(diào)用scroll篙挽,而scroll等于調(diào)用setHeaderTopBottomOffset。這里主要關(guān)注scroll的后2個(gè)參數(shù)镊靴,minOffset和maxOffset铣卡,minOffset傳的是getMaxDragOffset(child)即AppBarlayout的-mDownScrollRange链韭。這里就是AppBarlayout的可滑動(dòng)范圍,即toolbar的高度(包括margin)的負(fù)值煮落。minOffset和maxOffset代表的是滑動(dòng)上下限制敞峭,這個(gè)很好理解,因?yàn)橐苿?dòng)的時(shí)候改的是top和bottom蝉仇,比如top范圍就是[initTop-滑動(dòng)范圍,initTop]旋讹,所以這里的minOffset是-mDownScrollRange,maxOffset是0.

//HeaderBehavior
 @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
                        mActivePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }

                final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
                int dy = mLastMotionY - y;

                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
                    mIsBeingDragged = true;
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                }

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    // We're being dragged so scroll the ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }
    
      final int scroll(CoordinatorLayout coordinatorLayout, V header,
            int dy, int minOffset, int maxOffset) {
        return setHeaderTopBottomOffset(coordinatorLayout, header,
                getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
    }

再看scroll里面轿衔,簡單調(diào)用setHeaderTopBottomOffset沉迹,重點(diǎn)看第三個(gè)參數(shù)getTopBottomOffsetForScrollingSibling() - dy,這個(gè)算出來的就是經(jīng)過這次move即將到達(dá)的offset(不是top哦害驹,top=offset+mLayoutTop)鞭呕。getTopBottomOffsetForScrollingSibling就是獲取當(dāng)前的偏移量,這個(gè)命名我不太理解裙秋。setHeaderTopBottomOffset就是給header設(shè)置一個(gè)新的offset琅拌,這個(gè)offset用一個(gè)min一個(gè)max來制約缨伊,很簡單摘刑。setHeaderTopBottomOffset可以認(rèn)為就是view的offsetTopAndBottom,調(diào)整top和bottom達(dá)到平移的效果

發(fā)現(xiàn)AppBarlayout對(duì)getTopBottomOffsetForScrollingSibling復(fù)寫了刻坊,加了個(gè)mOffsetDelta枷恕,但是mOffsetDelta一直是0.

  @Override
        int getTopBottomOffsetForScrollingSibling() {
            return getTopAndBottomOffset() + mOffsetDelta;
        }

measure過程

http://blog.csdn.net/litefish/article/details/52327502曾經(jīng)分析過簡單情況下CoordinatorLayout的布局過程。這里稍有變化谭胚,主要在于第三次measure RelativeLayout的時(shí)候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
就是availableHeight-AppBar.measuredheight+toolbar高度徐块,結(jié)果就是availableHeight。
所以此時(shí)RelativeLayout的最終measure高度是1731灾而,這個(gè)高度是有意義的胡控,他比不可滾動(dòng)的appbar多了一個(gè)toolbar的高度,這么高的一個(gè)RelativeLayout在當(dāng)前屏幕是放不下的旁趟,所以RelativeLayout往往會(huì)用一個(gè)可滾動(dòng)的view來替換昼激,比如Recyclerview或者NestedScrollView。

上滑可以滑到狀態(tài)欄

上滑用的是setTopAndBottomOffset锡搜,并不會(huì)重新measure橙困,layout,而fitSystemWindow是在measure耕餐,layout的時(shí)候發(fā)揮作用的

AppBarLayout的range

mTotalScrollRange 525
mDownPreScrollRange -1
mDownScrollRange 525

總結(jié)

1凡傅、ScrollView滑動(dòng)的實(shí)現(xiàn)是通過修改scrollY,而AppBarLayout的實(shí)現(xiàn)是通過直接修改top和bottom的肠缔,其實(shí)就是把整個(gè)AppBarLayout內(nèi)部的東西往上平移夏跷。
2哼转、CoordinatorLayout里的mBehaviorTouchView就相當(dāng)于一般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑動(dòng)一樣始終只有一個(gè)view可以fling槽华,不可能A fling完 B fling

參考文章

http://dk-exp.com/2016/03/30/CoordinatorLayout/
http://www.reibang.com/p/99adaad8d55c
https://code.google.com/p/android/issues/detail?id=177729

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末释簿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子硼莽,更是在濱河造成了極大的恐慌庶溶,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懂鸵,死亡現(xiàn)場(chǎng)離奇詭異偏螺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)匆光,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門套像,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人终息,你說我怎么就攤上這事夺巩。” “怎么了周崭?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵柳譬,是天一觀的道長。 經(jīng)常有香客問我续镇,道長美澳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任摸航,我火速辦了婚禮制跟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酱虎。我一直安慰自己雨膨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布读串。 她就那樣靜靜地躺著聊记,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爹土。 梳的紋絲不亂的頭發(fā)上甥雕,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音胀茵,去河邊找鬼社露。 笑死,一個(gè)胖子當(dāng)著我的面吹牛琼娘,可吹牛的內(nèi)容都是我干的峭弟。 我是一名探鬼主播附鸽,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瞒瘸!你這毒婦竟也來了坷备?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤情臭,失蹤者是張志新(化名)和其女友劉穎省撑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俯在,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竟秫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了跷乐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肥败。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖愕提,靈堂內(nèi)的尸體忽然破棺而出馒稍,到底是詐尸還是另有隱情,我是刑警寧澤浅侨,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布纽谒,位于F島的核電站,受9級(jí)特大地震影響仗颈,放射性物質(zhì)發(fā)生泄漏佛舱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一挨决、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧订歪,春花似錦脖祈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至眼虱,卻和暖如春喻奥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捏悬。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工撞蚕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人过牙。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓甥厦,卻偏偏與公主長得像纺铭,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刀疙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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