淺析NestedScrolling嵌套滑動機(jī)制之CoordinatorLayout.Behavior

預(yù)覽

嵌套系列導(dǎo)航

本文已在公眾號鴻洋原創(chuàng)發(fā)布。未經(jīng)許可,不得以任何形式轉(zhuǎn)載似嗤!

概述

在前面《淺析NestedScrolling嵌套滑動機(jī)制之基礎(chǔ)篇》里的常見效果提到Behavior也是走NestedScrolling機(jī)制來實現(xiàn)各種神奇的滑動效果泳唠,它伴隨CoordinatorLayout在Revision 24.1.0的android.support.v4兼容包被引入,和CoordinatorLayout結(jié)合實現(xiàn)各個控件聯(lián)動,可以攔截代理CoordinatorLayout的測量、布局、WindowInsets躯肌、觸摸事件、嵌套滑動破衔。

Behavior簡介

Behavior是作用于 CoordinatorLayout的直接子View 的交互行為插件清女。一個Behavior 實現(xiàn)了用戶的一個或者多個交互行為,它們可能包括拖拽晰筛、滑動嫡丙、快滑或者其他一些手勢。

    /**
     * 泛型<V>是Behavior關(guān)聯(lián)的View
     */
    public static abstract class Behavior<V extends View> {

        /**
        * 默認(rèn)構(gòu)造方法,用于注解的方式創(chuàng)建或者在代碼中創(chuàng)建
        */
        public Behavior() {}

        /**
        * 用于xml解析layout_Behavior屬性的構(gòu)造方法,如果需要Behavior支持在xml中使用,則必須有此構(gòu)造方法
        */
        public Behavior(Context context, AttributeSet attrs) {}

        /**
        * 在LayoutParams實例化后調(diào)用,或者在調(diào)用了LayoutParams.setBehavior(behavior)時調(diào)用.
        */
        public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}

        /**
        * 同上面onAttachedToLayoutParams相反
        * 當(dāng)LayoutParams移除Behavior時調(diào)用,例如調(diào)用了LayoutParams.setBehavior(null).
        * View被從View Tree中移除時不會調(diào)用此方法.
        */
        public void onDetachedFromLayoutParams() {}

        /**
        * 在CoordinatorLayout分發(fā)給子View前攔截Touch事件
        */
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        /**
        * 在CoordinatorLayout分發(fā)給子View前消費(fèi)Touch事件
        */
        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        /**
        * 阻斷此Behavior所關(guān)聯(lián)View下層的View的交互
        */
        public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
            return getScrimOpacity(parent, child) > 0.f;
        }

        /**
        * 當(dāng)blocksInteractionBelow返回為true時读第,CoordinatorLayout將會在View的上層繪制
        * 一個屏蔽的getScrimColor()顏色來顯示無法進(jìn)行交互的區(qū)域
        */
        @ColorInt
        public int getScrimColor(CoordinatorLayout parent, V child) {
            return Color.BLACK;
        }

        /**
        * getScrimColor()繪制顏色的透明度
        */
        @FloatRange(from = 0, to = 1)
        public float getScrimOpacity(CoordinatorLayout parent, V child){
            return 0.f;
        }

        /**
        * 關(guān)聯(lián)的View和感興趣的View進(jìn)行依賴
        */
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
        * 依賴View的位置曙博、大小改變時回調(diào)
        */
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
        * 依賴View從布局移除時回調(diào)
        */
        public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}

        /**
        * 代理CoordinatorLayout子View的測量,注意這個子View是關(guān)聯(lián)了當(dāng)前Behavior,
        * 返回true表示使用Behavior的*onMeasureChild()來測量參數(shù)里child的這個子View怜瞒,
        * 返回false則使用*CoordinatorLayout的默認(rèn)測量子View的方法父泳。
        */
        public boolean onMeasureChild(CoordinatorLayout parent, V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            return false;
        }

        /**
        * 代理CoordinatorLayout子View的布局
        * 返回true表示使用Behavior的onLayoutChild()來布局子View
        * 返回false則使用CoordinatorLayout的默認(rèn)測量子View的方法。
        */
        public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
            return false;
        }
        
        /**
        *代理消費(fèi)CoordinatorLayout的WindowInsets
        */
        @NonNull
        public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
                V child, WindowInsetsCompat insets) {
            return insets;
        }

        //以下是NestedScrolling相關(guān)方法//
        @Deprecated
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes) {
            return false;
        }

        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
            return false;
        }

        @Deprecated
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes) {
        }

        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
        }

        @Deprecated
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target) {
        }

        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onStopNestedScroll(coordinatorLayout, child, target);
            }
        }

        @Deprecated
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
        }

        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            }
        }

        @Deprecated
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        }

        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
            }
        }

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

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

        //省略部分非常用方法
        ...
    }

