CoordinatorLayout 源码解析

《CoordinatorLayout 源码解析》

图片来源于网络

Google推出Support Design Library已经两年了,没错,两年了!虽然推出了这么久,也只是使用,并没有深入研究过,所以想要深入了解一下,于是有了此文。

备注:本文源码基于25.3.0

监听View的变化

onAttachedToWindow()方法中,使用ViewTreeObserver注册一个回调监听View变化。

@Override
public void onAttachedToWindow() {
    ...
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
        //视图将要绘制时,会回调此接口
    }
    ...
}

查看下OnPreDrawListener的具体实现,发现其调用了onChildViewsChanged()方法,并传递了一个typeEVENT_PRE_DRAW的参数,来进行标记。让我们来看看onChidViewsChanged()方法具体做了些什么?

子View能够相互依赖工作的根源——onChildViewsChanged()

// type:根据type来判断是什么时期调用的此方法,具体有3个type
// EVENT_PRE_DRAW 将要绘制时, EVENT_NESTED_SCROLL 滚动, EVENT_VIEW_REMOVED 移除
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 < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
            // Do not try to update GONE child views in pre draw updates.
            continue;
        }

        // Check child views before for anchor
        for (int j = 0; j < i; j++) {
            final View checkChild = mDependencySortedChildren.get(j);

            if (lp.mAnchorDirectChild == checkChild) {
                //调整child的位置到其所依赖的View(layout_anchor所设置的)的相关位置
                offsetChildToAnchor(child, layoutDirection);
            }
        }

        // Get the current draw rect of the view
        getChildRect(child, true, drawRect);

        // Accumulate inset sizes
        // 根据不同的Gravity,记录view进入CoordinatorLayout的尺寸
        // lp.insetEdge保存的是子View以什么方式进入CoordinatorLayout
        if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
            final int absInsetEdge = GravityCompat.getAbsoluteGravity(
                    lp.insetEdge, layoutDirection);
            switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
                case Gravity.TOP:
                    inset.top = Math.max(inset.top, drawRect.bottom);
                    break;
                case Gravity.BOTTOM:
                    inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
                    break;
            }
            switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.LEFT:
                    inset.left = Math.max(inset.left, drawRect.right);
                    break;
                case Gravity.RIGHT:
                    inset.right = Math.max(inset.right, getWidth() - drawRect.left);
                    break;
            }
        }
        // lp.dodgeInsetEdges 保存的是子View(A)需要避免的‘位置’
        // 其他子View(B)以相同的方式进入,会影响子View(A)的显示,子View(A)需要改变自身,避免被覆盖
        // Dodge inset edges if necessary
        if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
            offsetChildByInset(child, inset, layoutDirection); //子View(A)改变自身位置
        }

        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();
            //layoutDependsOn方法用于确定两个View是否有依赖关系
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    // If this is from a pre-draw and we have already been changed
                    // from a nested scroll, skip the dispatch and reset the flag
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }

                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:
                        // 回调Behavior的b.onDependentViewChanged,处理是否跟随依赖View,而改变自身状态(具体的改变状态的方式,在此方法中处理)
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }

                if (type == EVENT_NESTED_SCROLL) {
                    // If this is from a nested scroll, set the flag so that we may skip
                    // any resulting onPreDraw dispatch (if needed)
                    checkLp.setChangedAfterNestedScroll(handled);
                }
            }
        }
    }

    releaseTempRect(inset);
    releaseTempRect(drawRect);
    releaseTempRect(lastDrawRect);
}

通过对onChildViewsChanged()方法的解读,发现其使用了三种方式来处理View之间的关联。

  1. 通过判断layout_ahchor所设置的锚视图,使用offsetChildToAnchor()方法来改变View的位置。
  2. 通过LayoutParams中存储的insetEdgedodgeInsetEdges来进行判断(详细信息请查看注释),最后调用offsetChildByInset()方法来改变View的位置。
  3. 通过BehaviorlayoutDependsOn()方法,如果Behavior重写了layoutDependsOn()方法,在其中做了View的依赖判断,最终会回调BehavioronDependentViewChanged()方法,具体要怎么处理,就是onDependentViewChanged()实现的。

注意:mDependencySortedChildren是根据View的依赖关系排序存储的,具体排序涉及到的方法有prepareChildren()CoordinatorLayout.LayoutParams.dependsOn(),在这里就不做阐述了,有兴趣的可以自行研读。

我们再来看看offsetChidToAnchor()offsetChildByInset()方法。

设置layout_anchor锚视图后,View的位置是如何改变的——offsetChildToAnchor

