淺析NestedScrolling嵌套滑動機制之CoordinatorLayout.Behavior

嵌套系列導航

本文已在公衆號鴻洋原創發佈。未經許可,不得以任何形式轉載!java

概述

在前面《淺析NestedScrolling嵌套滑動機制之基礎篇》裏的常見效果提到Behavior也是走NestedScrolling機制來實現各類神奇的滑動效果,它伴隨CoordinatorLayout在Revision 24.1.0的android.support.v4兼容包被引入,和CoordinatorLayout結合實現各個控件聯動,能夠攔截代理CoordinatorLayout的測量、佈局、WindowInsets、觸摸事件、嵌套滑動。node

Behavior簡介

Behavior是做用於 CoordinatorLayout的直接子View 的交互行爲插件。一個Behavior 實現了用戶的一個或者多個交互行爲,它們可能包括拖拽、滑動、快滑或者其餘一些手勢。android

/** * 泛型<V>是Behavior關聯的View */
    public static abstract class Behavior<V extends View> {

        /** * 默認構造方法,用於註解的方式建立或者在代碼中建立 */
        public Behavior() {}

        /** * 用於xml解析layout_Behavior屬性的構造方法,若是須要Behavior支持在xml中使用,則必須有此構造方法 */
        public Behavior(Context context, AttributeSet attrs) {}

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

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

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

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

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

        /** * 當blocksInteractionBelow返回爲true時,CoordinatorLayout將會在View的上層繪製 * 一個屏蔽的getScrimColor()顏色來顯示沒法進行交互的區域 */
        @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;
        }

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

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

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

        /** * 代理CoordinatorLayout子View的測量,注意這個子View是關聯了當前Behavior, * 返回true表示使用Behavior的*onMeasureChild()來測量參數裏child的這個子View, * 返回false則使用*CoordinatorLayout的默認測量子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的默認測量子View的方法。 */
        public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
            return false;
        }
        
        /** *代理消費CoordinatorLayout的WindowInsets */
        @NonNull
        public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) {
            return insets;
        }

        //如下是NestedScrolling相關方法//
        @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設置Behavior

xml佈局文件設置

<!-- 佈局文件 -->
<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()調用Behavior兩個參數的構造方法建立。app

代碼動態設置

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設置Behavior,生效的是註解方式的Behavior,若在自定義Behavior使用此方式須要一個無參的構造函數,由於CoordinatorLayout在getResolvedLayoutParams()解析時調用反射Behavior的無參構造函數建立,而這種註解方式在support27.1.0版本打上了@Deprecated過期標籤。less

接口實現返回

View實現CoordinatorLayout.AttachedBehavior接口並複寫getBehavior()返回Behavior。在CoordinatorLayout在getResolvedLayoutParams()解析時調用getBehavior()獲取Behavior,而後調用CoordinatorLayout.LayoutParams.setBehavior()傳入。ide

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

Behavior中的代理

代理CoordinatorLayout子View的測量

Behavior的onMeasureChild()能夠代理CoordinatorLayout子View的測量,注意這個子View是關聯了當前Behavior,它的返回值爲Boolean類型,返回true表示使用Behavior的onMeasureChild()來測量參數裏child的這個子View,返回false則使用CoordinatorLayout的默認測量子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的默認測量子View的方法。post

//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()能夠代理消費CoordinatorLayout的WindowInsets。

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

在CoordinatorLayout的onLayout()裏能夠看出Behavior中的消費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分發給子View前被攔截消費,若Behavior攔截了來自CoordinatorLayout的Touch事件,CoordinatorLayout的各個子View天然就接受不到Touch事件,Behavior的blocksInteractionBelow()表示是否阻斷此Behavior所關聯View下層的View的交互,則這個方法能影響Touch事件的攔截,若blocksInteractionBelow()爲true時,getScrimOpacity()返回值大於0,CoordinatorLayout將會在View的上層繪製一個屏蔽的getScrimColor()顏色來顯示沒法進行交互的區域:

//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不爲空,事件分發給Behavior
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct(走CoordinatorLayout默認方法)
        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,根據這個標
        //識來給剩餘遍歷的Behavior分發個CANCEL的MotionEvent
        boolean newBlock = false;
        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();
        //根據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事件已經被前面遍歷的Behavior攔截或者newBlock爲true表示前面遍歷的Behavior已阻斷交互、且action不是DOWN時
            //那麼後面剩餘遍歷的Behavior分發個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不爲空,事件分發給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事件,標記其關聯的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()進入performIntercept()Behavior阻斷過,
                //再到onTouchEvent()進入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是否以前已經阻斷過此Behavior所關聯View下層的View的交互 */
    boolean didBlockInteraction() {
        if (mBehavior == null) {
            mDidBlockInteraction = false;
        }
        return mDidBlockInteraction;
    }

    /** * Behavior已經阻斷過此Behavior所關聯View下層的View的交互返回true, * 不然返回調用Behavior的blocksInteractionBelow並記錄已阻斷過 */
    boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
        if (mDidBlockInteraction) {
            return true;
        }
        return mDidBlockInteraction |= mBehavior != null
                ? mBehavior.blocksInteractionBelow(parent, child)
                : false;
    }    
