Android CoordinatorLayout和Behavior解析

Android CoordinatorLayout和Behavior解析

目录

在Materials Design中有一个名为CoordinatorLayout的布局,这是一个神奇的布局,可以实现各种控件间的联动效果,比如底部FloatingActionBar跟随Snackbar弹出而上移

比如AppBarLayout跟随NestedScrollView滑动而伸缩,FloatingActionBar跟随AppBarLayout伸缩而显隐

这些都是非常赞的效果实现,这次我们就从源码角度来分析下这个布局和协助它实现控件联动效果的Behavior.

CoordinatorLayout特性

要知道一个类的特性,应当从类继承和接口开始

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
    //...............
}

从上面可以知道这个布局是一个ViewGroup,而且支持作为嵌套滑动的父布局.

对于一个ViewGroup,应该关心什么呢?

个人觉得比较重要的有这几点

扫描二维码关注公众号,回复: 2589449 查看本文章
  • 测量过程
  • 布局过程
  • 绘制过程
  • 触摸事件处理

接下来看看CoordinatorLayout的这些重点过程的处理方式

CoordinatorLayout的测量过程

先查看其测量过程,其onMeasure方法的核心代码如下

    @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 Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);

            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }

        final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & View.MEASURED_STATE_MASK);
        final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << View.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }

代码主体是测量每一个子View的宽高,然后取子View中最大的距离消耗作为自己的宽高,这种方式貌似和FrameLayout很像.

然后有一段值得注意的代码

    final Behavior b = lp.getBehavior();
    if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
            childHeightMeasureSpec, 0)) {
        onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0);
    }

这里子View的测量过程居然可以使用子View的BehavioronMeasureChild方法代替,这感觉就像被黑客劫持了一样,子View自带的测量都废了.

CoordinatorLayout的布局过程

再看其布局过程,查看其onLayout代码如下

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();

            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

这里也是一样如果有Behavior存在,则使用Behavior中的布局方法.

如果没有Behavior呢?

继续追踪CoordinatorLayout自带的onLayoutChild方法

    public void onLayoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.checkAnchorChanged()) {
            throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                    + " measurement begins before layout is complete.");
        }
        if (lp.mAnchorView != null) {
            layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
        } else if (lp.keyline >= 0) {
            layoutChildWithKeyline(child, lp.keyline, layoutDirection);
        } else {
            layoutChild(child, layoutDirection);
        }
    }

由于其后续涉及的代码较多,在此只做简单说明

如果子View的LayoutParams设置了作为锚点的View(mAnchorView),那么会获得锚点View的Rect坐标,然后再借助子View的LayoutParamsGravity设置坐标;
如果子View没有设置锚点View,但是设置了keyline(这个只是CoordinatorLayout的keylines的index),且需要CoordinatorLayout也设置了keylins数组,然后使用keyline结合Gravity设置坐标,其中的CoordinatorLayout中的keylines是以dp为单位的一组int数组,用于限制子View横坐标,作用不大而且非本篇重点,就此略过;
如果什么都没有设置则是只根据Gravity布局,这点和FrameLayout也是一致的.

onLayout中的布局是根据一个子View列表mDependencySortedChildren依次布局的,查看这个子View列表的定义

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

看名字都知道,这是特殊排序过的,这个列表就很有意思了.

由于子View的Behavior可能对其它子View可能存在位置依赖关系,为了实现将被依赖的子View先布局而创建了这个列表.这个列表如何排序生成的呢?源码中在CoordinatorLayoutonMeasure中的prepareChildren中生成一个无回路有向图(DirectedAcyclicGraph),然后使用深度优先遍历算法(DFS)将图遍历出来,再进行反序处理(Collections.reverse)生成的,对算法比较感兴趣的可以去源码中查看下DirectedAcyclicGraph的结构和DFS算法的实现,在此就不做说明了.

CoordinatorLayout的绘制过程

CoordinatorLayout没有重写dispatchDraw,但是重写了onDrawdrawChild

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
        if (mDrawStatusBarBackground && mStatusBarBackground != null) {
            final int inset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
            if (inset > 0) {
                mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
                mStatusBarBackground.draw(c);
            }
        }
    }