View設(shè)置Behavior

xml布局文件設(shè)置

<!-- 布局文件 -->
<android.support.design.widget.CoordinatorLayout>
    <android.support.v4.widget.NestedScrollView 
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

<!-- values.xml -->
<string name="appbar_scrolling_view_behavior" translatable="false">
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
</string>

在布局文件對CoordinatorLayout的直接子View添加app:layout_behavio屬性,屬性是Behavior類全限包名惠窄,你可以把值放在values文件里蒸眠,也可以直接寫在布局文件里。在CoordinatorLayout的parseBehavior()調(diào)用Behavior兩個參數(shù)的構(gòu)造方法創(chuàng)建杆融。

代碼動態(tài)設(shè)置

    AppBarLayout.ScrollingViewBehavior behavior = new AppBarLayout.ScrollingViewBehavior();
    CoordinatorLayout.LayoutParams params =(CoordinatorLayout.LayoutParams) view.getLayoutParams();
    params.setBehavior(behavior);

注解方式

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}

注意如果同時使用注解和xml布局文件為同一個view設(shè)置Behavior楞卡,生效的是注解方式的Behavior,若在自定義Behavior使用此方式需要一個無參的構(gòu)造函數(shù),因為CoordinatorLayout在getResolvedLayoutParams()解析時調(diào)用反射Behavior的無參構(gòu)造函數(shù)創(chuàng)建擒贸,而這種注解方式在support27.1.0版本打上了@Deprecated過時標(biāo)簽臀晃。

接口實現(xiàn)返回

View實現(xiàn)CoordinatorLayout.AttachedBehavior接口并復(fù)寫getBehavior()返回Behavior觉渴。在CoordinatorLayout在getResolvedLayoutParams()解析時調(diào)用getBehavior()獲取Behavior介劫,然后調(diào)用CoordinatorLayout.LayoutParams.setBehavior()傳入。

public class MyLayout extends LinearLayout implements CoordinatorLayout.AttachedBehavior{
    @NonNull
    @Override
    Behavior getBehavior(){
        return new AppBarLayout.ScrollingViewBehavior()
    };
}

Behavior中的代理

代理CoordinatorLayout子View的測量

Behavior的onMeasureChild()可以代理CoordinatorLayout子View的測量案淋,注意這個子View是關(guān)聯(lián)了當(dāng)前Behavior座韵,它的返回值為Boolean類型,返回true表示使用Behavior的onMeasureChild()來測量參數(shù)里child的這個子View踢京,返回false則使用CoordinatorLayout的默認(rèn)測量子View的方法誉碴。

    //CoordinatorLayout.Behavior
    public boolean onMeasureChild(CoordinatorLayout parent, V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            return false;
    }

在CoordinatorLayout的onMeasure()里可以看出Behavior中的代理子View的測量:

    //CoordinatorLayout
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            ...
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ...
            //Behavior判空檢測是否可以代理measure
            final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
            ...
        }
    }

代理CoordinatorLayout子View的布局

