深入解析Android Design包——Behavior

上一篇深入解析AndroidDesign包——NestedScroll 已經(jīng)說過了,在AndroidDesign包中主要有兩個核心概念:一是NestedScroll,另一個就是Behavior。
相比于NestedScroll這個概念來說,Behavior分析起來會難很多,因為它幾乎遍布了AndroidDesign包的每一個控件,種類繁多;另外Behavior提供了二十多個空方法給使用者來重寫,主要分為四類:
1.與Touch事件相關(guān)的方法
2.與NestedScroll相關(guān)的方法
3.與控件依賴相關(guān)的方法(依賴這個概念可能接觸的不多,就是如果A依賴B,那么當(dāng)B變化時會通知A跟著變化)
4.其他方法,如測量和布局等
由此可見,Behavior的使用是非常靈活的,所以功能也是非常的強(qiáng)大。但是,對于越靈活的東西,就越難將它講清除。它有一百種用法,總不能我就舉出一百個例子來進(jìn)行說明,因此本文只能起到一個拋磚引玉的作用,要真正融會貫通還得靠各位自己去揣摩。

從CoordinatorLayout入手

好好的干嘛扯到CoordinatorLayout呢?
如果你這么問那你就外行了,因為如果沒有CoordinatorLayout,光有Behavior是啥用都沒有滴。
CoordinatorLayout就是一個容器,主要功能就是為它里面的控件傳遞命令,更準(zhǔn)確的說就是使用Behavior來讓子控件們相互調(diào)用。
CoordinatorLayout有自己的LayoutParams類

    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        /**
         * A {@link Behavior} that the child view should obey.
         */
        Behavior mBehavior;

它的布局參數(shù)類定義的第一個屬性就是Behavior,而且還有l(wèi)ayout_behavior屬性供布局文件使用,可在布局文件中為CoordinatorLayout內(nèi)部的控件設(shè)置behavior對象。
另外,所有的Behavior的祖宗都是CoordinatorLayout.Behavior,這是一個靜態(tài)-內(nèi)部-虛擬類,頭銜有點長~ 我們抓住這個靜態(tài)內(nèi)部類就算是抓到Behavior的精髓了。
除了以上兩點,最重要的一層關(guān)系是:所有Behavior的方法都是在CoordinatorLayout中調(diào)用的,比如來了個NestedScroll事件,那么CoordinatorLayout會調(diào)用自己的onNestedScroll()方法,然后在方法內(nèi)部,就會調(diào)用childView的behavior對應(yīng)的onNestedScroll()方法了。
具體過程,我們來詳細(xì)分析。

如何處理Touch相關(guān)事件

找到CoordinatorLayout中的Behavior類,可以發(fā)現(xiàn)該類中定義了如下兩個方法:

        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

很顯然,這是攔截觸摸事件和處理觸摸事件的方法。
我們看看這兩個方法是如何被CoordinatorLayout調(diào)用的。

先看onInterceptTouchEvent

根據(jù)View的事件體系可知,對事件是否攔截的處理在onInterceptTouchEvent()方法中,于是找到CoordinatorLayout的這個方法:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        // Make sure we reset in case we had missed a previous important event.
        if (action == MotionEvent.ACTION_DOWN) {
            resetTouchBehaviors();
        }
        //這里才是處理事件攔截的代碼
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

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

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

        return intercepted;
    }

從上面代碼可知,真正的處理邏輯在performIntercept()方法中,注意它的第二個參數(shù)TYPE_ON_INTERCEPT。然后再看performIntercept方法:

private boolean performIntercept(MotionEvent ev, final int type) {

        ...
        //遍歷所有顯示出來了的childView
        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 || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // 如果事件已經(jīng)被攔截,那么向其他childView發(fā)送cancelEvent
                //...省略代碼...
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    //如果事件未攔截,且childView設(shè)置了behavior,則進(jìn)行攔截判斷
                    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;
    }

對代碼進(jìn)行簡化之后,邏輯就很明顯了。先從childView中取出LayoutParams對象,然后從LayoutParams對象中取出Behavior對象,如果performIntercept()方法第二個參數(shù)傳進(jìn)來的是TYPE_ON_INTERCEPT,則調(diào)用behavior.onInterceptTouchEvent()方法判斷是否攔截事件。換句話說就是,是否攔截事件跟CoordinatorLayout本身沒有一毛錢關(guān)系。

