CoordinatorLayout源码解析

来源:互联网 发布:巫师3优化好 编辑:程序博客网 时间:2024/05/22 06:49

图片来源于网络

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

备注:本文源码基于25.3.0


监听View的变化

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

@Overridepublic 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;}@Overridepublic 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);        }    }}@Overridepublic 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();    }    ...}@Overridepublic 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);    }}@Overridepublic 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);    }}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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,代码如下:

@Overridepublic 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的源码解析也算是完结了,虽然只是分析了部分源码,但是也大致清楚了其工作原理。如果有分析得不对的地方还望指正。最后推荐几篇不错的文章:

  • 源码看CoordinatorLayout.Behavior原理——亓斌
  • 一步一步深入理解CoordinatorLayout——程序亦非猿
  • Android NestedScrolling 实战
  • Android NestedScrolling机制完全解析 带你玩转嵌套滑动——张鸿洋
0 0
原创粉丝点击