浅析NestedScrolling嵌套滑动机制之CoordinatorLayout.Behavior

预览

嵌套系列导航

本文已在公众号鸿洋原创发布。未经许可,不得以任何形式转载!

概述

在前面《浅析NestedScrolling嵌套滑动机制之基础篇》里的常见效果提到Behavior也是走NestedScrolling机制来实现各种神奇的滑动效果,它伴随CoordinatorLayout在Revision 24.1.0的android.support.v4兼容包被引入,和CoordinatorLayout结合实现各个控件联动,可以拦截代理CoordinatorLayout的测量、布局、WindowInsets、触摸事件、嵌套滑动。

Behavior简介

Behavior是作用于 CoordinatorLayout的直接子View 的交互行为插件。一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。

    /**
     * 泛型<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两个参数的构造方法创建。

代码动态设置

    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过时标签。

接口实现返回

View实现CoordinatorLayout.AttachedBehavior接口并复写getBehavior()返回Behavior。在CoordinatorLayout在getResolvedLayoutParams()解析时调用getBehavior()获取Behavior,然后调用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是关联了当前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的方法。

    //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代理嵌套滑动是通过NestedScrollingParent2、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结合实现滑动更为方便。


    image

    image

    上图是我之前写过的《浅析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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342