和上面類似,Behavior的onLayoutChild()可以代理CoordinatorLayout子View的布局瓣距,它的返回值為Boolean類型黔帕,返回true表示使用Behavior的onLayoutChild()來布局子View,返回false則使用CoordinatorLayout的默認(rèn)測量子View的方法蹈丸。

    //CoordinatorLayout.Behavior
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        return false;
    }

在CoordinatorLayout的onLayout()里可以看出Behavior中的代理子View的布局:

    //CoordinatorLayout
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ...
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            ...
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();
            //Behavior判空檢測是否可以代理layout
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

代理CoordinatorLayout的WindowInsets

Behavior的onApplyWindowInsets()可以代理消費(fèi)CoordinatorLayout的WindowInsets成黄。

    //CoordinatorLayout.Behavior
    public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
            V child, WindowInsetsCompat insets) {
        return insets;
    }

在CoordinatorLayout的onLayout()里可以看出Behavior中的消費(fèi)CoordinatorLayout的WindowInsets:
setFitsSystemWindows()->setupForInsets()->setWindowInsets()->dispatchApplyWindowInsetsToBehaviors()

    //CoordinatorLayout
    private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
        ...
        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);
            if (ViewCompat.getFitsSystemWindows(child)) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final Behavior b = lp.getBehavior();

                if (b != null) {
                    // If the view has a behavior, let it try first
                    insets = b.onApplyWindowInsets(this, child, insets);
                    if (insets.isConsumed()) {
                        // If it consumed the insets, break
                        break;
                    }
                }
            }
        }
        return insets;
    }

代理CoordinatorLayout的Touch事件

Behavior的onInterceptTouchEvent()、onTouchEvent()可以在CoordinatorLayout分發(fā)給子View前被攔截消費(fèi)逻杖,若Behavior攔截了來自CoordinatorLayout的Touch事件奋岁,CoordinatorLayout的各個子View自然就接受不到Touch事件,Behavior的blocksInteractionBelow()表示是否阻斷此Behavior所關(guān)聯(lián)View下層的View的交互荸百,則這個方法能影響Touch事件的攔截闻伶,若blocksInteractionBelow()為true時,getScrimOpacity()返回值大于0够话,CoordinatorLayout將會在View的上層繪制一個屏蔽的getScrimColor()顏色來顯示無法進(jìn)行交互的區(qū)域:

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

    public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
            return getScrimOpacity(parent, child) > 0.f;
    }

    public float getScrimOpacity(CoordinatorLayout parent, V child) {
        return 0.f;
    }

    public int getScrimColor(CoordinatorLayout parent, V child) {
        return Color.BLACK;
    }

接下來看看CoordinatorLayout的onInterceptTouchEvent()蓝翰、onTouchEvent()如何被Behavior代理:

    //CoordinatorLayout
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        ...
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;
        ...
        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();
            //Behavior不為空,事件分發(fā)給Behavior
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct(走CoordinatorLayout默認(rèn)方法)
        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);
        }
        ...
        return handled;
    }

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        //記錄是否Behavior的blocksInteractionBelow()返回true女嘲,根據(jù)這個標(biāo)
        //識來給剩余遍歷的Behavior分發(fā)個CANCEL的MotionEvent
        boolean newBlock = false;
        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();
        //根據(jù)View的層級由高到低排序畜份,儲放在臨時的容器
        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        //(先遍歷最外層View的Behavior的Touch事件代理)
        // 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();
            //若Touch事件已經(jīng)被前面遍歷的Behavior攔截或者newBlock為true表示前面遍歷的Behavior已阻斷交互、且action不是DOWN時
            //那么后面剩余遍歷的Behavior分發(fā)個CANCEL的MotionEvent
            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent == null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            //沒有攔截Touch事件澡为,Behavior不為空漂坏,事件分發(fā)給Behavior
            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;
                }
                //如果Behavior攔截了Touch事件,標(biāo)記其關(guān)聯(lián)的View
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }

            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                //這里要考慮onInterceptTouchEvent()進(jìn)入performIntercept()Behavior阻斷過,
                //再到onTouchEvent()進(jìn)入performIntercept()就不必再遍歷
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }
        topmostChildList.clear();
        return intercepted;
    }

    //CoordinatorLayout.LayoutParams
    /**
    * Behavior是否之前已經(jīng)阻斷過此Behavior所關(guān)聯(lián)View下層的View的交互
    */
    boolean didBlockInteraction() {
        if (mBehavior == null) {
            mDidBlockInteraction = false;
        }
        return mDidBlockInteraction;
    }

    /**
    * Behavior已經(jīng)阻斷過此Behavior所關(guān)聯(lián)View下層的View的交互返回true顶别,
    * 否則返回調(diào)用Behavior的blocksInteractionBelow并記錄已阻斷過
    */
    boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
        if (mDidBlockInteraction) {
            return true;
        }
        return mDidBlockInteraction |= mBehavior != null
                ? mBehavior.blocksInteractionBelow(parent, child)
                : false;
    }    

