CoordinatorLayout Behavior一些筆記

最近因為需要研究一個滑動懸浮效果巴柿,偶然間發(fā)現(xiàn)了CoordinatorLayout這個很強大的布局雌贱,這個控件一般需要配合AppBarLayout啊送、CollapsingToolbarLayout使用來實現(xiàn)一些懸浮和漸變的高級效果,相關(guān)的使用文章有很多欣孤,這篇就不介紹這些了馋没,寫這篇的主要目的是要記錄一個問題,給CoordinatorLayout的子View設(shè)置Behavior后降传,Behavior 的layoutDependsOn和onDependentViewChanged方法是CoordinatorLayout何時進行回調(diào)來達到協(xié)調(diào)的目的的篷朵。
如果不是太了解CoordinatorLayout可以先看一下這兩篇文章:
CoordinatorLayout (這篇介紹了CoordinatorLayout 最基本的一個使用方式)
一步一步深入理解CoordinatorLayout( 這篇介紹了一下部分代碼)

下面開始我的分析和記錄

創(chuàng)建&&使用自定義的Behavior

  • 當我們想自定義Behavior時需要繼承CoordinatorLayout.Behavior<V extends View> ,例如我定義了如下Behavior
public class MyBehavior extends CoordinatorLayout.Behavior<Button> {
    public MyBehavior(Context context, AttributeSet attrs){
        super(context,attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
       return dependency instanceof TestTextView;
    }


    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
        //do something
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

然后當我使用時我可以在xml中定義一個Button類型的ChildView使用這個Behavior

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#FFCC00"
        android:text="Hello"
        app:layout_behavior="com.humorous.myapplication.coordinatorTest.behavior.MyBehavior"/>
    
    <com.humorous.myapplication.coordinatorTest.widget.TestTextView
        android:id="@+id/textView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#3366CC" />

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

這樣當我UI中TestTextView控件有相關(guān)變化時婆排,就會回調(diào)Behavior中的方法声旺,來改變我們的childView

何時回調(diào)layoutDependsOn 和onDependentViewChanged?

  • 這里才是我這篇文章想要記錄的重點段只,我很好奇腮猖,我的dependency View改變時CoordinatorLayout是怎么通知我的Behavior的,這里就需要貼一些源碼了
 private OnPreDrawListener mOnPreDrawListener;
....
@Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors();
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver(); 
            //在這里將實現(xiàn)了OnPreDrawListener的對象注冊到ViewTreeObserver中
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
            // We're set to fitSystemWindows but we haven't had any insets yet...
            // We should request a new dispatch of window insets
            ViewCompat.requestApplyInsets(this);
        }
        mIsAttachedToWindow = true;
    }
....
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

上面的代碼表明赞枕,CoordinatorLayout的一個內(nèi)部類OnPreDrawListener實現(xiàn)了ViewTreeObserver.OnPreDrawListener澈缺,然后注冊到了ViewTreeObserver上,OnPreDrawListener是ViewTreeObserver上的一個回調(diào)接口內(nèi)部聲明如下:

 /**
     * Interface definition for a callback to be invoked when the view tree is about to be drawn.
     */
    public interface OnPreDrawListener {
        /**
         * Callback method to be invoked when the view tree is about to be drawn. At this point, all
         * views in the tree have been measured and given a frame. Clients can use this to adjust
         * their scroll bounds or even to request a new layout before drawing occurs.
         *
         * @return Return true to proceed with the current drawing pass, or false to cancel.
         *
         * @see android.view.View#onMeasure
         * @see android.view.View#onLayout
         * @see android.view.View#onDraw
         */
        public boolean onPreDraw();
    }

這個接口會在viewTree準備繪制時回調(diào)炕婶,可以利用這個方法在繪制發(fā)生之前去調(diào)整滾動的邊界或者去請求一個新的layout姐赡,所以CoordinatorLayout就是在onPreDraw()方法中回調(diào)我們Behavior中的方法,具體的調(diào)用方法就是CoordinatorLayout 中onChildViewsChanged柠掂,該方法的代碼如下(省略部分):

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i < childCount; i++) {
             //獲取根據(jù)z軸排序的子View
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Get the current draw rect of the view
            getChildRect(child, true, drawRect);

            // Accumulate inset sizes
          
            ....//省略部分代碼

            // Dodge inset edges if necessary
           
           ....//省略部分代碼
 
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                //獲取i+1位置開始的ChildView
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                 //獲取Child的Behavior
                final Behavior b = checkLp.getBehavior();
                //Child的Behavior不為空雏吭,并且Behavior的b.layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            // 在這里回調(diào)了Behavior的b.onDependentViewChanged方法來通知ChildView的dependency發(fā)生了改變
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }

        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }

看到這個方法,我心中的疑惑就解開了陪踩,當子View改變時杖们,會引起ViewTree重新繪制,然后因為CoordinatorLayout 設(shè)置了OnPreDrawListener會在重新繪制前通知CoordinatorLayout肩狂,CoordinatorLayout在通過調(diào)用onChildViewsChanged來遍歷子View摘完,因為子View已經(jīng)經(jīng)過排序傻谁,遍歷到每一個子View時孝治,會在去遍歷當前這個子View之后的View,過程如下:

  // Update any behavior-dependent views for the change
          for (int j = i + 1; j < childCount; j++) {
                //省略具體代碼
         }

然后在遍歷到每一個i+1位置開始時的子View時岂座,會獲取這個子View 的LayoutParams,然后調(diào)用getBehavior方法獲取Behavior杭措,過程如下:

for (int j = i + 1; j < childCount; j++) {
              //獲取i+1位置開始的ChildView
              final View checkChild = mDependencySortedChildren.get(j);
              final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
               //獲取Child的Behavior
              final Behavior b = checkLp.getBehavior();
              //省略后面的代碼
        }

在拿到這個Behavior后费什,如果這個Behavior不為空手素,并且Behavior的layoutDependsOn返回了true鸳址,代表j位置的子View依賴于i位置的子View泉懦,才會回調(diào)Behavior的onDependentViewChanged稿黍,過程如下:

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                //Child的Behavior不為空,并且Behavior的b.layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                   //省略部分代碼崩哩。巡球。邓嘹。
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                           // 在這里回調(diào)了Behavior的b.onDependentViewChanged方法來通知ChildView的dependency發(fā)生了改變
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }

我自定的Behavior中的實現(xiàn):

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
        return dependency instanceof TestTextView;
    }
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
        //do something
        return super.onDependentViewChanged(parent, child, dependency);
    }

這樣此時的i位置的子View是TestTextView類型時,我的layoutDependsOn會返回true钉嘹,然后會回調(diào)onDependentViewChanged鸯乃,我可以在拿到Button child, View dependency后,可以做一些變化操作鸟悴,例如開頭推薦閱讀的第一篇文章中的效果。
最后奖年,其實onChildViewsChanged并不只是在onPreDraw中才會回調(diào),通過入?yún)⑽覀兛梢钥吹秸鸸螅琽nChildViewsChanged需要傳入一個@DispatchChangeEvent final int type的參數(shù)水评,這個type一共有三種類型

    static final int EVENT_PRE_DRAW = 0;
    static final int EVENT_NESTED_SCROLL = 1;
    static final int EVENT_VIEW_REMOVED = 2;

最后我們可以通過查看onChildViewsChanged方法前面的描述來知道它的作用到底是在做什么猩系,這里我只把這個描述貼出來中燥,就不做翻譯了,因為我的翻譯水平有限,會破壞了原有的意境

/**
     * Dispatch any dependent view changes to the relevant {@link Behavior} instances.
     *
     * Usually run as part of the pre-draw step when at least one child view has a reported
     * dependency on another view. This allows CoordinatorLayout to account for layout
     * changes and animations that occur outside of the normal layout pass.
     *
     * It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting
     * is completed within the correct coordinate window.
     *
     * The offsetting behavior implemented here does not store the computed offset in
     * the LayoutParams; instead it expects that the layout process will always reconstruct
     * the proper positioning.
     *
     * @param type the type of event which has caused this call
     */
    final void onChildViewsChanged(@DispatchChangeEvent final int type)
到這就結(jié)束了吟秩,在這里簡單的做個筆記绽淘,對于源碼的閱讀,雖然相比剛畢業(yè)那會清晰了不少收恢,但是還有些不那么流暢,這東西估計只能一點點來靠時間的積累了伦意,希望讀過的人可以對整個過程有些了解,也歡迎大家對發(fā)現(xiàn)的錯誤批評指正~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熏矿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子票编,更是在濱河造成了極大的恐慌卵渴,老刑警劉巖慧域,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浪读,死亡現(xiàn)場離奇詭異,居然都是意外死亡碘橘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門仰禽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吐葵,你說我怎么就攤上這事∥虑停” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵诚镰,是天一觀的道長。 經(jīng)常有香客問我清笨,道長,這世上最難降的妖魔是什么抠艾? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮检号,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘翘盖。我一直安慰自己,他們只是感情好馍驯,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著汰瘫,像睡著了一般擂煞。 火紅的嫁衣襯著肌膚如雪混弥。 梳的紋絲不亂的頭發(fā)上对省,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音官辽,去河邊找鬼粟瞬。 笑死,一個胖子當著我的面吹牛裙品,可吹牛的內(nèi)容都是我干的钠右。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼农尖,長吁一口氣:“原來是場噩夢啊……” “哼入宦!你這毒婦竟也來了帅腌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤速客,失蹤者是張志新(化名)和其女友劉穎五鲫,沒想到半個月后溺职,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體位喂,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年七冲,在試婚紗的時候發(fā)現(xiàn)自己被綠了规婆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片癞埠。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡聋呢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出削锰,到底是詐尸還是另有隱情,我是刑警寧澤颅夺,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站吧黄,受9級特大地震影響唆姐,放射性物質(zhì)發(fā)生泄漏拗慨。R本人自食惡果不足惜奉芦,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望声功。 院中可真熱鬧,春花似錦先巴、人聲如沸冒冬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽证逻。三九已至乐埠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丈咐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工棵逊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留银酗,地道東北人辆影。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓黍特,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灭衷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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

  • 前言&效果預(yù)覽 最近幾個周末基本在研究 CoordinatorLayout 控件和自定義 Behavior 當中迫像,...
    HelloCsl閱讀 31,700評論 52 386
  • CoordinatorLayout 終于到這個控件了瞳遍,其實我的內(nèi)心是忐忑的闻妓,因為我其實一直想要深入的理解 Coor...
    Anonymous___閱讀 1,981評論 3 18
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,506評論 25 707
  • 關(guān)系由缆,是用來處理我們內(nèi)在心靈中的“壞”的份蝴。 這句話發(fā)人深省犁功,或者反過來說我們要修煉內(nèi)心中的好婚夫,必須是在關(guān)系中淬煉署鸡。...
    w小郭閱讀 148評論 0 0
  • 聲明全局常量 在頭文件中使用extern來聲明全局常量,并在相關(guān)實現(xiàn)文件中定義其值.這種常量要出現(xiàn)在全局符號表中,...
    zbj_閱讀 507評論 0 50