ViewGrouponDraw只有在含有background时才会调用,而且CoordinatorLayout的处理也只是对于状态栏背景的处理,无足轻重.

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.mBehavior != null) {
            final float scrimAlpha = lp.mBehavior.getScrimOpacity(this, child);
            if (scrimAlpha > 0f) {
                if (mScrimPaint == null) {
                    mScrimPaint = new Paint();
                }
                mScrimPaint.setColor(lp.mBehavior.getScrimColor(this, child));
                mScrimPaint.setAlpha(MathUtils.clamp(Math.round(255 * scrimAlpha), 0, 255));

                final int saved = canvas.save();
                if (child.isOpaque()) {
                    // If the child is opaque, there is no need to draw behind it so we'll inverse
                    // clip the canvas
                    canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(),
                            child.getBottom(), Region.Op.DIFFERENCE);
                }
                // Now draw the rectangle for the scrim
                canvas.drawRect(getPaddingLeft(), getPaddingTop(),
                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom(),
                        mScrimPaint);
                canvas.restoreToCount(saved);
            }
        }
        return super.drawChild(canvas, child, drawingTime);
    }

drawChild的处理倒是有点意思,这里获取了子View的Behavior的阴影颜色和阴影透明度,然后在绘制子View的位置之外绘制一层阴影.

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;

        final int action = ev.getActionMasked();

        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();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        }
        //.................
        return handled;
    }

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // 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();

            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;
            }

            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;
                }
                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) {
                // 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的触摸事件控制逻辑.

先看onInterceptTouchEvent的逻辑,主要表现在了performIntercept这个方法中,在onInterceptTouchEvent中,先对各Behavior的onInterceptTouchEvent方法分发down事件,直到有子View的BehavioronInterceptTouchEvent返回true或者LayoutParamsisBlockingInteractionBelow返回true,则停止继续分发.那么何时isBlockingInteractionBelow返回true呢?当子View的BehaviorblocksInteractionBelow返回true时isBlockingInteractionBelow为true.当BehavioronInterceptTouchEvent返回true,会用一个mBehaviorTouchView的变量标记Behavior所附属的View.在对于非down事件的分发时,如果事件因为BehaviorblocksInteractionBelow强制拦截了,剩余之前接收到down事件的Behavior则会收到一个cancel事件.onInterceptTouchEvent的处理几乎都是交给Behavior处理的,它自己没有做任何处理.这里应当注意的一个问题是,如果有子View的BehavioronInterceptTouchEvent返回true,则CoordinatorLayout的所有子View的触摸事件都将失去响应.

然后看下onTouchEvent的逻辑.先判断之前是否有记录mBehaviorTouchView,如果之前有记录则直接调用该View的BehavioronTouchEvent,如果没有记录mBehaviorTouchView,则执行performIntercept方法寻找会拦截的Behavior,找到后执行BehavioronTouchEvent,并且用mBehaviorTouchView记录Behavior所附属的View,从performIntercept方法出来后,由于performIntercept返回值是true,所以在这里仍然会调用一次BehavioronTouchEvent.在这里同一个事件调用了BehavioronTouchEvent两次,讲道理这应该也算是一个bug了,值得留意.如果没有Behavior做出拦截,则会调用父类的onTouchEvent方法.如果在onTouchEvent中执行了performIntercept方法,而且此方法返回true,为了防止之前已经给父类传了事件,也会在给父类的onTouchEvent传一个cancel事件.

注意,不管是在onInterceptTouchEvent或是onTouchEvent中,传给子ViewBehaviorMotionEvent是基于CoordinatorLayout的而不是基于子View的.

还有一个值得注意的地方performIntercept中的子View的遍历,使用了getTopSortedChildren(topmostChildList);方法,该方法会生成一个根据层级从上往下的子View列表,这个列表在api21之前以子View添加顺序相反的顺序作为默认顺序,在api21及以后会根据子View的Elevation排序.performIntercept使用这个列表进行遍历,从此也可以很轻易的知道,事件分发是根据子View层级从层顶到层底分发到各子View的Behavior的.

总结一下触摸事件的处理,onInterceptTouchEventonTouchEvent其实也是调用了BehavioronInterceptTouchEventonTouchEvent,如果所有子View都没有设置Behavior或者子View的Behavior没有做处理,则CoordinatorLayout本身没有做过于特殊的处理.
整体而言,触摸事件的处理显得有点复杂而且繁琐,而且会有大量的非正常的cancel事件出现,由于其复杂的逻辑,重写BehavioronInterceptTouchEventonTouchEvent时应当非常注意其逻辑在CoordinatorLayoutonInterceptTouchEventonTouchEvent的合理性.

CoordinatorLayout的嵌套滑动支持

CoordinatorLayout支持了嵌套滑动的NestedScrollParent2,来查看下其中的接口实现.

    @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);
            if (view.getVisibility() == View.GONE) {
                // If it's GONE, don't dispatch
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
        mNestedScrollingTarget = target;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target,
                        nestedScrollAxes, type);
            }
        }
    }

    //.....................

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

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

    //.......................

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