CoordinatorLayout的onInterceptTouchEvent()執(zhí)行攔截主要邏輯在performIntercept()里:

  • 1.首先根據(jù)子View的層級由高到低排序后按順序遍歷子View的Behavior谷徙;
  • 2.在遍歷中先判斷Touch事件已經(jīng)被前面遍歷的Behavior攔截或者阻斷、且不是DOWN事件驯绎,若符合這些條件則給剩余遍歷的Behavior分發(fā)個CANCEL的MotionEvent完慧;
  • 3.然后將根據(jù)參數(shù)type調(diào)用Behavior對應(yīng)的事件攔截、消費(fèi)的方法剩失,如果Behavior攔截了Touch事件則以變量mBehaviorTouchView記錄其關(guān)聯(lián)的View屈尼;
  • 4.接著調(diào)用CoordinatorLayout.LayoutParams的兩個判斷阻斷交互方法用變量newBlock記錄Behavior的阻斷交互。

CoordinatorLayout的onTouchEvent()邏輯如下:

  • 1.先判斷之前在onInterceptTouchEvent()是否有記錄mBehaviorTouchView拴孤,若有則直接調(diào)用Behavior的onTouchEvent()脾歧;若無則調(diào)用performIntercept()且返回值賦值變量cancelSuper;
  • 2.若cancelSuper為true說明已有Behavior調(diào)用onTouchEvent()消費(fèi)Touch事件了并記錄mBehaviorTouchView,然后通過mBehaviorTouchView的LayoutParam 再次調(diào)用Behavior的onTouchEvent()(ps:雖然根據(jù)源碼注釋說在這調(diào)用performIntercept()返回true是為了確保mBehaviorTouchView不為空演熟,但按邏輯理解Behavior的onTouchEvent()被執(zhí)行2次)鞭执;
  • 3.接著如果沒有Behavior做出攔截,則會調(diào)用父類的onTouchEvent(),如果沒則判讀前面的變量cancelSuper是否為true芒粹,若true則為了防止之前已經(jīng)給父類傳了事件給父類的onTouchEvent傳一個cancel事件兄纺。

這里小結(jié)一下:如果重寫B(tài)ehavior的onInterceptTouchEvent()、onTouchEvent()應(yīng)當(dāng)非常注意其邏輯在 CoordinatorLayout中onInterceptTouchEvent()化漆、onTouchEvent()的合理性估脆,因為在Behavior代理觸摸事件的處理顯得有點復(fù)雜而且繁瑣,而且會有大量的非正常的cancel事件出現(xiàn)。

代理CoordinatorLayout的嵌套滑動