複製代碼

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

  • 1.首先根據子View的層級由高到低排序後按順序遍歷子View的Behavior;
  • 2.在遍歷中先判斷Touch事件已經被前面遍歷的Behavior攔截或者阻斷、且不是DOWN事件,若符合這些條件則給剩餘遍歷的Behavior分發個CANCEL的MotionEvent;
  • 3.而後將根據參數type調用Behavior對應的事件攔截、消費的方法,若是Behavior攔截了Touch事件則以變量mBehaviorTouchView記錄其關聯的View;
  • 4.接着調用CoordinatorLayout.LayoutParams的兩個判斷阻斷交互方法用變量newBlock記錄Behavior的阻斷交互。

CoordinatorLayout的onTouchEvent()邏輯以下:

  • 1.先判斷以前在onInterceptTouchEvent()是否有記錄mBehaviorTouchView,如有則直接調用Behavior的onTouchEvent();若無則調用performIntercept()且返回值賦值變量cancelSuper;
  • 2.若cancelSuper爲true說明已有Behavior調用onTouchEvent()消費Touch事件了並記錄mBehaviorTouchView,而後經過mBehaviorTouchView的LayoutParam 再次調用Behavior的onTouchEvent()(ps:雖然根據源碼註釋說在這調用performIntercept()返回true是爲了確保mBehaviorTouchView不爲空,但按邏輯理解Behavior的onTouchEvent()被執行2次);
  • 3.接着若是沒有Behavior作出攔截,則會調用父類的onTouchEvent(),若是沒則判讀前面的變量cancelSuper是否爲true,若true則爲了防止以前已經給父類傳了事件給父類的onTouchEvent傳一個cancel事件。

這裏小結一下:若是重寫Behavior的onInterceptTouchEvent()、onTouchEvent()應當很是注意其邏輯在 CoordinatorLayout中onInterceptTouchEvent()、onTouchEvent()的合理性,由於在Behavior代理觸摸事件的處理顯得有點複雜並且繁瑣,並且會有大量的非正常的cancel事件出現。

代理CoordinatorLayout的嵌套滑動

CoordinatorLayout實現了NestedScrollingParent2接口並也覆寫兼容NestedScrollingParent,但它自己並無處理嵌套滑動而是所有給Behavior代理,Behavior代理嵌套滑動是經過NestedScrollingParent二、NestedScrollingParent對應的方法多了兩個參數:一個是CoordinatorLayout,一個是Behavior關聯的View。由於涉及到方法比較多,這裏不宜展開,關於嵌套滑動能夠參考我以前寫的的《淺析NestedScrolling嵌套滑動機制之基礎篇》

//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關聯的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並調用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並調用onNestedPreScroll()。

小結

Behavior很強大,可是通常而言子View的測量、佈局這部分邏輯能夠放在自定義View內部處理,而CoordinatorLayout的分發WindowInsets、Touch事件給子View都有固定的順序,若是你在Behavior處理時應該注意其邏輯在CoordinatorLayout的合理性,不必爲了使用Behavior而是用它,嵌套滑動在實現神奇滑動的效果倒是十分有用,也能夠解耦自定義NestedScrollParent的邏輯。

Behavior的View依賴關係

創建View之間的依賴關係

Behavior的View依賴關係
Behavior能夠經過layoutDependsOn()讓其關聯的View和感興趣的View進行依賴,從而能夠監聽依賴View的位置、大小改變時回調onDependentViewChanged(),依賴View從佈局移除時回調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設置錨點來創建依賴關係,不過這種依賴關係 只能監聽依賴View的位置、大小改變時回調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的依賴關係

CoordinatorLayout對View的依賴關係經過support包的DirectedAcyclicGraph有向無環圖進行拓撲排序。

維基百科有向無環圖

在圖論中,若是一個有向圖從任意頂點出發沒法通過若干條邊回到該點,則這個圖是一個有向無環圖(DAG,directed acyclic graph)--維基百科