这里只贴了几个具有代表性的方法,除了getNestedScrollAxes,其他的方法都是通过调用View的Behavior的同名方法实现的.

值得注意的是,在onStartNestedScroll方法中,由于子View不止一个,所以用lp.setNestedScrollAccepted(type, accepted);记录了子View是否接受嵌套滑动,然后在onNestedScrollAccepted方法中调用lp.isNestedScrollAccepted(type)判断子View是否接受嵌套滑动.

还有一点,在onNestedPreScroll中,consumed值是取的各子View的Behavior消耗最大值.

CoordinatorLayout属性总结

通过对CoordinatorLayout的结构分析可以获得如下结论

  • 如果子View存在Behavior,CoordinatorLayout对子View的大部分操作都会交给Behavior来处理,借助这个属性,可以通过设置Behavior来实现对子View操作的劫持.
  • CoordinatorLayout在子View没有设置Behavior的情况下,几乎是就是一个FrameLayout.
  • CoordinatorLayout支持嵌套滑动,但是都是交给子View的Behavior来处理的.

CoordinatorLayout中的核心 – Behavior

在了解了CoordinatorLayout的大致结构后,会发现各种操作都和Behavior息息相关,那么Behavior到底是什么东西呢?

Behavior的构成

先来看看Behavior的构成吧.

查阅其方法,会发现其中有着大部分和NestedScrollingParent2一样的方法,在对CoordinatorLayout的分析中,已然了解,CoordinatorLayout的嵌套滑动事件都会传递给子View的Behavior,这些方法属于回调,而且在基础的Behavior方法中都是空方法,在此不多做解释.

除开嵌套滑动部分,其主要有如下方法

    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) {
        }

        /**
         * 当Behavior脱离LayoutParams时调用,例如调用了LayoutParams.setBehavior(null).
         * View被从View Tree中移除时不会调用此方法.
         */
        public void onDetachedFromLayoutParams() {
        }

        /**
         * 接收CoordinatorLayout的触摸拦截事件,按从上到下的层级顺序分发拦截事件,
         * 如果返回true,会在CoordinatorLayout中的onTouchEvent中调用这个View的Behavior的onTouchEvent方法.
         * 
         * 这里的拦截应当慎重,一旦有Behavior返回true,则会导致CoordinatorLayout的所有子View触摸事件无效.
         */
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        /**
         * 接收CoordinatorLayout的触摸事件,
         * 事件分发按层从上到下分发,一旦有Behavior的onTouchEvent返回true,
         * 则此Behavior所附属的View的下面所有的CoordinatorLayout子View的Behavior都收不到onTouchEvent回调.
         */
        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        /**
         * 获得当前Behavior附属View空间之外的阴影颜色
         */
        @ColorInt
        public int getScrimColor(CoordinatorLayout parent, V child) {
            return Color.BLACK;
        }

        /**
         * 获得当前Behavior附属View空间之外的阴影透明度
         */
        @FloatRange(from = 0, to = 1)
        public float getScrimOpacity(CoordinatorLayout parent, V child) {
            return 0.f;
        }

        /**
         * 是否阻止此Behavior所附属View下层的View的交互
         */
        public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
            return getScrimOpacity(parent, child) > 0.f;
        }

        /**
         * 用于判断是否为依赖的View,一般重写该方法来获取需要联动的View
         */
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
         * 当依赖的View发生改变时回调此方法,用于监听依赖View的状态
         */
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
         * 当依赖的View被移除时回调此方法,用于监听依赖View的状态
         */
        public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
        }

        /**
         * 代替CoordinatorLayout的默认测量子View的方法,
         * 返回true使用Behavior的测量方法来测量当前Behavior所附属View,
         * 返回flase则使用CoordinatorLayout的默认方式
         */
        public boolean onMeasureChild(CoordinatorLayout parent, V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            return false;
        }

        /**
         * 代替CoordinatorLayout的默认布局子View的方法给该Behavior的附属View布局,
         * 返回true则使用Behavior的布局方式来给Behavior所属View布局,
         * 返回false则使用CoordinatorLayout的默认方式
         */
        public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
            return false;
        }
    }

Behavior的构造方法

需要注意的一点,一般的Behavior都需要有两个构造方法,一个用于在代码中创建,无参,通过CoordinatorLayout.LayoutParams.setBehavior设置到View的LayoutParams中或者使用注解的方式设置View默认的Behavior.

注解的方式使用举例:

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

如果使用注解的方式,必须有一个无参数的构造方法,因为实际上是反射构建的.其实例创建过程是在CoordinatorLayout中的getResolvedLayoutParams中调用