CoordinatorLayout實現(xiàn)了NestedScrollingParent2接口并也覆寫兼容NestedScrollingParent座云,但它本身并沒有處理嵌套滑動而是全部給Behavior代理疙赠,Behavior代理嵌套滑動是通過NestedScrollingParent2、NestedScrollingParent對應(yīng)的方法多了兩個參數(shù):一個是CoordinatorLayout疙教,一個是Behavior關(guān)聯(lián)的View棺聊。因為涉及到方法比較多,這里不宜展開贞谓,關(guān)于嵌套滑動可以參考我之前寫的的《淺析NestedScrolling嵌套滑動機(jī)制之基礎(chǔ)篇》

    //CoordinatorLayout.Behavior
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        }
    }

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

接下來看看CoordinatorLayout的嵌套滑動讓Behavior代理限佩,這里分析只兩個方法,其他的方法十分類似:

    //CoordinatorLayout
    @Override
    public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            ...
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                //Behavior代理onStartNestedScroll
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                //在Behavior關(guān)聯(lián)的View的LayoutParams記錄是否接受嵌套滑動
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }

    //CoordinatorLayout.LayoutParams
    void setNestedScrollAccepted(int type, boolean accept) {
        switch (type) {
            case ViewCompat.TYPE_TOUCH:
                mDidAcceptNestedScrollTouch = accept;
                break;
            case ViewCompat.TYPE_NON_TOUCH:
                mDidAcceptNestedScrollNonTouch = accept;
                break;
        }
    }

在CoordinatorLayout的onStartNestedScroll()里遍歷子View裸弦,獲取子View的Behavior并調(diào)用onStartNestedScroll()并在LayoutParams記錄是否接受嵌套滑動祟同。

    //CoordinatorLayout
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {
        ...
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            ...
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //判斷Behavior是否接受嵌套滑動
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                ...
                ////Behavior代理onNestedPreScroll
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
                ...
            }
        }
        ...
    }

在CoordinatorLayout的onNestedPreScroll()里遍歷子View,獲取子View的LayoutParams判斷Behavior是否接受嵌套滑動理疙,若接受則獲取子View的Behavior并調(diào)用onNestedPreScroll()晕城。

小結(jié)

Behavior很強(qiáng)大,但是一般而言子View的測量窖贤、布局這部分邏輯可以放在自定義View內(nèi)部處理砖顷,而CoordinatorLayout的分發(fā)WindowInsets贰锁、Touch事件給子View都有固定的順序,如果你在Behavior處理時應(yīng)該注意其邏輯在CoordinatorLayout的合理性滤蝠,沒必要為了使用Behavior而是用它豌熄,嵌套滑動在實現(xiàn)神奇滑動的效果卻是十分有用,也可以解耦自定義NestedScrollParent的邏輯物咳。

Behavior的View依賴關(guān)系

建立View之間的依賴關(guān)系

Behavior的View依賴關(guān)系

Behavior可以通過layoutDependsOn()讓其關(guān)聯(lián)的View和感興趣的View進(jìn)行依賴锣险,從而可以監(jiān)聽依賴View的位置、大小改變時回調(diào)onDependentViewChanged()览闰,依賴View從布局移除時回調(diào)onDependentViewRemoved()芯肤。


anchor
<android.support.design.widget.CoordinatorLayout>
    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"/>
    <android.support.design.widget.FloatingActionButton
        app:layout_anchor="@id/app_bar" 
        app:layout_anchorGravity="bottom|end"
    />
</android.support.design.widget.CoordinatorLayout>

還有一種就是在布局文件添加layout_anchor設(shè)置錨點來建立依賴關(guān)系,不過這種依賴關(guān)系 只能監(jiān)聽依賴View的位置压鉴、大小改變時回調(diào)onDependentViewChanged()崖咨。

    //CoordinatorLayout.Behavior
    /**
    * 返回值表示child是否依賴dependency
    */
    public boolean layoutDependsOn(CoordinatorLayout parent, V child,
     View dependency) {
        return false;
    }

    /**
    * 返回值表示Behavior是否改變child的大小或者位置
    */
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, 
    View dependency) {
        return false;
    }

    public void onDependentViewRemoved(CoordinatorLayout parent, V child, 
    View dependency) {
    }