在CoordinatorLayout的onMeasure()裏的prepareChildren()就是對View依賴關係進行排序:

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當節點添加進有向無環圖
            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是否存在的依賴關係
                    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);
                }
            }
        }
        //(將圖節點以深度優先排序的list存放在list容器裏)
        // Finally add the sorted graph list to our list
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //(反轉list讓沒有依賴關係的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,調用CoordinatorLayout.LayoutParams.findAnchorView()找到View的Anchor錨點,並將當前view做爲節點添加到有向無環圖裏。
  • 2.在循環裏在開啓循環遍歷其餘子View,經過CoordinatorLayout.LayoutParams.dependsOn()判斷與外層循環的view是否存在依賴關係,如有則創建在圖添加邊創建依賴。
  • 3.兩層循壞執行完後,將有向無環圖的節點以深度優先排序的list存放在mDependencySortedChildren裏,而後反轉mDependencySortedChildren讓沒有依賴關係的view排在list的前面。

Behavior依賴View回調觸發過程

Behavior的onDependentViewChanged()和onDependentViewRemoved()被觸發在CoordinatorLayout的onChildViewsChanged(),這方法type參數有三個值: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錨點位置是否發生變化來調整依賴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:
                            //(分發依賴view從佈局移除事件給Behavior)
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //(分發依賴view繪製以前事件或嵌套滑動事件給Behavior)
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }
                }
            }
        }
    ...
    }

    void offsetChildToAnchor(View child, int layoutDirection) {
        ...
        //注意:這裏view和anchor錨點位置都調整了,將這變化通知給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嵌套滑動事件分發給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的構造方法裏經過setOnHierarchyChangeListener()註冊OnHierarchyChangeListener監聽添加或移除View的層級變化,而CoordinatorLayout.OnHierarchyChangeListener在View被移除回調中調用onChildViewsChanged(EVENT_VIEW_REMOVED)將依賴view從佈局移除事件類型分發給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從佈局移除事件類型分發給Behavior
            onChildViewsChanged(EVENT_VIEW_REMOVED);
            ...
        }
    }
複製代碼

在CoordinatorLayout的onAttachedToWindow()中往ViewTreeObserver註冊個CoordinatorLayout.OnPreDrawListener,它會在每次刷新肯定各View大小位置後並繪製以前回調,而在回調裏調用onChildViewsChanged()將依賴view繪製以前事件類型分發給對應的Behavior。

//是否須要註冊mOnPreDrawListener標識
    private boolean mNeedsPreDrawListener;
    //是否已經執行onAttachedToWindow()標識
    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() {
            //分發依賴view繪製以前事件類型
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
    }
複製代碼

雖然onAttachedToWindow()會被調用在onDraw()以前,但也可能在onMeasure()以前調用,若是View之間不存在依賴關係則mOnPreDrawListener從ViewTree移除防止內存泄露,因此在onMeasure()的ensurePreDrawListener()裏檢測View之間是否存在依賴關係對mOnPreDrawListener進行註冊或註銷。

void ensurePreDrawListener() {
        boolean hasDependencies = false;
        final int childCount = getChildCount();
        //遍歷子View,看它們是否存在依賴關係
        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() {
        //若是已經執行onAttachedToWindow()
        if (mIsAttachedToWindow) {
            // Add the listener
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        //(由於onMeasure()與onAttachedToWindow()調用順序不肯定,
        //因此這裏標識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以前您能夠參考系統自帶的Behavior可否知足需求,如FloatActionButton內部的Behavior能保證Snackbar彈出的時候不被FAB遮擋等:

    Behavior繼承樹

  • 2.是否有必要爲子View的測量、佈局、分發WindowInsets和Touch事件而使用CoordinatorLayout+Behavior,這部分邏輯是否能夠放在自定義View內部處理。

  • 3.Behavior的View依賴關係與NestedScrolling結合實現滑動更爲方便。

    上圖是我以前寫過的《淺析NestedScrolling嵌套滑動機制之實踐篇-仿寫餓了麼商家詳情頁》效果,若是改爲經過自定義Behavior實現思路:Content部分處理嵌套滑動邏輯,而Header部分、Collapse Content部分、TopBar部分、Shop Bar部分經過Behavior.layoutDependsOn()都與Content部分創建依賴,監聽Content部分的滑動回調Behavior.onDependentViewChanged()進行各自部分的動畫、alpha、Transition等效果,相對於以前自定義View,這種實現邏輯更加解耦清晰。

總結

CoordinatorLayout和Behavior結合很強大,但本文偏向概念性內容,不免有些枯燥,下篇文章實踐自定義Behavior,因爲本人水平有限僅給各位提供參考,但願可以拋磚引玉,若是有什麼能夠討論的問題能夠在評論區留言或聯繫本人。

參考

Intercepting everything with CoordinatorLayout Behaviors

相關文章
相關標籤/搜索