DefaultBehavior defaultBehavior = null;
defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)
defaultBehavior.value().getDeclaredConstructor().newInstance()

实现的.

不过现在这种方法已经废弃了,新的方式是View通过实现

    public interface AttachedBehavior {

        @NonNull Behavior getBehavior();
    }

接口,然后CoordinatorLayout会在getResolvedLayoutParams判断子View是否实现这个接口,如果实现了,则通过getBehavior获取默认Behavior.

还有一个构造方法用于在xml中使用,这种方式也是使用反射生成的Behavior实例,所以构造方法参数必须只能为Context context, AttributeSet attrs.

在xml中使用举例:

layout文件

app:layout_behavior="@string/appbar_scrolling_view_behavior"

字符串资源

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

有兴趣了解其反射解析过程的话可以自行查看CoordinatorLayoutparseBehavior方法:

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) 

在此不做赘述.

BehaviorDependentView

BehaviorDependentView这也是一个Behavior的重要概念,在此解释下.

这是CoordinatorLayout实现控件联动的关键之一,在子View的Behavior中根据layoutDependsOn方法获得一个满足条件的DependentView,然后当DependentView发生改变时,会触发BehavioronDependentViewChangedonDependentViewRemoved回调.

在此分析下其实现原理

跟踪layoutDependsOn的调用,会找到这样一个方法

    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();

            //.......................................

            if (type != EVENT_VIEW_REMOVED) {
                // Did it change? if not continue
                getLastChildRect(child, lastDrawRect);
                if (lastDrawRect.equals(drawRect)) {
                    continue;
                }
                recordLastChildRect(child, drawRect);
            }

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();

                if (b != null && b.layoutDependsOn(this, checkChild, child)) {

                    //......................

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    //........................

                }
            }
        }

        //......................

    }

前面说过mDependencySortedChildren是一个将子View根据依赖关系进行排序的List,这里先遍历一次这个List判断子View的位置大小是否变化,如果有变化则再从当前已经遍历到的列表节点开始再遍历一次剩余的节点,根据BehaviorlayoutDependsOn方法判断这个子View的DependentView是否有在这个List中的.如果有的话,则再根据改变类型调用BehavioronDependentViewRemovedonDependentViewChanged方法.

继续追踪onChildViewsChanged方法的调用.发现很多是嵌套滑动的的回调方法中调用的onChildViewsChanged(EVENT_NESTED_SCROLL);都是类似这种

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int type) {

        //...........................

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

这部分相对简单,不再重复写了.

更值得关注的是EVENT_VIEW_REMOVEDEVENT_PRE_DRAW类型,追踪到发现这两个都是用Listener监听的回调.

EVENT_VIEW_REMOVED类型部分

    private class HierarchyChangeListener implements OnHierarchyChangeListener {

        //..................

        @Override
        public void onChildViewRemoved(View parent, View child) {
            onChildViewsChanged(EVENT_VIEW_REMOVED);

            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
            }
        }
    }

然后在构造方法中注册

super.setOnHierarchyChangeListener(new HierarchyChangeListener());

这里简介下OnHierarchyChangeListener,在ViewGroup中注册此Listener,当有子View添加或者删除时会回调此Listener.

EVENT_PRE_DRAW类型部分

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

onAttachedToWindow方法中添加了OnPreDrawListener

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors(false);
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
            // We're set to fitSystemWindows but we haven't had any insets yet...
            // We should request a new dispatch of window insets
            ViewCompat.requestApplyInsets(this);
        }
        mIsAttachedToWindow = true;
    }

然后在onDetachedFromWindow会删除这个Listener.

注册OnPreDrawListener后,这个Listener会在每次刷新确定各View大小位置后,绘制之前调用.

还有一个部分的OnPreDrawListener调用过程比较繁琐,文字简述下吧.

这部分是onMeasure–>ensurePreDrawListener–>判断所有子View的Behavior是否存在满足其依赖关系的View,只要存在,则添加OnPreDrawListener,否则删除OnPreDrawListener.

再追踪BehavioronDependentViewChanged方法,发现CoordinatorLayout中有一个dispatchDependentViewsChanged方法,也存在调用,不过这是一个开放给用户的方法,源码中并没有发现调用之处;
还有一个调用的地方是上面提到的onChildViewsChanged方法–>offsetChildToAnchor–>Behavior.onDependentViewChanged,这里的判断是判断改变的View是否为某个子View的锚点View,如果是,则调用这个子View的BehavioronDependentViewChanged方法.