排序View的依賴關(guān)系

CoordinatorLayout對View的依賴關(guān)系通過support包的DirectedAcyclicGraph有向無環(huán)圖進(jìn)行拓?fù)渑判颉?/p>

維基百科有向無環(huán)圖

在圖論中,如果一個有向圖從任意頂點出發(fā)無法經(jīng)過若干條邊回到該點晴弃,則這個圖是一個有向無環(huán)圖(DAG,directed acyclic graph)--維基百科

在CoordinatorLayout的onMeasure()里的prepareChildren()就是對View依賴關(guān)系進(jìn)行排序:

    private final List<View> mDependencySortedChildren = new ArrayList<>();
    private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

    private void prepareChildren() {
        mDependencySortedChildren.clear();
        mChildDag.clear();

        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);
            //找到View的Anchor錨點
            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
            //將view當(dāng)節(jié)點添加進(jìn)有向無環(huán)圖
            mChildDag.addNode(view);

            // Now iterate again over the other children, adding any dependencies to the graph
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                if (lp.dependsOn(this, view, other)) {//判斷view與other是否存在的依賴關(guān)系
                    if (!mChildDag.contains(other)) {
                        //(如果other沒在圖里則添加才能確保view與other在圖建立依賴)
                        // Make sure that the other node is added
                        mChildDag.addNode(other);
                    }
                    //(將view與other在圖添加邊建立依賴)
                    // Now add the dependency to the graph
                    mChildDag.addEdge(other, view);
                }
            }
        }
        //(將圖節(jié)點以深度優(yōu)先排序的list存放在list容器里)
        // Finally add the sorted graph list to our list
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //(反轉(zhuǎn)list讓沒有依賴關(guān)系的view排在list的前面)
        // We also need to reverse the result since we want the start of the list to contain
        // Views which have no dependencies, then dependent views after that
        Collections.reverse(mDependencySortedChildren);
    }
  • 1.CoordinatorLayout遍歷遍歷子view掩幢,調(diào)用CoordinatorLayout.LayoutParams.findAnchorView()找到View的Anchor錨點逊拍,并將當(dāng)前view作為節(jié)點添加到有向無環(huán)圖里上鞠。
  • 2.在循環(huán)里在開啟循環(huán)遍歷其他子View,通過CoordinatorLayout.LayoutParams.dependsOn()判斷與外層循環(huán)的view是否存在依賴關(guān)系,若有則建立在圖添加邊建立依賴芯丧。
  • 3.兩層循壞執(zhí)行完后芍阎,將有向無環(huán)圖的節(jié)點以深度優(yōu)先排序的list存放在mDependencySortedChildren里,然后反轉(zhuǎn)mDependencySortedChildren讓沒有依賴關(guān)系的view排在list的前面缨恒。

Behavior依賴View回調(diào)觸發(fā)過程

Behavior的onDependentViewChanged()和onDependentViewRemoved()被觸發(fā)在CoordinatorLayout的onChildViewsChanged()谴咸,這方法type參數(shù)有三個值:EVENT_PRE_DRAW(依賴view繪制之前事件類型)、EVENT_NESTED_SCROLL(依賴view嵌套滑動事件類型)骗露、EVENT_VIEW_REMOVED(依賴view從布局移除事件類型)岭佳。

    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();
            ...
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                if (lp.mAnchorDirectChild == checkChild) {
                    //檢測view的anchor錨點位置是否發(fā)生變化來調(diào)整依賴view的位置
                    offsetChildToAnchor(child, layoutDirection);
                }
            }
            ...
            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();
                //判斷checkChild是否依賴child
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    ...
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            //(分發(fā)依賴view從布局移除事件給Behavior)
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //(分發(fā)依賴view繪制之前事件或嵌套滑動事件給Behavior)
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }
                }
            }
        }
    ...
    }

    void offsetChildToAnchor(View child, int layoutDirection) {
        ...
        //注意:這里view和anchor錨點位置都調(diào)整了,將這變化通知給Behavior
        // If we have needed to move, make sure to notify the child's Behavior
        final Behavior b = lp.getBehavior();
        if (b != null) {
            b.onDependentViewChanged(this, child, lp.mAnchorView);
        }
        ...
    }

