Android CoordinatorLayout和Behavior解析
目录
在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
中,传给子ViewBehavior
的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
的使用
说了那么多原理,是时候说怎么用了.
Behavior
的用法应该可以分为三部分.
第一部分是用于控制子View的布局,测量,设置阴影还有拦截CoordinatorLayout
的触摸事件,这部分都算是拦截和子View自身相关属性的部分,通过重写Behavior
的onInterceptTouchEvent
,onTouchEvent
,getScrimColor
,getScrimOpacity
,onMeasureChild
,onLayoutChild
等方法实现,这部分较为简单,在此不多做说明及演示了.
第二部分第三部分都是控件联动部分,在此先暂定第二部分为依赖联动部分,第三部分为嵌套滑动联动部分.
依赖联动
在这里重点说明下第二部分依赖联动部分的使用.
Behavior
依赖联动部分有3个重要方法layoutDependsOn
,onDependentViewChanged
,onDependentViewRemoved
.一般重写这三个方法就可以实现简单的控件联动.
使用举例
创建个例子吧,这样比较有说服力.
在此就做一个相互关联运动相反的两个方块作为例子吧.
先写xml的布局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/child"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@android:color/holo_green_light"
app:layout_behavior=".MyTextViewBehavior" />
<TextView
android:id="@+id/dependency"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="right|bottom"
android:background="@android:color/holo_blue_light" />
</android.support.design.widget.CoordinatorLayout>
然后在主Activity
中的onCreate
中设置被依赖的View的触摸事件
TextView textView = (TextView) findViewById(R.id.dependency);
textView.setOnTouchListener(this);
主Activity
实现OnTouchListener
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.dependency:
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX() + v.getLeft();
int y = (int) event.getY() + v.getTop();
int left = x - v.getWidth() / 2;
int top = y - v.getHeight() / 2;
setViewLocationInCoordinatorLayout(left, top, v);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
default:
break;
}
break;
default:
break;
}
return true;
}
这个触摸事件的处理很简单,就是让控件中心跟随手指运动.
其中setViewLocationInCoordinatorLayout
是一个改变CoordinatorLayout
子View在CoordinatorLayout
位置的一个静态方法,其实现如下
public static void setViewLocationInCoordinatorLayout(int leftMargin, int topMargin, View child) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (lp == null) {
lp = new CoordinatorLayout.LayoutParams(child.getWidth(), child.getHeight());
}
lp.setMargins(leftMargin, topMargin, 0, 0);
lp.gravity = Gravity.NO_GRAVITY;
child.setLayoutParams(lp);
}
然后实现id为child
的Behavior
– MyTextViewBehavior
主要重写以下两个方法即可
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
if (child.getId() == R.id.child) {
if (dependency.getId() == R.id.dependency) {
return true;
}
}
return super.layoutDependsOn(parent, child, dependency);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
int leftMargin = parent.getWidth() - dependency.getRight();
int topMargin = parent.getHeight() - dependency.getBottom();
MainActivity.setViewLocationInCoordinatorLayout(leftMargin, topMargin, child);
return true;
}
这里只做了两个正方形的,对称,位置相反的方块,然后给id为dependency
的TextView
设置了触摸事件,让这个TextView
跟随手指移动,然后给id为child
的TextView
设置了一个MyTextViewBehavior
,这个Behavior
简单的根据id来判断是否为依赖的View,简单粗暴,然后当id为dependency
的这个View位置发现改变时则会触发MyTextViewBehavior
的onDependentViewChanged
,然后计算id为dependency
的这个View的偏移距离,然后根据这个距离改变这个Behavior
所附属的View的位置.
放一个效果图
嵌套滑动联动
这个主要是在有子View支持NestedScrollingChild
接口时,支持NestedScrollingChild
接口的这个子View分发嵌套滑动事件,然后回调CoordinatorLayout
的NestedScrollingParent
方法,然后在CoordinatorLayout
中的这些NestedScrollingParent
方法中调用子View的Behavior
的与NestedScrollingParent
同名的方法以实现联动.
具体实例及分析将结合一个非常有研究价值的问题放在下篇WebView和AppBarLayout嵌套滑动联动无效分析及解决办法中讲解.