再看onTouchEvent

直接看CoordinatorLayout中的onTouchEvent()方法源碼:

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

        final int action = MotionEventCompat.getActionMasked(ev);
        
        //先判斷mBehaviorTouchView是否為null,如果不為null,則不會執(zhí)行后面的performIntercept()
        //如果等于null,則調(diào)用performIntercept方法,該方法如果返回true會對mBehaviorTouchView賦值
        if (mBehaviorTouchView != null 
                || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
                
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // 經(jīng)過上面的兩重判斷之后,如果mBehavior還是null,則說明childView不消費(fèi)touch事件
        // 那么該touch事件交給CoordinatorLayout的parent去處理
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } 
        ...
        return handled;
    }

上面的代碼加了注釋之后,應(yīng)該不需要多說什么了。
還是那句話,CoordinatorLayout本身也不會消費(fèi)touch事件。

如何處理NestedScroll相關(guān)事件

先看Behavior中跟NestedScroll相關(guān)的方法

        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }

        public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Do nothing
        }

        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
            // Do nothing
        }

        public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            // Do nothing
        }

        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // Do nothing
        }

        public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY, boolean consumed) {
            return false;
        }

        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY) {
            return false;
        }

大家不要被這么多方法給嚇到了,如果你有閱讀這篇文章深入解析Android Design包——NestedScroll 就能夠發(fā)現(xiàn),這些方法都是NestedScrollingParent接口中定義的方法。并且CoordinatorLayout本身是實現(xiàn)了NestedScrollingParent接口的,那么CoordinatorLayout會如何調(diào)用Behavior的這些方法呢? 肯定是一一對應(yīng)的來調(diào)用。
我想Google這么設(shè)計的目的應(yīng)該是為了解耦,只要給控件提供一個Behavior就可以擁有NestedScrollingParent的功能,這樣一來控件本身就與NestedScrollingParent完全無關(guān)了。
由于方法比較多,這里就不一一展示調(diào)用過程了,挑onNestedScroll方法來說一下吧。
先看CoordinatorLayout中的onNestedScroll方法:

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        final int childCount = getChildCount();
        boolean accepted = false;

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            //獲取childView的behavior,并調(diào)用behavior的onNestedScroll方法
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

代碼邏輯非常清晰,就是直接把nestedScroll事件通過behavior傳遞給childView去處理。
但是,我們注意到最后一段代碼,調(diào)用了一個onChildViewsChanged()方法。
這個方法具體邏輯我們在下一小結(jié)分析,它主要是處理那些依賴控件的。之所以在此處加一句,是為了那些跟滑動控件存在依賴關(guān)系的其他控件,也可以做出響應(yīng)。

如何處理依賴相關(guān)事件

接下來,我們來看看Behavior中依賴相關(guān)的方法

    //判斷child和dependency是否存在依賴關(guān)系
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
    
    //dependency發(fā)生改變時,回調(diào)此方法
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

    //dependency被移除時,回調(diào)此方法
    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
        }

要完成上面三個方法的使命,需要滿足兩點:
1.需要對CoordinatorLayout所有childView進(jìn)行兩兩判斷,看它們是否存在依賴關(guān)系。
2.當(dāng)一個childView發(fā)生布局改變時,CoordinatorLayout需要回調(diào)通知與其有依賴關(guān)系的其他childView。

判斷依賴關(guān)系

一個View在Android系統(tǒng)中的顯示都是:onMeasure, onLayout, onDraw
所以先看onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ...
    }

沒想到第一句就看到重點了,prepareChildren就是為了整理CoordinatorLayout內(nèi)部的childView,自然也會將childView之間的依賴關(guān)系確定好,來看代碼:

private void prepareChildren() {
        mDependencySortedChildren.clear(); //List<View> 
        mChildDag.clear(); //圖結(jié)構(gòu) --- 無回路有向圖
        //遍歷所有childView
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);

            mChildDag.addNode(view);
            //再次遍歷,需要雙重遍歷才能將childView兩兩判斷dependency
            //將判斷的結(jié)果保存在有向圖mChildDag中
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                final LayoutParams otherLp = getResolvedLayoutParams(other);
                //這個dependsOn方法就是判斷依賴關(guān)系的,內(nèi)部會調(diào)用Behavior.layoutDependsOn()方法
                if (otherLp.dependsOn(this, other, view)) { 
                    if (!mChildDag.contains(other)) {
                        mChildDag.addNode(other);
                    }
                    mChildDag.addEdge(view, other);
                }
            }
        }
        //將圖中的數(shù)據(jù)排序,并保存在List中
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //將List倒序設(shè)置,讓被依賴的childView排在前面,依賴于它的排在后面
        Collections.reverse(mDependencySortedChildren);
    }

