一、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);
}
}
}
ViewGroup的onDraw只有在含有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 接口方法是怎样回调的
- 在 Action_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法
- 在 Action_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动
- 当 ScrollingChild(如:RecyclerView) 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法
- 在 Action_up,ACTION_CANCEL 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。
- 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作
- 在 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来处理的.