void offsetChildToAnchor(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.mAnchorView != null) {
        final Rect anchorRect = acquireTempRect();
        final Rect childRect = acquireTempRect();
        final Rect desiredChildRect = acquireTempRect();
        // 获取Anchor View的Rect
        getDescendantRect(lp.mAnchorView, anchorRect);
        // 获取child View的Rect
        getChildRect(child, false, childRect);

        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        // 获取到想要的Rect,保存在desiredChildRect中
        getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
                desiredChildRect, lp, childWidth, childHeight);
        // 通过对desiredChildRect和childRect比对,看是否位置发生了变化
        boolean changed = desiredChildRect.left != childRect.left ||
                desiredChildRect.top != childRect.top;
        // 加入margin和padding,计算最终的Rect,保存在desiredChildRect中
        constrainChildRect(lp, desiredChildRect, childWidth, childHeight);

        final int dx = desiredChildRect.left - childRect.left;
        final int dy = desiredChildRect.top - childRect.top;
        //改变View的位置
        if (dx != 0) {
            ViewCompat.offsetLeftAndRight(child, dx);
        }
        if (dy != 0) {
            ViewCompat.offsetTopAndBottom(child, dy);
        }
        // 如果位置有变化,且View有Behavior,则通知其Behavior的onDependentViewChanged方法
        if (changed) {
            // 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);
            }
        }

        releaseTempRect(anchorRect);
        releaseTempRect(childRect);
        releaseTempRect(desiredChildRect);
    }
}

分析完offsetChildToAnchor(),我们知道了,当设置了layout_anchor,如果View的位置改变,也会回调BehavioronDependentViewChanged()方法。

offsetChildByInset()又做了些什么呢?

子View之间是如何避免被“遮挡”——offsetChidByInset

private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) {
     ...
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Behavior behavior = lp.getBehavior();
    final Rect dodgeRect = acquireTempRect();
    final Rect bounds = acquireTempRect();
    bounds.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
    // getInsetDodgeRect方法默认返回false,需要自己实现最终的Rect并设置给dodgeRect,并返回true
    if (behavior != null && behavior.getInsetDodgeRect(this, child, dodgeRect)) {
        // Make sure that the rect is within the view's bounds
        if (!bounds.contains(dodgeRect)) {
                throw new IllegalArgumentException("Rect should be within the child's bounds."
                        + " Rect:" + dodgeRect.toShortString()
                        + " | Bounds:" + bounds.toShortString());
        }
    } else {
        dodgeRect.set(bounds);
    }

    // We can release the bounds rect now
    releaseTempRect(bounds);

    if (dodgeRect.isEmpty()) {
        // Rect is empty so there is nothing to dodge against, skip...
        releaseTempRect(dodgeRect);
        return;
    }
    // 获取需要躲避的方向
    final int absDodgeInsetEdges = GravityCompat.getAbsoluteGravity(lp.dodgeInsetEdges,
            layoutDirection);
    // 根据absDodgeInsetEdges,改变View相应的位置
    boolean offsetY = false;
    if ((absDodgeInsetEdges & Gravity.TOP) == Gravity.TOP) {
        int distance = dodgeRect.top - lp.topMargin - lp.mInsetOffsetY;
        if (distance < inset.top) {
            setInsetOffsetY(child, inset.top - distance);
            offsetY = true;
        }
    }
    ...// 省略Gravity.BOTTOM判断,和Gravity.TOP类似
    if (!offsetY) {
        setInsetOffsetY(child, 0);
    }

    boolean offsetX = false;
    if ((absDodgeInsetEdges & Gravity.LEFT) == Gravity.LEFT) {
        int distance = dodgeRect.left - lp.leftMargin - lp.mInsetOffsetX;
        if (distance < inset.left) {
            setInsetOffsetX(child, inset.left - distance);
            offsetX = true;
        }
    }
    ...// 省略Gravity.RIGHT判断,和Gravity.LEFT类似
    if (!offsetX) {
        setInsetOffsetX(child, 0);
    }

    releaseTempRect(dodgeRect);
}

阅读完offsetChildByInset()方法,发现如果我们的View需要避免某个方向的其他View进入,我们需要实现BehaviorgetInsetDodgeRect()方法,还要设置LayoutParams.dodgeInsetEdgesdodgeInsetEdges的设置可以重写BehavioronAttachedToLayoutParams(CoordinatorLayout.LayoutParams params)方法。

CoodinatorLayout中的滚动机制——NestedScrolling

NestedScrolling机制,是从Android 5.0开始引入,提供了一套父View和子View滑动交互的机制。包含两个接口和两个帮助类:

  1. NestedScrollingChild
  2. NestedScrollingParent
  3. NestedScrollingChildHelper
  4. NestedScrollingParentHelper

父View必须实现NestedScrollingParent接口,而其必须要有一个子View实现NestedScrollingChild接口,只有这样才能达到预想的滑动交互效果。实现NestedScrollingChild接口很简单,只需要在其实现的方法中调用NestedScrollingChildHelper中对应的方法即可。并在相应的Touch事件中调用startNestedScroll()方法以及stopNestedScroll()方法,剩下的通知父View等事情,NestedScrollingChildHelper都帮我们处理好了。