這里涉及到一種比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)——無回路有向圖,篇幅有限就不在這里多說,我們只要知道會調(diào)用layoutDependsOn方法來判斷依賴關(guān)系,然后將數(shù)據(jù)最后保存在mDependencySortedChildren這個List中。
這個mDependencySortedChildren列表中保存的都是childView,不過是按照特定的順序進(jìn)行了排序:
如果childView被其他view依賴的次數(shù)最多,則排在最前面,以此類推。
至于依賴關(guān)系并沒有保存,到時候要用到時,再次調(diào)用layoutDependsOn方法來判斷,寫到這里我好像明白了為什么要將依賴次數(shù)多的放列表前面了。

childView發(fā)生布局改變

OK,依賴關(guān)系確定了,那就看看當(dāng)childView發(fā)生改變時,如何讓依賴的view跟著改變。
其實在NestedScroll相關(guān)方法中,最后都會調(diào)用一句代碼

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }

上面也有提到,這個方法就是處理依賴控件的變化的。在分析它之前,還有必要看看其他地方有沒有調(diào)用此方法。
然后,就看到了在onAttachToWindow()方法中,為CoordinatorLayout設(shè)置了OnPreDrawListener 回調(diào),也就是說在執(zhí)行onDraw之前,回執(zhí)行onPreDraw方法中的代碼。
我們先來看OnPreDrawListener:

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

onPreDraw()的代碼很簡單,就是執(zhí)行onChildViewsChanged()方法,也就是說每次onDraw都會調(diào)用這個方法來處理依賴控件。

接下來重點看onChildViewsChanged()方法了。

final void onChildViewsChanged(@DispatchChangeEvent final int type) {

        final int childCount = mDependencySortedChildren.size();
    
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // ... 省略了對anchor和inset相關(guān)的代碼 ...       
            //可以說,上面的代碼都是為了提升效率,下面的才是真正的處理邏輯
            
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                //獲取對應(yīng)的Behavior對象
                final Behavior b = checkLp.getBehavior();

                //判斷依賴關(guān)系
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        //當(dāng)NestedScroll 和 Draw都會觸發(fā)這個方法,
                        //這里二者只能有一個繼續(xù)往下執(zhí)行
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // 調(diào)用Behavior對應(yīng)的方法,通知依賴這個child的其他view
                            //這里的child的被依賴,checkChild是依賴于它
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // 除了刪除事件,其他的都調(diào)用下面的方法
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        //這里跟上面的checkLp.getChangedAfterNestedScroll()對應(yīng)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
        ...
    }

同樣的,為了突出處理邏輯,把一些代碼省略掉了。
代碼不長,希望大家根據(jù)我的注釋讀一下代碼,自然就明白了。
其實,onChildViewsChanged這個方法也只是在調(diào)用Behavior的相關(guān)方法而已,也就是說如果childA依賴于childB,那么當(dāng)childB發(fā)生布局變化時,childB的Behavior就會把這個變化同時作用到childA身上。

結(jié)語

說到這里,一句話總結(jié)CoordinatorLayout本身啥事兒也不干,全讓底下的childView去干了。
而Behavior之所以強(qiáng)大,是因為在我們不修改View的情況下,可以對View的行為進(jìn)行修改和控制。

本來是要舉例說明一下Behavior的具體用法的,但是沒想到一寫一寫已經(jīng)這么長了,太長的文章不利于閱讀,也不利于學(xué)習(xí)吸收,就只能把舉例說明移到下一篇去寫了。
這里大概說一下會分析一個什么樣的示例吧。
布局層級大概就是下面這個樣子

<CoordinatorLayout> 

    <AppBarLayout>
        <CollapsingToolbarLayout />
    </AppBarLayout>
    
    <NestedScrollView />
    
</CoordinatorLayout>

這個例子涉及到四個控件,之所以選它來說明Behavior的用法是因為,這個例子中Android都為我們提供了相應(yīng)的Behavior,分析起來也更有權(quán)威性。
如果大家感興趣,就敬請期待吧~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容