CoordinatorLayout与Behavior源码分析

时间:2021-6-7 作者:qvyue

CoordainatorLayout作为控制内部一个或多个的子控件协同交互的容器,通过设置Behavior去控制多个控件的协同交互效果,测量尺寸、布局位置及触摸响应。

Behavior的具体用法如下图所示:

CoordinatorLayout与Behavior源码分析
1621857367(1).png
控件之间的相互依赖

首先我们需要在回到Behavior在哪里进行初始化,阅读CoordinatorLayout的源码我们知道,Behavior是在CoordinatorLayout的内部类LayoutParams的构造函数进行初始化。

LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_Layout);
            ......
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);//1
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));//2
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }

1处首先判定有否在布局中有layout_behavior这个标签。例如:

= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

        try {
            Map> constructors = sConstructors.get();
            if (constructors == null) {
                constructors = new HashMap();
                sConstructors.set(constructors);
            }
            Constructor c = constructors.get(fullName);
            if (c == null) {
                final Class clazz =
                        (Class) Class.forName(fullName, false, context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

先抛出一个自己在实际操作的过程中遇到的问题,就是自定义Behavior的时候,没有添加对应的构造函数而发生崩溃。自己定义的代码如下:

public class TranslationBehavior extends FloatingActionButton.Behavior {

    //必须添加这个构造函数,不然会崩了

    /**
     * parseBehavior():c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
     * static final Class>[] CONSTRUCTOR_PARAMS = new Class>[] {
     *             Context.class,
     *             AttributeSet.class
     *     };
     *  需要解析这两个参数的构造函数
     * @param context
     * @param attrs
     */

    public TranslationBehavior(Context context, AttributeSet attrs){
        super(context, attrs);
    }


    @Override
    public boolean onStartNestedScroll(@NonNull  CoordinatorLayout coordinatorLayout, @NonNull  FloatingActionButton child, @NonNull  View directTargetChild, @NonNull  View target, int axes, int type) {
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    private boolean isOut = false;
    @Override
    public void onNestedScroll(@NonNull  CoordinatorLayout coordinatorLayout, @NonNull  FloatingActionButton child, @NonNull  View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
        if (dyConsumed > 0){
            if (!isOut){
                int mY = ((CoordinatorLayout.LayoutParams)child.getLayoutParams()).bottomMargin + child.getMeasuredHeight();
                child.animate().translationY(mY).setDuration(1000).start();
                isOut = true;
            }
        }else {
            if (isOut){
                child.animate().translationY(0).setDuration(1000).start();
                isOut = false;
            }

        }
    }
}

先分析原来的parseBehavior()再来解释为什么会崩溃。

parseBehavior()主要做了以下两件事:

1、如果在设置app:layout_behavior=”.xx.xx.xx.xxxxBehavior”标签的时候,后面的部分只写了.xxxxBehavior,系统的则将包名与这个名字拼接,如果是像上面那样全拼,则直接就是返回这个全名。

2、根据上面得到的名字从一个map里面去查找对应的Behavior,如果没有找到,采用反射的方式创建Behavior实例。并保存起来,但是在采用反射的过程中,用的是含有两个参数的构造方法进行创建。

所以在我们自定义的Behavior中,如果我们不构建一个两个参数的构造函数,在进行解析的时候就会报错。

下面继续来分析控件之间的相互依赖:

由于View的生命周期的开始是在onAttachedToWindow方法中,在CoordinatorLayout类中找到onAttachedToWindow方法
发现它调用getViewTreeObserver,获得ViewTreeObserver,然后调用了addOnPreDrawListener,ViewTreeObserver 注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得

public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors(false);
        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() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

在OnPreDrawListener中调用了onChildViewChanged()。

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i 

首先看DispatchChangeEvent,这个有三个值,分别为:EVENT_PRE_DRAW, EVENT_NESTED_SCROLL, EVENT_VIEW_REMOVED,分别表示绘制前,需要滚动,和移除。

在1处调用layoutDependsOn()先判断看一下这个child是不是被依赖的。使用了一个名为mDependencySortedChildren的集合,通过遍历该集合,我们可以获取集合中控件的LayoutParam,得到LayoutParam后,我们可以继续获取相应的Behavior。并调用其layoutDependsOn方法找到所依赖的控件,如果找到了当前控件所依赖的另一控件,那么就调用Behavior中的onDependentViewChanged方法。

所以我们在具体操作的时候我们可以如下操作:

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        boolean dependOn = isDependOn(dependency);
        Log.i(TAG, "layoutDependsOn: dependOn =" + dependOn);
        return dependOn;
    }

    private boolean isDependOn(View dependency) {
        return dependency != null && dependency.getId() == mDependsLayoutId;
    }
    
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        offsetChildAsNeeded(parent, child, dependency);
        return false;
    }

    private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
        /* int translationY = (int) (denpendencyTranslationY / (getHeaderOffsetRange() * 1.0f)
         * getScrollRange(dependency));*/
        float denpendencyTranslationY = dependency.getTranslationY();
        Log.d(TAG, "offsetChildAsNeeded: denpendencyTranslationY=" + denpendencyTranslationY
                + " denpendencyTranslationY=" + denpendencyTranslationY);
        //        child.setTranslationY(translationY);

        //  is a negative number
        int maxTranslationY = -(dependency.getHeight() - getFinalY());
        if (denpendencyTranslationY 
CoordinatorLayout内部嵌套滑动原理

先看一张图,如下:

CoordinatorLayout与Behavior源码分析
1621853386(1).png

场景:一个RecyclerView和一个FloatingActionButton,当RecyclerView向上滑的时候,FloatingActionButton则隐藏,否则则显示。

我们都知道,滑动事件都是围绕onInterceptTouchEvent与onTouchEvent方法展开的。下面来看
CoordinatorLayout的onInterceptTouchEvent和onTouchEvent事件。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        // Make sure we reset in case we had missed a previous important event.
        if (action == MotionEvent.ACTION_DOWN) {
            resetTouchBehaviors(true);
        }

        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

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

        return intercepted;
    }

从上面可以看到,onInterceptTouchEvent的返回值是根据performIntercept的返回值来定,

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

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();

        final List topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);//1

        final int childCount = topmostChildList.size();
        for (int i = 0; i 

在1处,我们对view根据Z轴进行排序,然后进行查询,由于intercepted和newBlock一开始都为false,所以第一个if不会进去,进入第二个if,而intercepted又是根据Behavior的onInterceptTouchEvent()来决定,而Behavior的onInterceptTouchEvent()默认是返回false,相当于不拦截。则会调用子类的onInterceptTouchEvent()。

@Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
    
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(e);

        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mIgnoreMotionEventTillDown) {
                    mIgnoreMotionEventTillDown = false;
                }
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                if (mScrollState == SCROLL_STATE_SETTLING) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    stopNestedScroll(TYPE_NON_TOUCH);
                }

                // Clear the nested offsets
                mNestedOffsets[0] = mNestedOffsets[1] = 0;

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);//1
                break;


            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = x;
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = y;
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
            }
            break;
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll(TYPE_TOUCH);
            }
            break;

            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();
            }
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

