CoordinatorLayout源码分析

一、Behavior 的初始化

Behavior 是 CoordinatorLayout 里面的一个静态类。重写里面的若干方法,我们可以实现各种炫酷的效果,比如仿 UC 主页,仿新浪微博,仿 QQ 浏览器主页,仿知乎首页等效果。

Behavior是CoordinatorLayout与其子View进行通信的桥梁,如:事件拦截。Behavior是CoordinatorLayout的子View通过LayoutParams所持有的。

给CoordinatorLayout子View设置Behavior方式有三种:

  • 在布局文件中,给CoordinatorLayout子View设置layout_behavior属性,值为Behavior类的全路径。
  • 通过调用 CoordinatorLayout.LayoutParams 的 setBehavior(@Nullable Behavior behavior) 为子View设置 behavior
  • 通过CoordinatorLayout提供的注解:DefaultBehavior指定。
通过layout_behavior属性设置Behavior

在LayoutInflate解析xml文件,生成各个控件的对象的时候,都会调用父容器的 LayoutParams generateLayoutParams(AttributeSet attrs) 方法,为child生成一个LayoutParams,CoordinatorLayout重写了该方法,返回的对象类型是CoordinatorLayout.LayoutParams(CoordinatorLayout的内部类)。

CoordinatorLayout#generateLayoutParams(AttributeSet attrs)方法:

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

可以看出,该方法调用了CoordinatorLayout.LayoutParams两个参数的构造方法:

LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);

    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.CoordinatorLayout_Layout);

    this.gravity = a.getInteger(
            R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
            Gravity.NO_GRAVITY);
    // 设置一个锚点View,即该View向锚点View看齐,形成一个联动效果。
    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
            View.NO_ID);
    this.anchorGravity = a.getInteger(
            R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
            Gravity.NO_GRAVITY);

    this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
            -1);

    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
    dodgeInsetEdges = a.getInt(
            R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
    // 判断是否设置了layout_behavior
    mBehaviorResolved = a.hasValue(
            R.styleable.CoordinatorLayout_Layout_layout_behavior);
    if (mBehaviorResolved) {
       // 如果设置了layout_behavior属性,则解析该属性,并生成对应的Behavior对象
        mBehavior = parseBehavior(context, attrs, a.getString(
                R.styleable.CoordinatorLayout_Layout_layout_behavior));
    }
    a.recycle();

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

根据layout_behavior属性提供的值,通过反射的方式生成对象的Behavior对象,parseBehavior方法源码如下:

/**
* @params name  layout_behavior属性指定的Behavior类名全路径
*/
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) {
              // 根据全类名拿到类对象
                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                // 获取两个参数的构造函数对象
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                // 设置该构造方法可访问
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            // 调用构造方法,初始化Behavior对象,所以我们自定义的Behavior必须实现两个参数的构造函数
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

至此,通过layout_behavior属性设置Behavior的分析完成。

通过 CoordinatorLayout.LayoutParams 的 setBehavior(@Nullable Behavior behavior)方法设置Behavior

CoordinatorLayout的子View的LayoutParams都是CoordinatorLayout.LayoutParams类型,所以都可以通过获取子View的LayoutParams,为其设置Behavior。
CoordinatorLayout.LayoutParams的setBehavior方法如下:

public void setBehavior(@Nullable Behavior behavior) {
  // 如果设置的Behavior对象与已经存在的Behavior不是同一个,则替换
    if (mBehavior != behavior) {
        if (mBehavior != null) {
            // First detach any old behavior
            // 首先通知原来的Behavior解除绑定
            mBehavior.onDetachedFromLayoutParams();
        }

        // 为Behavior赋值
        mBehavior = behavior;
        mBehaviorTag = null;
        // 当前View设置了Behavior
        mBehaviorResolved = true;

        if (behavior != null) {
            // Now dispatch that the Behavior has been attached
            // 绑定Behavior到当前LayoutParams
            behavior.onAttachedToLayoutParams(this);
        }
    }
}
通过CoordinatorLayout提供的注解:DefaultBehavior设置Behavior。

通过DefaultBehavior注解设置为Child设置Behavior,会在CoordinatorLayout执行onMeasure方法的时候, 去解析该注册,并生成对象的Behavior对象,并通过CoordinatorLayout.LayoutParams#setBehavior方法设置给View的LayoutParams。

在onMeasure方法中,会调用prepareChildren()方法, 在该方法内部会调用getResolvedLayoutParams(View child) 方法,在这个方法里面,才真正的去解析child的DefaultBehavior注解:

LayoutParams getResolvedLayoutParams(View child) {
  // 获取child的LayoutParams(类型是CoordinatorLayout.LayoutParams)
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    // 首先判断该View是否设置过Behavior
    if (!result.mBehaviorResolved) {
        Class<?> childClass = child.getClass();
        DefaultBehavior defaultBehavior = null;
        // 循环遍历,判断child是否设置了DefaultBehavior注解
        while (childClass != null &&
                (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
            childClass = childClass.getSuperclass();
        }
        // 如果child设置DefaultBehavior注解
        if (defaultBehavior != null) {
            try {
              // 根据注解设置的Behavior实例化Behavior对象,并调用LayoutParams#setBehavior方法设置给该child的LayoutParams
                result.setBehavior(
                        defaultBehavior.value().getDeclaredConstructor().newInstance());
            } catch (Exception e) {
                Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                        " could not be instantiated. Did you forget a default constructor?", e);
            }
        }
        // 把该child是否处理过Behavior标志设置为true
        result.mBehaviorResolved = true;
    }
    return result;
}

AppBarLayout就是采用这种方式设置Behavior的:

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

二、CoordinatorLayout特性

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

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

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

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

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

  • 测量过程
  • 布局过程
  • 绘制过程
  • 触摸事件处理
1、CoordinatorLayout的测量过程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1.准备子View,根据依赖关系,为子view排序
        prepareChildren();
        // 2.根据情况判断是否需要添加或者移除绘制监听事件
        ensurePreDrawListener();

        --------------------------------------------------------------
        // 3.测量子view和CoordinatorLayout的宽高
        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();
            --------------------------------------------------------------

            // 获取子view的Behavior
            final Behavior b = lp.getBehavior();
            // 如果Behavior为空,或者子view通过Behavior自己测量了自己,则CoordinatorLayout不在测量该子view
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                // CoordinatorLayout测量该子view
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
            // 取所有子view中测量的最大宽度
            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);
            // 取所有子view中测量的最大高度
            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }
        // 根据测量模式、父容器宽度和测量的子view的最大宽度,得到最终的CoordinatorLayout宽度
        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);
        // 把最终测量的宽高设置给CoordinatorLayout
        setMeasuredDimension(width, height);
    }