在CoordinatorLayout的onNestedFling()萧锉、onNestedPreScroll()珊随、onNestedPreScroll()里如果NestedScrollingChild處理了嵌套滑動都會通過onChildViewsChanged(EVENT_NESTED_SCROLL)將依賴view嵌套滑動事件分發(fā)給Behavior,下面以onNestedScroll代碼為例。

    //CoordiantorLayout.java
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int type) {
        ...
        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

在CoordinatorLayout的構(gòu)造方法里通過setOnHierarchyChangeListener()注冊O(shè)nHierarchyChangeListener監(jiān)聽添加或移除View的層級變化柿隙,而CoordinatorLayout.OnHierarchyChangeListener在View被移除回調(diào)中調(diào)用onChildViewsChanged(EVENT_VIEW_REMOVED)將依賴view從布局移除事件類型分發(fā)給Behavior叶洞。

    public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        ...
        super.setOnHierarchyChangeListener(new HierarchyChangeListener();
    }    

    private class HierarchyChangeListener implements OnHierarchyChangeListener {
        ...
        @Override
        public void onChildViewRemoved(View parent, View child) {
            //將依賴view從布局移除事件類型分發(fā)給Behavior
            onChildViewsChanged(EVENT_VIEW_REMOVED);
            ...
        }
    }

在CoordinatorLayout的onAttachedToWindow()中往ViewTreeObserver注冊個CoordinatorLayout.OnPreDrawListener,它會在每次刷新確定各View大小位置后并繪制之前回調(diào)禀崖,而在回調(diào)里調(diào)用onChildViewsChanged()將依賴view繪制之前事件類型分發(fā)給對應(yīng)的Behavior衩辟。

    //是否需要注冊mOnPreDrawListener標(biāo)識
    private boolean mNeedsPreDrawListener;
    //是否已經(jīng)執(zhí)行onAttachedToWindow()標(biāo)識
    private boolean mIsAttachedToWindow;
    private OnPreDrawListener mOnPreDrawListener;

    @Override
    public void onAttachedToWindow() {
        ...
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        ...
        mIsAttachedToWindow = true;
    }

    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            //分發(fā)依賴view繪制之前事件類型
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
    }

雖然onAttachedToWindow()會被調(diào)用在onDraw()之前,但也可能在onMeasure()之前調(diào)用波附,如果View之間不存在依賴關(guān)系則mOnPreDrawListener從ViewTree移除防止內(nèi)存泄露艺晴,所以在onMeasure()的ensurePreDrawListener()里檢測View之間是否存在依賴關(guān)系對mOnPreDrawListener進(jìn)行注冊或注銷昼钻。

    void ensurePreDrawListener() {
        boolean hasDependencies = false;
        final int childCount = getChildCount();
        //遍歷子View,看它們是否存在依賴關(guān)系
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (hasDependencies(child)) {
                hasDependencies = true;
                break;
            }
        }

        if (hasDependencies != mNeedsPreDrawListener) {
            if (hasDependencies) {
                //存在依賴封寞,注冊mOnPreDrawListener
                addPreDrawListener();
            } else {
                ////不存在依賴换吧,注銷mOnPreDrawListener
                removePreDrawListener();
            }
        }
    }

    void addPreDrawListener() {
        //如果已經(jīng)執(zhí)行onAttachedToWindow()
        if (mIsAttachedToWindow) {
            // Add the listener
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        //(因為onMeasure()與onAttachedToWindow()調(diào)用順序不確定,
        //所以這里標(biāo)識mNeedsPreDrawListener變量來處理注冊mOnPreDrawListener)
        // Record that we need the listener regardless of whether or not we're attached.
        // We'll add the real listener when we become attached.
        mNeedsPreDrawListener = true;
    }

    void removePreDrawListener() {
        if (mIsAttachedToWindow) {
            if (mOnPreDrawListener != null) {
                final ViewTreeObserver vto = getViewTreeObserver();
                vto.removeOnPreDrawListener(mOnPreDrawListener);
            }
        }
        mNeedsPreDrawListener = false;
    }
}

