本篇来自 忆_析风 的投稿,对 Android CoordinatorLayout和Behavior进行了细致分析,希望对大家有所帮助!
忆_析风 的博客地址:
https://blog.csdn.net/dqh147258
前言
在 Materials Design 中有一个名为 CoordinatorLayout 的布局,这是一个神奇的布局,可以实现各种控件间的联动效果,比如底部 FloatingActionBar 跟随 Snackbar 弹出而上移
比如 AppBarLayout 跟随 NestedScrollView 滑动而伸缩,FloatingActionBar 跟随 AppBarLayout 伸缩而显隐
这些都是非常赞的效果实现,这次我们就从源码角度来分析下这个布局和协助它实现控件联动效果的 Behavior.
CoordinatorLayout特性
要知道一个类的特性,应当从类继承和接口开始
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
//...............
}
从上面可以知道这个布局是一个 ViewGroup,而且支持作为嵌套滑动的父布局.
对于一个 ViewGroup,应该关心什么呢?
个人觉得比较重要的有这几点
-
测量过程
-
布局过程
-
绘制过程
-
触摸事件处理
接下来看看 CoordinatorLayout 的这些重点过程的处理方式
CoordinatorLayout的测量过程
先查看其测量过程,其 onMeasure 方法的核心代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//...............
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
//..................
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin);
heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin);
childState = View.combineMeasuredStates(childState, child.getMeasuredState());
}
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);
setMeasuredDimension(width, height);
}
代码主体是测量每一个子View的宽高,然后取子View中最大的距离消耗作为自己的宽高,这种方式貌似和 FrameLayout 很像.
然后有一段值得注意的代码
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
这里 子View 的测量过程居然可以使用 子View 的 Behavior 的 onMeasureChild 方法代替,这感觉就像被黑客劫持了一样,子View 自带的测量都废了.
CoordinatorLayout的布局过程
再看其布局过程,查看其 onLayout 代码如下
@Override
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);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
这里也是一样如果有 Behavior 存在,则使用 Behavior 中的布局方法.
如果没有 Behavior 呢?
继续追踪 CoordinatorLayout 自带的 onLayoutChild 方法
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.");
}
if (lp.mAnchorView != null) {
layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
} else if (lp.keyline >= 0) {
layoutChildWithKeyline(child, lp.keyline, layoutDirection);
} else {
layoutChild(child, layoutDirection);
}
}
由于其后续涉及的代码较多,在此只做简单说明
如果 子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 也是一致的.
在 onLayout 中的布局是根据一个 子View 列表 mDependencySortedChildren 依次布局的,查看这个 子View 列表的定义
private final List<View> mDependencySortedChildren = new ArrayList<>();
看名字都知道,这是特殊排序过的,这个列表就很有意思了.
由于 子View 的 Behavior 可能对其它 子View 可能存在位置依赖关系,为了实现将被依赖的 子View 先布局而创建了这个列表.这个列表如何排序生成的呢?源码中在 CoordinatorLayout 的 onMeasure 中的 prepareChildren 中生成一个无回路有向图(DirectedAcyclicGraph),然后使用深度优先遍历算法(DFS)将图遍历出来,再进行反序处理(Collections.reverse)生成的,对算法比较感兴趣的可以去源码中查看下DirectedAcyclicGraph的结构和DFS算法的实现,在此就不做说明了.
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 的处理也只是对于状态栏背景的处理,无足轻重.
@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();
if (child.isOpaque()) {
// If the child is opaque, there is no need to draw behind it so we'll inverse
// clip the canvas
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);
}
}
return super.drawChild(canvas, child, drawingTime);
}
drawChild 的处理倒是有点意思,这里获取了 子View 的 Behavior 的阴影颜色和阴影透明度,然后在绘制 子View 的位置之外绘制一层阴影.
CoordinatorLayout的触摸事件处理
接下来继续看触摸事件的处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//.................
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
//..............
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
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
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
}
//.................
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;
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();
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.
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;
}
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.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
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;
}
根据代码理一下 CoordinatorLayout 的触摸事件控制逻辑.
先看 onInterceptTouchEvent 的逻辑,主要表现在了 performIntercept 这个方法中,在 onInterceptTouchEvent 中,先对各 Behavior的onInterceptTouchEvent 方法分发down事件,直到有 子View 的 Behavior 的 onInterceptTouchEvent 返回 true 或者 LayoutParams 的 isBlockingInteractionBelow 返回 true,则停止继续分发.
那么何时 isBlockingInteractionBelow 返回 true 呢? 当 子View 的 Behavior 的 blocksInteractionBelow 返回 true 时 isBlockingInteractionBelow 为 true.当 Behavior 的 onInterceptTouchEvent 返回 true,会用一个 mBehaviorTouchView 的变量标记 Behavior 所附属的View.在对于非down事件的分发时,如果事件因为 Behavior 的 blocksInteractionBelow 强制拦截了,剩余之前接收到down事件的 Behavior 则会收到一个cancel事件.onInterceptTouchEvent 的处理几乎都是交给 Behavior 处理的,它自己没有做任何处理.这里应当注意的一个问题是,如果有 子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 两次,讲道理这应该也算是一个bug了,值得留意.如果没有 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 的合理性.
CoordinatorLayout的嵌套滑动支持
CoordinatorLayout 支持了嵌套滑动的 NestedScrollParent2,来查看下其中的接口实现.
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
mNestedScrollingTarget = target;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScrollAccepted(this, view, child, target,
nestedScrollAxes, type);
}
}
}
//.....................
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
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[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
//.......................
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
这里只贴了几个具有代表性的方法,除了 getNestedScrollAxes,其他的方法都是通过调用 View 的 Behavior 的同名方法实现的.
值得注意的是,在 onStartNestedScroll 方法中,由于 子View 不止一个,所以用lp.setNestedScrollAccepted(type, accepted);记录了 子View 是否接受嵌套滑动,然后在 onNestedScrollAccepted 方法中调用 lp.isNestedScrollAccepted(type) 判断 子View 是否接受嵌套滑动.
还有一点,在 onNestedPreScroll 中,consumed 值是取的各 子View 的 Behavior 消耗最大值.
CoordinatorLayout属性总结
通过对 CoordinatorLayout 的结构分析可以获得如下结论
-
如果 子View 存在 Behavior,CoordinatorLayout 对 子View 的大部分操作都会交给 Behavior 来处理,借助这个属性,可以通过设置 Behavior 来实现对 子View 操作的劫持.
-
CoordinatorLayout 在 子View 没有设置 Behavior 的情况下,几乎是就是一个 FrameLayout.
-
CoordinatorLayout 支持嵌套滑动,但是都是交给 子View 的 Behavior 来处理的.
CoordinatorLayout核心-Behavior
在了解了 CoordinatorLayout 的大致结构后,会发现各种操作都和 Behavior 息息相关,那么 Behavior 到底是什么东西呢?
Behavior的构成
先来看看 Behavior 的构成吧.
查阅其方法,会发现其中有着大部分和 NestedScrollingParent2 一样的方法,在对 CoordinatorLayout 的分析中,已然了解,CoordinatorLayout 的嵌套滑动事件都会传递给 子View 的 Behavior,这些方法属于回调,而且在基础的 Behavior 方法中都是空方法,在此不多做解释.
除开嵌套滑动部分,其主要有如下方法
public static abstract class Behavior<V extends View> {
/**
* 默认构造方法,用于注解的方式创建或者在代码中创建
*/
public Behavior() {
}
/**
* 用于xml解析layout_Behavior属性的构造方法,如果需要Behavior支持在xml中使用,则必须有此构造方法
*/
public Behavior(Context context, AttributeSet attrs) {
}
/**
* 此方法会在LayoutParams实例化后调用,或者在调用了LayoutParams.setBehavior(behavior)时调用.
*/
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
}
/**
* 当Behavior脱离LayoutParams时调用,例如调用了LayoutParams.setBehavior(null).
* View被从View Tree中移除时不会调用此方法.
*/
public void onDetachedFromLayoutParams() {
}
/**
* 接收CoordinatorLayout的触摸拦截事件,按从上到下的层级顺序分发拦截事件,
* 如果返回true,会在CoordinatorLayout中的onTouchEvent中调用这个View的Behavior的onTouchEvent方法.
*
* 这里的拦截应当慎重,一旦有Behavior返回true,则会导致CoordinatorLayout的所有子View触摸事件无效.
*/
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
/**
* 接收CoordinatorLayout的触摸事件,
* 事件分发按层从上到下分发,一旦有Behavior的onTouchEvent返回true,
* 则此Behavior所附属的View的下面所有的CoordinatorLayout子View的Behavior都收不到onTouchEvent回调.
*/
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
/**
* 获得当前Behavior附属View空间之外的阴影颜色
*/
@ColorInt
public int getScrimColor(CoordinatorLayout parent, V child) {
return Color.BLACK;
}
/**
* 获得当前Behavior附属View空间之外的阴影透明度
*/
@FloatRange(from = 0, to = 1)
public float getScrimOpacity(CoordinatorLayout parent, V child) {
return 0.f;
}
/**
* 是否阻止此Behavior所附属View下层的View的交互
*/
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
return getScrimOpacity(parent, child) > 0.f;
}
/**
* 用于判断是否为依赖的View,一般重写该方法来获取需要联动的View
*/
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
/**
* 当依赖的View发生改变时回调此方法,用于监听依赖View的状态
*/
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
/**
* 当依赖的View被移除时回调此方法,用于监听依赖View的状态
*/
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
/**
* 代替CoordinatorLayout的默认测量子View的方法,
* 返回true使用Behavior的测量方法来测量当前Behavior所附属View,
* 返回flase则使用CoordinatorLayout的默认方式
*/
public boolean onMeasureChild(CoordinatorLayout parent, V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
return false;
}
/**
* 代替CoordinatorLayout的默认布局子View的方法给该Behavior的附属View布局,
* 返回true则使用Behavior的布局方式来给Behavior所属View布局,
* 返回false则使用CoordinatorLayout的默认方式
*/
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}
}
Behavior的构造方法
需要注意的一点,一般的 Behavior 都需要有两个构造方法,一个用于在代码中创建,无参,通过 CoordinatorLayout.LayoutParams.setBehavior 设置到 View 的 LayoutParams 中或者使用注解的方式设置 View 默认的 Behavior.
注解的方式使用举例:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
//............................
}
如果使用注解的方式,必须有一个无参数的构造方法,因为实际上是反射构建的.其实例创建过程是在 CoordinatorLayout 中的 getResolvedLayoutParams 中调用
DefaultBehavior defaultBehavior = null;
defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)
defaultBehavior.value().getDeclaredConstructor().newInstance()
实现的.
不过现在这种方法已经废弃了,新的方式是View通过实现
public interface AttachedBehavior {
@NonNull Behavior getBehavior();
}
接口,然后 CoordinatorLayout 会在 getResolvedLayoutParams 判断 子View 是否实现这个接口,如果实现了,则通过 getBehavior 获取默认 Behavior.
还有一个构造方法用于在xml中使用,这种方式也是使用反射生成的 Behavior 实例,所以构造方法参数必须只能为 Context context, AttributeSet attrs.
在xml中使用举例:
layout文件
app:layout_behavior="@string/appbar_scrolling_view_behavior"
字符串资源
<string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>
有兴趣了解其反射解析过程的话可以自行查看 CoordinatorLayout 的 parseBehavior 方法:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name)
在此不做赘述.
Behavior的DependentView
Behavior 的 DependentView 这也是一个 Behavior 的重要概念,在此解释下.
这是 CoordinatorLayout 实现控件联动的关键之一,在 子View 的 Behavior 中根据 layoutDependsOn 方法获得一个满足条件的 DependentView,然后当 DependentView 发生改变时,会触发 Behavior 的 onDependentViewChanged 和 onDependentViewRemoved 回调.
在此分析下其实现原理
跟踪 layoutDependsOn 的调用,会找到这样一个方法
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
//........................
final int childCount = mDependencySortedChildren.size();
//..........................
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//.......................................
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();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//......................
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:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
//........................
}
}
}
//......................
}
前面说过 mDependencySortedChildren 是一个将 子View 根据依赖关系进行排序的List,这里先遍历一次这个List判断 子View 的位置大小是否变化,如果有变化则再从当前已经遍历到的列表节点开始再遍历一次剩余的节点,根据 Behavior 的 layoutDependsOn 方法判断这个 子View 的 DependentView 是否有在这个List中的.如果有的话,则再根据改变类型调用 Behavior 的 onDependentViewRemoved 和 onDependentViewChanged 方法.
继续追踪 onChildViewsChanged 方法的调用.发现很多是嵌套滑动的的回调方法中调用的onChildViewsChanged(EVENT_NESTED_SCROLL);都是类似这种
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
//...........................
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
这部分相对简单,不再重复写了.
更值得关注的是 EVENT_VIEW_REMOVED 和 EVENT_PRE_DRAW 类型,追踪到发现这两个都是用 Listener 监听的回调.
EVENT_VIEW_REMOVED类型部分
private class HierarchyChangeListener implements OnHierarchyChangeListener {
//..................
@Override
public void onChildViewRemoved(View parent, View child) {
onChildViewsChanged(EVENT_VIEW_REMOVED);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
然后在构造方法中注册
super.setOnHierarchyChangeListener(new HierarchyChangeListener());
这里简介下 OnHierarchyChangeListener,在 ViewGroup 中注册此Listener,当有 子View 添加或者删除时会回调此Listener.
EVENT_PRE_DRAW类型部分
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
在onAttachedToWindow方法中添加了OnPreDrawListener
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
然后在 onDetachedFromWindow 会删除这个Listener.
注册 OnPreDrawListener 后,这个Listener会在每次刷新确定各View大小位置后,绘制之前调用.
还有一个部分的 OnPreDrawListener 调用过程比较繁琐,文字简述下吧.
这部分是 onMeasure–>ensurePreDrawListener–>判断所有子View的Behavior是否存在满足其依赖关系的View,只要存在,则添加OnPreDrawListener,否则删除OnPreDrawListener.
再追踪 Behavior的onDependentViewChanged 方法,发现 CoordinatorLayout 中有一个dispatchDependentViewsChanged方法,也存在调用,不过这是一个开放给用户的方法,源码中并没有发现调用之处;
还有一个调用的地方是上面提到的onChildViewsChanged方法–>offsetChildToAnchor–>Behavior.onDependentViewChanged,这里的判断是判断改变的View是否为某个子View的锚点View,如果是,则调用这个子View的Behavior的onDependentViewChanged方法.
总结一下
-
当一个View的Behavior依赖的View位置或者大小发生改变时,会回调此View的Behavior的onDependentViewChanged方法
-
当一个View的Behavior依赖的View从View Tree中删除时,会回调此View的Behavior的onDependentViewRemoved方法
-
当一个View的LayoutParams设置了锚点View(mAnchorView),如果这个锚点View的位置发现变化,则会回调此View的Behavior的onDependentViewChanged方法
那么到此,Behavior 大部分重要的结构原理都分析完毕了.
因为微信篇幅限制,Behavior的使用以及实战例子 请点击最后 阅读原文 查看