由上面的源码分析我们知道,测量过程分为三个步骤:

  • 根据子view的依赖关系,对子view进行排序
  • 根据需要判断是否需要添加或者移除绘制监听事件(OnPreDrawListener)
  • 测量所有子view的宽高,取最大的子view宽度、高度作为CoordinatorLayout的宽高
1.1 根据子view的依赖关系,对子view进行排序,prepareChildren()方法源码如下:
private void prepareChildren() {
        mDependencySortedChildren.clear();
        mChildDag.clear();

        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            // 找到当前子view的所依赖的锚点view
            lp.findAnchorView(this, view);
            // mChildDag是一个有向图,用于临时存放子view; 把view当做一个节点添加到有向图mChildDag中
            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);
                // 判断两个子view是否具有依赖关系
                if (lp.dependsOn(this, view, other)) {
                   // 子view是否已经作为节点添加到图中
                    if (!mChildDag.contains(other)) {
                        // Make sure that the other node is added
                        mChildDag.addNode(other);
                    }
                    // Now add the dependency to the graph
                    // 以当前view为起点,以被依赖的view为终点,构成一条有向边,并添加到图中
                    // 一条边的两个顶点都必须已经添加到图中
                    mChildDag.addEdge(other, view);
                }
            }
        }

        // Finally add the sorted graph list to our list
        // mChildDag.getSortedList():采用深度优先遍历算法,获取有向图中顶点的顺序列表
        // 1.如果所有的子view都没有依赖关系,则返回view列表就是子view添加的顺序
        // 2.如果有依赖关系,则被依赖的排在后面,依赖的view排在前面
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        // 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
        // 数据倒置,结果:
        // 1.如果所有的子view都没有依赖关系,则最终返回的就是添加顺序的倒置,即最后添加的view排在最前面
        // 2.如果有依赖关系,则被依赖的排在前面,依赖的view排在后面
        Collections.reverse(mDependencySortedChildren);
    }