自定義Behavior

  • 1.在自定義Behavior之前您可以參考系統(tǒng)自帶的Behavior能否滿足需求钥星,如FloatActionButton內(nèi)部的Behavior能保證Snackbar彈出的時候不被FAB遮擋等:


    Behavior繼承樹
  • 2.是否有必要為子View的測量沾瓦、布局、分發(fā)WindowInsets和Touch事件而使用CoordinatorLayout+Behavior谦炒,這部分邏輯是否可以放在自定義View內(nèi)部處理贯莺。

  • 3.Behavior的View依賴關(guān)系與NestedScrolling結(jié)合實現(xiàn)滑動更為方便。


    image

    image

    上圖是我之前寫過的《淺析NestedScrolling嵌套滑動機(jī)制之實踐篇-仿寫?zhàn)I了么商家詳情頁》效果宁改,如果改成通過自定義Behavior實現(xiàn)思路:Content部分處理嵌套滑動邏輯缕探,而Header部分、Collapse Content部分还蹲、TopBar部分爹耗、Shop Bar部分通過Behavior.layoutDependsOn()都與Content部分建立依賴,監(jiān)聽Content部分的滑動回調(diào)Behavior.onDependentViewChanged()進(jìn)行各自部分的動畫谜喊、alpha潭兽、Transition等效果,相對于之前自定義View斗遏,這種實現(xiàn)邏輯更加解耦清晰山卦。

總結(jié)

CoordinatorLayout和Behavior結(jié)合很強(qiáng)大,但本文偏向概念性內(nèi)容诵次,難免有些枯燥账蓉,下篇文章實踐自定義Behavior,由于本人水平有限僅給各位提供參考逾一,希望能夠拋磚引玉铸本,如果有什么可以討論的問題可以在評論區(qū)留言或聯(lián)系本人。

參考

Intercepting everything with CoordinatorLayout Behaviors

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遵堵,一起剝皮案震驚了整個濱河市箱玷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鄙早,老刑警劉巖汪茧,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異限番,居然都是意外死亡舱污,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門弥虐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扩灯,“玉大人媚赖,你說我怎么就攤上這事≈椴澹” “怎么了惧磺?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捻撑。 經(jīng)常有香客問我磨隘,道長,這世上最難降的妖魔是什么顾患? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任番捂,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己惋鹅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布鳖枕。 她就那樣靜靜地躺著,像睡著了一般桨螺。 火紅的嫁衣襯著肌膚如雪宾符。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天彭谁,我揣著相機(jī)與錄音吸奴,去河邊找鬼。 笑死缠局,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的考润。 我是一名探鬼主播狭园,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼糊治!你這毒婦竟也來了唱矛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤井辜,失蹤者是張志新(化名)和其女友劉穎绎谦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粥脚,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窃肠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了刷允。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冤留。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡碧囊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纤怒,到底是詐尸還是另有隱情糯而,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布泊窘,位于F島的核電站熄驼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏烘豹。R本人自食惡果不足惜谜洽,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吴叶。 院中可真熱鬧阐虚,春花似錦、人聲如沸蚌卤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逊彭。三九已至咸灿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間侮叮,已是汗流浹背避矢。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留囊榜,地道東北人审胸。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像卸勺,于是被迫代替她去往敵國和親砂沛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359