对与NestedScrolling机制,这里只做简要的说明,具体的分析留待以后的文章。如果有想了解的,可先自行搜索相关文章。

回到正题CoordinatorLayout,没错,CoordinatorLayout充当的就是父View的角色,其实现了NestedScrollingParent接口,具体的实现如下所示:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;
            //存储是否要处理滚动
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    ...
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
        }
    }
}

@Override
public void onStopNestedScroll(View target) {
    mNestedScrollingParentHelper.onStopNestedScroll(target);
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            viewBehavior.onStopNestedScroll(this, view, target);
        }
        lp.resetNestedScroll();
        lp.resetChangedAfterNestedScroll();
    }
    ...
}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    final int childCount = getChildCount();
    boolean accepted = false;
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed);
            accepted = true;
        }
    }
    if (accepted) {// 调用了前面讲的重要方法
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    int xConsumed = 0;
    int yConsumed = 0;
    boolean accepted = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            mTempIntPair[0] = mTempIntPair[1] = 0;
            viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

            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代表自身去执行相应方向的距离滑动
    consumed[0] = xConsumed;
    consumed[1] = yConsumed;
    if (accepted) {//注意调用了onChildViewsChanged
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
                    consumed);
        }
    }
    if (handled) {// 注意调用了onChildViewsChanged
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
    return handled;
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...// 省略获取view及其LayoutParams
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
        }
    }
    return handled;
}

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

代码虽然有点长,但是没有什么难点,相应的方法都被委派给了Behavior的对应方法处理。如果我们的子View想要响应滚动效果,只需要重写Behavior的相关方法。

开发最关心的——Behavior

通过上面的分析,大家应该都发现了,几乎所有的东西都和Behavior有关。我们想要自己的控件在CoordinatorLayout中有炫酷的效果,那么我们只需要自定义自己的Behavior,实现相关方法即可。

如何给View设置Behavior

  1. 在xml中通过layout_behavior绑定,例如app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
  2. 在自定义的控件中,使用DefaultBehavior注解绑定,例如AppBarLayout中的@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
  3. 在代码中通过LayoutParams设置

需要注意的:

  1. Behavior需要设置给CoordinatorLayout的直接子View,因为Behavior的解析是在CoordinatorLayout.LayoutParams的构造方法中进行的,只有直接子View才具有CoordinatorLayout.LayoutParams
  2. 自定义Behavior必须具有Behavior(Context context, AttributeSet attrs)构造方法,因为Behavior的实例化是靠类的反射完成的,具体可看如下源码:
//指定Behavior的参数类型
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
};

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }
    // 获取完整包名
    final String fullName;
    if (name.startsWith(".")) {
        // Relative to the app package. Prepend the app package name.
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 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<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            // 利用反射获取Behavior
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    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中常用的方法

layoutDependsOn

通过之前的分析,layoutDependsOn是用来确定依赖关系的,如果想要控件依赖某个控件,重写这个方法是必须的。例如给RecycleView设置的ScrollingViewBehavior,关联AppBarLayout,代码如下:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    // We depend on any AppBarLayouts
    return dependency instanceof AppBarLayout;
}

onDependentViewChanged

根据之前的分析,此方法会在依赖View发生改变的时候回调,我们可以在此方法中做相应的处理,达到想要的效果。

 /* <p>If the Behavior changes the child view's size or position, it should return true. * The default implementation returns false.</p> * * @param parent the parent view of the given child * @param child the child view to manipulate * @param dependency the dependent view that changed * @return true if the Behavior changed the child view's size or position, false otherwise */
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
    return false;
}

注意官方的注释,如果改变了child的位置、大小,需要返回true

onStartNestedScroll

如果想要实现滚动效果,在你想要滚动的条件下,此方法需要返回true。其返回值会通过lp.acceptNestedScroll(accepted)存储在LayoutParams中。其他滚动相关回调都会基于此返回值,true的时候才会被回调。可以查看前面的CoordinatorLayout滚动机制部分,都用用下面的代码进行判断:

if (!lp.isNestedScrollAccepted()) {
            continue;
        }

onNestedPreScroll

Child滑动前,都会通知Parent,Parent会回调此方法,可以在此方法中做滑动拦截。该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2。

onNestedScroll

Child滑动以后,会通知Parent,回调onNestedScroll()。可以在此方法中做进一步的滚动处理,因为其可以获取到被消耗的和未被消耗的滚动距离。

结语

到这里CoordinatorLayout的源码解析也算是完结了,虽然只是分析了部分源码,但是也大致清除了其工作原理。如果有分析得不对的地方还望指正。最后推荐几篇不错的文章:

    原文作者:Android
    原文地址: https://juejin.im/entry/58df274161ff4b006162ccc5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