由上述代码分析,可以知道该方法主要是对子view进行排序,最终的排序结果是:

  • 如果所有的子view都没有依赖关系,则最终返回的就是添加顺序的倒置,即最后添加的view排在最前面
  • 如果有依赖关系,则被依赖的排在前面,依赖的view排在后面
  • 举例说明:
    排序的结果是 最后被依赖的 View 会排在最前面。举个例子 A 依赖于 B,那么 B会排在前面,A 会排在 B 的 后面。这样的排序结果是合理的,因为 A 既然依赖于 B,那么 B 肯定要优先 measure。
1.2 根据子view的依赖关系,对子view进行排序,ensurePreDrawListener()方法源码如下:
void ensurePreDrawListener() {
    boolean hasDependencies = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        // 判断该子view是否有依赖的view
        if (hasDependencies(child)) {
            hasDependencies = true;
            break;
        }
    }
    // mNeedsPreDrawListener默认为false,如果有依赖,hasDependencies=true
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {// 如果有依赖
            addPreDrawListener();
        } else {// 没有依赖,则删除绘制监听事件
            removePreDrawListener();
        }
    }
}
---------------------

其实它所做的工作就是 判断 子View ,如果有依赖的话,添加 OnPreDrawListener 监听,没有的话,移除 OnPreDrawListener 监听。

2、CoordinatorLayout的布局过程:
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);
        // 跳过被gone掉的view
        if (child.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();
        // 如果Behavior为空,或者子view通过Behavior自己layout了,则CoordinatorLayout不在为该子view进行布局
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
          // CoordinatorLayout为该子view布局
            onLayoutChild(child, layoutDirection);
        }
    }
}

//  为子view布局
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.");
      }
      // 如果该子view设置了锚点view
      if (lp.mAnchorView != null) {
          layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
      } else if (lp.keyline >= 0) {
          layoutChildWithKeyline(child, lp.keyline, layoutDirection);
      } else {
        // 普通子view布局
          layoutChild(child, layoutDirection);
      }
}

onLayout方法代码比较简单,主要做了两件事:

  • 如果设置了Behavior,调用Behavior#onLayoutChild方法进行布局
  • 否则调用CoordinatorLayout#onLayoutChild方法进行布局

由以上分析可以知道,我们可以通过Behavior#onLayoutChild方法,控制view的布局(即位置)。

CoordinatorLayout#onLayoutChild方法中,布局主要分为三种情况:

  • 如果view设置了锚点view
  • 设置了keyLine
  • 默认布局方式

详细描述:

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

CoordinatorLayout没有重写dispatchDraw,但是重写了onDraw和drawChild 方法:

@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的处理也只是对于状态栏背景的处理,无足轻重.

ViewGroup在dispatchDraw方法中,会调用drawChild方法进行子View的绘制:

@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();
            // 如果子view是不透明的
            if (child.isOpaque()) {
                // If the child is opaque, there is no need to draw behind it so we'll inverse
                // clip the canvas
                // 如果子view是不透明的,裁剪画布,留下除该子view以外的区域
                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);
        }
    }
    // 绘制子view
    return super.drawChild(canvas, child, drawingTime);
}

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

3、CoordinatorLayout的触摸事件处理:

对于一个ViewGroup来说,事情的处理会经过三个方法:

  • 分发事件:dispatchTouchEvent
  • 判断是否拦截事件:onInterceptTouchEvent
  • 处理事件:onTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    MotionEvent cancelEvent = null;

    final int action = ev.getActionMasked();

    // Make sure we reset in case we had missed a previous important event.
    // 重置响应的Behavoir
    if (action == MotionEvent.ACTION_DOWN) {
        resetTouchBehaviors();
    }
    // 在这里选择一个最佳Behavior进行处理
    // 调用performIntercept方法,判断是否需要拦截此次事件
    // 1.如果有子view的Behavior的onInterceptTouchEvent方法返回true,则mBehaviorTouchView等于该子View
    //  则intercepted==true,所以CoordinatorLayout会拦截所有的事件
    // 2.如果有子view的Behavior的onInterceptTouchEvent方法均没有返回true,则CoordinatorLayout不拦截事件。
    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }
    // 重置响应的Behavior
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;
    final int action = ev.getActionMasked();
    // mBehaviorTouchView在performIntercept方法中赋值,如果子view的Behavior不为空,且消费了对应的事件的子view
    // 如果mBehaviorTouchView不等于空或者performIntercept返回true
    // 1.如果mBehaviorTouchView不等于空,则performIntercept方法不会执行,即mBehaviorTouchView拦截了事件
    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
    // 说明没有子view消费事件或者所有子view都没有设置Behavior,则事件交给上层派发
    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);
    }
    if (!handled && action == MotionEvent.ACTION_DOWN) {
    }
    if (cancelEvent != null) {
        cancelEvent.recycle();
    }
    // 恢复状态
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }
    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;
    // API>=21时,使用elevation由低到高排列View;API<21时,按View添加顺序排列
    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();
        // 1.判断是否派发cancel事件
        // 如果找到一个子view消费了事件或者
        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.
            // 对于非down事件来说,如果有子view的Behavior的onInterceptTouchEvent方法返回true或者onTouchEvent方
            // 法返回true ,则向该子view之后的所有子view传递一个cancel事件
            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;
        }

        // 2.事件处理逻辑
        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.

        // 3.判断该子View是否阻止与其它View的交互,即其它View是否能接收到事件

        // didBlockInteraction方法:如果该子view关联的Behavior之前阻止了与其它view的交互,则返回true,默认返回false
        // isBlockingInteractionBelow方法:判断该子view是否阻止与其它view的交互
        // 当子View的Behavior的blocksInteractionBelow返回true时isBlockingInteractionBelow为true.
        // 即当 前Behavior附属View空间之外的阴影透明度大于0时(getScrimOpacity方法),isBlockingInteractionBelow为true
        final boolean wasBlocking = lp.didBlockInteraction();
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
        newBlock = isBlocking && !wasBlocking;
        // 假设View:A的Behavior的getScrimOpacity( 当前Behavior附属的View空间之外的阴影透明度)方法大于0,
        // 则isBlockingInteractionBelow方法返回true,则mDidBlockInteraction==true
        // 之后再执行didBlockInteraction方法时,也返回true
        // 所以在onInterceptTouchEvent方法中,对于View:A来说,wasBlocking==false,isBlocking==true,newBlock==true
        // 在onTouchEvent方法中执行performIntercept方法时,对于View:A来说,wasBlocking==true,isBlocking==true,newBlock==false,
        // 所以直接跳出了循环,其它View的都不会接收到事件

        // 如果是非down事件,且View的Behavior设置了空间之外的透明度,则直接跳出循环
        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;
}

performIntercept方法详解:

  • 先对各Behavior的onInterceptTouchEvent方法分发down事件,直到有子View的Behavior的onInterceptTouchEvent返回true或者LayoutParams的isBlockingInteractionBelow返回true,则停止继续分发,即其它子View不会接收到事件。
  • 当Behavior的onInterceptTouchEvent返回true,会用一个mBehaviorTouchView的变量标记Behavior所附属的View
  • 如果有子View的Behavior的onInterceptTouchEvent返回true,则CoordinatorLayout的所有子View的触摸事件都将失去响应.

onTouchEvent的逻辑:

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

注意:

  • 不管是在onInterceptTouchEvent或是onTouchEvent中,传给子View Behavior的MotionEvent是基于CoordinatorLayout的而不是基于子View的.
  • performIntercept中的子View的遍历,使用了getTopSortedChildren(topmostChildList);方法,该方法会生成一个根据层级从上往下的子View列表,这个列表在api21之前以子View添加顺序相反的顺序作为默认顺序,在api21及以后会根据子View的Elevation排序.performIntercept使用这个列表进行遍历,从此也可以很轻易的知道,事件分发是根据子View层级从层顶到层底分发到各子View的Behavior的.