在1处,当手指按下的时候,会调用startNestedScroll(),

public boolean startNestedScroll(int axes, int type) {
        return getScrollingChildHelper().startNestedScroll(axes, type);
    }

getScrollingChildHelper()返回的是NestedScrollingChildHelper的实例。

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

会调用父类的onStartNestedScroll().并且onStartNestedScroll()返回true时,会调用父类的的onNestedScrollAccepted()。

@Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
    }
    
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i 

在父类中,优惠找对应的Behavior,然后调用Behavior的onStartNestedScroll()。结合最开始的那张图,发现对应的流程为,先由子类开始发起,然后询问父类,父类最后有调用Behavior中对应的方法。

在回到onInterceptTouchEvent()的ACTION_MOVE。在ACTION_MOVE中,将状态设置为SCROLL_STATE_DRAGGING。看返回值则为true,表示子类拦截,根据事件分发流程,自己拦截,将会交给自己的TouchEvent().

public boolean onTouchEvent(MotionEvent e) {
        

            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                ......
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(//1
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            }
            break;
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetScroll();//2
            }
            break;

            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();//3
            }
            break;
        }
        return true;
    }

在1处,直接调用dispatchNestedPreScroll(),该方法表示在子控件滑动前,将事件分发给父控件,由父控件判断消耗多少。

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
                type);
    }
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (dx != 0 || dy != 0) {
                ......
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
                .....
                return consumed[0] != 0 || consumed[1] != 0;
            } 
        }
        return false;
    }

进而会调用父类的onNestedPreScroll()。

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
    }
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 

最终调用的是Behavior的onNestedPreScroll。

当为UP和ACTION_CANCEL状态时,会调用stopNestedScroll(),其后面的分析跟前面的一样。

所以总结如下面的图所示:

CoordinatorLayout与Behavior源码分析
Behavior的工作原理.png
Behavior的布局
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 

先从mDependencySortedChildren拿取View,先判断它是否可见,不可见则跳过。

然后找对应的Behavior,如果Behavior有,则调用Behavior的onLayoutChild(),判断Behavior是否有自己的布局,如果有则按Behavior的布局走,没有则调用自己的onLayoutChild()。onLayoutChild中调用了layoutChild()。layoutChild里面调用onLayout().

Behavior的测量
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //省略部分代码….
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i 

观察上述代码,我们发现该方法与CoordinatorLayout的布局逻辑非常相似,也是对子控件进行遍历,并调那个用子控件的Behavior的onMeasureChild方法,判断是否自主测量,如果为true,那么则以子控件的测量为准。当子控件测量完毕后。会通过widthUsed 和 heightUsed 这两个变量来保存CoordinatorLayout中子控件最大的尺寸。这两个变量的值,最终将会影响CoordinatorLayout的宽高。

在实际开发中遇到一个问题,布局为在CoordinatorLayout中包裹着一个头部和ViewPager,向上滑动将将头部隐藏,发现ViewPAger下面将会留下一段空白。

发生这个情况的原因是,根据view的测量规则,viewpager的高度为屏幕的高度-头部的高度。当头部滚出屏幕后,就会出现一段空白。

解决方法:

重写onMeasure(),然后做如下操作。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
        layoutParams.height  = getMeasuredHeight() - mNavView.getMeasuredHeight();
        mViewPager.setLayoutParams(layoutParams);
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    }
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。