总结一下

  • 当一个View的Behavior依赖的View位置或者大小发生改变时,会回调此View的BehavioronDependentViewChanged方法
  • 当一个View的Behavior依赖的View从View Tree中删除时,会回调此View的BehavioronDependentViewRemoved方法
  • 当一个View的LayoutParams设置了锚点View(mAnchorView),如果这个锚点View的位置发现变化,则会回调此View的BehavioronDependentViewChanged方法

那么到此,Behavior大部分重要的结构原理都分析完毕了.

Behavior的使用

说了那么多原理,是时候说怎么用了.

Behavior的用法应该可以分为三部分.

第一部分是用于控制子View的布局,测量,设置阴影还有拦截CoordinatorLayout的触摸事件,这部分都算是拦截和子View自身相关属性的部分,通过重写BehavioronInterceptTouchEvent,onTouchEvent,getScrimColor,getScrimOpacity,onMeasureChild,onLayoutChild等方法实现,这部分较为简单,在此不多做说明及演示了.

第二部分第三部分都是控件联动部分,在此先暂定第二部分为依赖联动部分,第三部分为嵌套滑动联动部分.

依赖联动

在这里重点说明下第二部分依赖联动部分的使用.
Behavior依赖联动部分有3个重要方法layoutDependsOn,onDependentViewChanged,onDependentViewRemoved.一般重写这三个方法就可以实现简单的控件联动.

使用举例

创建个例子吧,这样比较有说服力.

在此就做一个相互关联运动相反的两个方块作为例子吧.

先写xml的布局

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/child"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:background="@android:color/holo_green_light"
        app:layout_behavior=".MyTextViewBehavior" />

    <TextView
        android:id="@+id/dependency"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_gravity="right|bottom"
        android:background="@android:color/holo_blue_light" />

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

然后在主Activity中的onCreate中设置被依赖的View的触摸事件

TextView textView = (TextView) findViewById(R.id.dependency);
textView.setOnTouchListener(this);

Activity实现OnTouchListener

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.dependency:
                switch (event.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN:

                        break;
                    case MotionEvent.ACTION_MOVE:
                        int x = (int) event.getX() + v.getLeft();
                        int y = (int) event.getY() + v.getTop();
                        int left = x - v.getWidth() / 2;
                        int top = y - v.getHeight() / 2;
                        setViewLocationInCoordinatorLayout(left, top, v);
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                    default:

                        break;
                }


                break;
            default:

                break;
        }
        return true;
    }

这个触摸事件的处理很简单,就是让控件中心跟随手指运动.

其中setViewLocationInCoordinatorLayout是一个改变CoordinatorLayout子View在CoordinatorLayout位置的一个静态方法,其实现如下

    public static void setViewLocationInCoordinatorLayout(int leftMargin, int topMargin, View child) {
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if (lp == null) {
            lp = new CoordinatorLayout.LayoutParams(child.getWidth(), child.getHeight());
        }
        lp.setMargins(leftMargin, topMargin, 0, 0);
        lp.gravity = Gravity.NO_GRAVITY;
        child.setLayoutParams(lp);
    }

然后实现id为childBehaviorMyTextViewBehavior

主要重写以下两个方法即可

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        if (child.getId() == R.id.child) {
            if (dependency.getId() == R.id.dependency) {
                return true;
            }
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
        int leftMargin = parent.getWidth() - dependency.getRight();
        int topMargin = parent.getHeight() - dependency.getBottom();
        MainActivity.setViewLocationInCoordinatorLayout(leftMargin, topMargin, child);
        return true;
    }

这里只做了两个正方形的,对称,位置相反的方块,然后给id为dependencyTextView设置了触摸事件,让这个TextView跟随手指移动,然后给id为childTextView设置了一个MyTextViewBehavior,这个Behavior简单的根据id来判断是否为依赖的View,简单粗暴,然后当id为dependency的这个View位置发现改变时则会触发MyTextViewBehavioronDependentViewChanged,然后计算id为dependency的这个View的偏移距离,然后根据这个距离改变这个Behavior所附属的View的位置.

放一个效果图

嵌套滑动联动

这个主要是在有子View支持NestedScrollingChild接口时,支持NestedScrollingChild接口的这个子View分发嵌套滑动事件,然后回调CoordinatorLayoutNestedScrollingParent方法,然后在CoordinatorLayout中的这些NestedScrollingParent方法中调用子View的Behavior的与NestedScrollingParent同名的方法以实现联动.

具体实例及分析将结合一个非常有研究价值的问题放在下篇WebView和AppBarLayout嵌套滑动联动无效分析及解决办法中讲解.

依赖联动示例源码

CoordinatorLayoutAndBehavior

猜你喜欢

转载自blog.csdn.net/dqh147258/article/details/81266442