总结:

  • onInterceptTouchEvent和onTouchEvent其实也是调用了Behavior的onInterceptTouchEvent和onTouchEvent,
  • 如果所有子View都没有设置Behavior或者子View的Behavior没有做处理,则CoordinatorLayout本身没有做过于特殊的处理.此时依然走传统的事件传递流程
  • 整体而言,触摸事件的处理显得有点复杂而且繁琐,而且会有大量的非正常的cancel事件出现,由于其复杂的逻辑,重写Behavior的onInterceptTouchEvent和onTouchEvent时应当非常注意其逻辑在CoordinatorLayout中onInterceptTouchEvent和onTouchEvent的合理性.

3、CoordinatorLayout的嵌套滑动支持

CoordinatorLayout嵌套滚动时序图分析:
[外链图片转存失败(img-XigELvH1-1562918960560)(https://raw.githubusercontent.com/meiSThub/AtomProject/master/image/pic_4.png)]
由上面CoordinatorLayout类的定义可以看出,CoordinatorLayout实现了NestedScrollParent2接口,NestedScrollParent2接口继承与NestedScrollParent接口。

NestedScrollParent接口的定义:

public interface NestedScrollingChild {

    /**
     * 设置嵌套滑动是否可用
     *
     * @param enabled
     */
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 判断嵌套滑动是否可用
     *
     * @return
     */
    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动,在滚动之前调用
     *
     * @param axes 表示方向 有一下两种值
     *             ViewCompat.SCROLL_AXIS_HORIZONTAL 横向哈东
     *             ViewCompat.SCROLL_AXIS_VERTICAL 纵向滑动
     */
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滑动
     */
    public void stopNestedScroll();

    /**
     * 是否有父View 支持 嵌套滑动,  会一层层的网上寻找父View
     * @return
     */
    public boolean hasNestedScrollingParent();

    /**
     * 在处理滑动之后 调用
     * @param dxConsumed x轴上 被消费的距离
     * @param dyConsumed y轴上 被消费的距离
     * @param dxUnconsumed x轴上 未被消费的距离
     * @param dyUnconsumed y轴上 未被消费的距离
     * @param offsetInWindow view 的移动距离
     * @return
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * 一般在滑动之前调用, 在ontouch 中计算出滑动距离, 然后 调用该 方法, 就给支持的嵌套的父View 处理滑动事件
     * 如:在RecyclerView的onTouchEvent方法中,计算出滑动的距离,然后调用该方法把事件传递给支持嵌套滑动的父View,如:CoordinatorLayout
     * @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离
     * @param dy y 轴上滑动的距离
     * @param consumed 一个数组, 可以传 一个空的 数组,  表示 x 方向 或 y 方向的事件 是否有被消费
     * @param offsetInWindow   支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     *
     * @param velocityX x 轴上的滑动速度
     * @param velocityY y 轴上的滑动速度
     * @param consumed 是否被消费
     * @return
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     *
     * @param velocityX x 轴上的滑动速度
     * @param velocityY y 轴上的滑动速度
     * @return
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingParent2接口的定义:

public interface NestedScrollingParent2 extends NestedScrollingParent {
    /**
     * 判断父容器是否支持嵌套滑动
     * 当子view的调用NestedScrollingChild的方法startNestedScroll时,会调用该方法.
     * 一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数
     * 假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断,如:只纵向滑动时接收滑动事件。
     * @param child ViewParent包含触发嵌套滚动的view的对象
     * @param target 触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的,如RecyclerView
     * @param axes 嵌套滚动的滚动方向 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     * @param type the type of input which cause this scroll event
     * @return true if this ViewParent accepts the nested scroll operation
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    /**
     * 接受嵌套滑动,只有在onStartNestedScroll方法返回true的时候调用,否则不调用
     * 它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化的.
     *
     * <p>This method will be called after
     * {@link #onStartNestedScroll(View, View, int, int) onStartNestedScroll} returns true. It
     * offers an opportunity for the view and its superclasses to perform initial configuration
     * for the nested scroll. Implementations of this method should always call their superclass's
     * implementation of this method if one is present.</p>
     *
     * @param child Direct child of this ViewParent containing target
     * @param target View that initiated the nested scroll
     * @param axes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     * @param type the type of input which cause this scroll event
     * @see #onStartNestedScroll(View, View, int, int)
     * @see #onStopNestedScroll(View, int)
     */
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);

    /**
     * 停止嵌套滑动,即滑动停止。可以做一个还原的工作
     * 停止滚动了,当子view调用stopNestedScroll时会调用该方法.
     *
     * @param target View that initiated the nested scroll
     * @param type the type of input which cause this scroll event
     */
    void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

    /**
     * 这个方法在滚动View(如RecyclerView)分发嵌套滚动事件的时候调用,这个方法只有在onStartNestedScroll方法返
     * 回true的时候才会调用
     * 即在滚动View(如RecyclerView)滚动之后,才会调用该方法
     *
     * 消耗的距离和未消耗的距离都会报告给父容器,
     * <p>Both the consumed and unconsumed portions of the scroll distance are reported to the
     * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll
     * position of multiple child elements, for example. The unconsumed portion may be used to
     * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling
     * a list within a vertical drawer where the drawer begins dragging once the edge of inner
     * scrolling content is reached.</p>
     *
     * @param target The descendent view controlling the nested scroll
     * @param dxConsumed 表示target(如RecyclerView)已经消费的x方向的距离
     * @param dyConsumed 表示target(如RecyclerView)已经消费的y方向的距离
     * @param dxUnconsumed 表示x方向剩下的滑动距离(如RecyclerView未消耗的距离)
     * @param dyUnconsumed 表示y方向剩下的滑动距离(如RecyclerView未消耗的距离)
     * @param type the type of input which cause this scroll event
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

    /**
     * 当子view调用dispatchNestedPreScroll方法是,会调用该方法.
     * 该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数
     * consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
     *
     * 当嵌套滑动发送时,如果父容器想消耗一部分的滑动距离,就可以在这个方法中处理。
     * 如滑动RecyclerView的时候,我想让父容器的内容整体移动对应的滑动距离,则可以在该方法中处理相关的逻辑,并
     * 把消耗的距离通过consumed参数,告知RecyclerView。如果消耗了全部的距离,则RecyclerView本身就不滚动了。
     *
     * <p>When working with nested scrolling often the parent view may want an opportunity
     * to consume the scroll before the nested scrolling child does. An example of this is a
     * drawer that contains a scrollable list. The user will want to be able to scroll the list
     * fully into view before the list itself begins scrolling.</p>
     *
     * <p><code>onNestedPreScroll</code> is called when a nested scrolling child invokes
     * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should
     * report how any pixels of the scroll reported by dx, dy were consumed in the
     * <code>consumed</code> array. Index 0 corresponds to dx and index 1 corresponds to dy.
     * This parameter will never be null. Initial values for consumed[0] and consumed[1]
     * will always be 0.</p>
     *
     * @param target 触发嵌套滚动的view,如RecyclerView
     * @param dx 表示target本次滚动产生的x方向的滚动总距离
     * @param dy 表示target本次滚动产生的y方向的滚动总距离
     * @param consumed 表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
     * @param type the type of input which cause this scroll event
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
            @NestedScrollType int type);

}
NestedScrollingParent 与 NestedScrollingChild 接口方法是怎样回调的
  1. 在 Action_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法
  2. 在 Action_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动
  3. 当 ScrollingChild(如:RecyclerView) 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法
  4. 在 Action_up,ACTION_CANCEL 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。
  5. 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作
  6. 在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法
CoordinatorLayout属性总结:

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

  • 如果子View存在Behavior,CoordinatorLayout对子View的大部分操作都会交给Behavior来处理,借助这个属性,可以通过设置Behavior来实现对子View操作的劫持.
  • CoordinatorLayout在子View没有设置Behavior的情况下,几乎是就是一个FrameLayout.
  • CoordinatorLayout支持嵌套滑动,但是都是交给子View的Behavior来处理的.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值