您的位置:首页 > 移动开发 > Android开发

CoordinatorLayout源码解析,探索Behavior机制的奥秘

2016-07-25 21:20 519 查看

1.文章内容概述

本文主要是针对Behavior的运作机制,通过对CoordinatorLayout的源码中对此Behavior的执行过程进行分析,得出其运行原理。本文主要是针对behavior的layout child的方式,touch事件的处理,以及内嵌滑动事件的处理进行重要分析。

2.阅读本文前的准备工作

关于内嵌滑动,这是在android5.0之后google提出的,并且在support包也包含,主要是支持内嵌滑动的控件支持内嵌滑动。也就是实现了NestedScrollingParent,NestedScrollingChild这两个接口的view支持。

了解内嵌滑动的执行原理,可以查看这篇文章:http://blog.csdn.net/chen930724/article/details/50307193 ,但是没有对 onNestedPreScroll()
和onNestedScroll()进行更加深刻的分析,但是本人通过对系统中实现了NestedScrollingChild接口的两个view(NestedSrcoller和RecyclerView)进行源码解析,得出如下结论:

/**
* 此方法是在 子nestedview的 onTouchEvent()方法的 Move事件 中被调用的( 通过查看NestedScrollView 和 RecyclerView得知)
* 只要执行到 子 nestedview的 onTouchEvent()就会执行到此方法
* 注意:如果此方法将dx 和 dy 全都消耗完了,那么 子nested view 就不能处理移动事件了,因为 当前父nested view(这里是 behavior,他是在 CoordinatorLayout的回调函数中调用的)
* 已经将全部的touch move事件全部消耗完了
* @param coordinatorLayout
* @param child 当前behavior所在的view
* @param target 当前nestedscroll 的child的view (正在内嵌滑动的view)
* @param dx
* @param dy
* @param consumed
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}

/**
* 此方法也是在 子nestedView 的 onTouchEvent()方法中的 Move事件 中被调用,但是它是在上面的方法的后面调用,并且调用是有条件的.(得知同上)
* 条件: 当前move移动的距离 减去 consumed消耗的距离,如果小于 touchSlop ,那么就不会执行到这个方法,否则才会执行到这个方法
* 注意: 这里 dxConsumed和 dyConsumed 和上面那个方法的consumed消耗的距离是没有任何关系的.这个方法的x,y消耗的值是上面的pre方法执行完之后,当前child view移动的距离,
* 而dxUnconsumed 和 dyUnconsumed 是 move事件移动的距离减去 上面pre方法消耗的距离之后的 数值,减去本方法的dxConsumed和dyConsumed的值.
* @param coordinatorLayout
* @param child
* @param target
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}

3.CoordinatorLayout源码解析

首先,Behavior的创建,因为Behavior是存在CoordinatorLayout中的LayoutParams的mBehavior的变量,并且实例化是在LayoutParams的构造函数中进行初始化的。代码如下所示:

LayoutParams(Context context, AttributeSet attrs) {
.......
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}

a.recycle();
}主要是看parseBehavior()是怎么实现
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);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}可以看出,内部是通过反射的方式创建的Behavior,并且调用的两个参数的构造函数,所以如果想要试用behavior就必须实现它的构造函数,不然就会报异常。

下面从CoordiantorlLayout 的onlayout()方法开始进行分析,因为此方法开始布局子view,代码如下:

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);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();

if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}通过上面代码,我们可以看出CoordinatorLayout 在遍历子view的时候会判断每个子view是否设置了behavior,如果设置了,就会执行它的behavior的onLayoutChild(),如果此方法返回true,就不会试用CoordinatorLayout的onLayoutChild()的算法来布局当前子view;如果返回false,就会使用CoodinatorLayout的方式。
得出结论:

/**
* 此方法是用来自定义当前View的 布局方式(也就是决定当前View的位置)
* @param parent
* @param child
* @param layoutDirection
* @return
*/
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
return super.onLayoutChild(parent, child, layoutDirection);
}

下面对Behavior的touch事件处理的原理进行解析,下面看CoordiantorLayout 的onInterceptTouchEvent()源码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;

final int action = MotionEventCompat.getActionMasked(ev);

// 在interceptTouch事件处理到down事件的时候,会重置当前选中的mBehaviorTouchView为null,然后遍历所有的子view,将他们的behavior的touch重置状态
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
//这是核心方法 ,touch事件的处理都在这里(onTouchEvent()也是回调的这个方法)
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

if (cancelEvent != null) {
cancelEvent.recycle();
}
//up和cancel时,也就是touch事件结束的时候,再重置behavior的状态
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}

return intercepted;//返回值也是由 performIntercept()方法实现.
}对核心代码performIntercept()方法的分析:
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;

MotionEvent cancelEvent = null;

final int action = MotionEventCompat.getActionMasked(ev);

final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);//这个方法很重要,如果没有对child的绘制优先级作修改,这里获取到child的顺序是倒序

// 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) {//如果是down事件,就不会回调到后面view的behavior的onInterceptTouchEvent()方法
// 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);//只要不是down事件,都会将所有子view的behavior的onInterceptTouchEvent()
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}

if (!intercepted && b != null) {//如果一个behavior 的onInterceptTouchEvent()方法返回true了,那么它后面的view的behaviro只有通过上面的代码执行了
switch (type) {
case TYPE_ON_INTERCEPT://interceptTouch事件中的回调处理
intercepted = b.onInterceptTouchEvent(this, child, ev);//调用的behavior的回调方法
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);//调用behavior的回调方法
break;
}
if (intercepted) {
mBehaviorTouchView = child;//第一个子view的 behavior的 onInterceptTouchEvent()方法返回true,会将当前锁定处理behavior的touch事件的view
//这个view就是在onTouchEvent()回调behavior的子view(具体查看onTouchEvent()代码)
}
}

//下面代码是决定是否跳出这个循环,也就不会执行这个view后面的view的behavior的onInterceptTouchEvent()和onTouchEvent()方法
//分析:因为onInterceptTouchEvent()中会先重置所有子view的behavior的touch状态,也就是lp.didBlockInteraction()会置为false,所以onInterceptTouchEvent()的时候,
//会将所有子view的behavior的onInterceptTouchEvent()执行到;但是在执行到onTouchEvent()的时候,已经在onInterceptTouchEvent()中将lp.didBlockInteraction();置为true
// 所以,执行到第一个子view的的onTouchEvent()就会break截断掉.(而且第一个子view的)
// 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);//这个会返回true
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {//这个执行到,只有在wasBlocking为false的时候会执行
// 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的onTouchEvent()源码。

public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;

final int action = MotionEventCompat.getActionMasked(ev);
//由此可知,如果一个子view的behavior的onInterceptTouchEvent()返回true,就会用mBehaviorTouchView记录这个view;所以这里就会直接执行这个view的behavior的onTouchEvent()回调函数
//如果所有的子view的behavior的onInterceptTouchEvent()都没有返回true,就会执行performIntercept()方法,而且这个方法会将第一个含有behavior的子 view的执行完,下面的子view的behavior
//都不会被截断掉(具体在performIntercept()方法里面分析了)
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);
}
}
.......

if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();//重置behavior的状态
}

return handled;
} 最后,对behavior的touch分发事件的处理,总结如下:
/**
* 方法详解说明:CoordinatorLayout内部调用原理:
* 1.在CoordinatorLayout的 onInterceptTouchEvent()方法中被调用,在其super方法之前被调用
* 2.调用顺序: 按照CoordiantorLayout中child的 添加的倒叙进行调用(除了修改CoordinatorLayout的 getChildDrawingOrder()更改了绘制顺序,会改变此顺序,具体看源码)
* 3.运行原理:如果此方法在Down事件返回true, 那么它后面的 view的 behavior都执行不到此方法;并且执行onTouchEvent()事件的时候只会执行此behavior的onTouchEvent()方法
* 如果不是Down事件返回true,那么它后面的view的behavior的此方法都会执行到,但是还是执行执行此behavior的onTouchEvent()方法
* 如果所有的view的behavior都没有返回true,那么在执行CoordinatorLayout的onTouchEvent()会回调所有child的behavior的onTouchEvent()方法
* 4.CoordinatorLayout的onInterceptTouchEvent()返回值:默认返回false, 返回值由 child的 behavior的此方法决定
* @param parent
* @param child
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) {
return super.onInterceptTouchEvent(parent, child,event);
}

/**
* 方法详解说明:
* 1.在CoordinatorLayout的 onTouchEvent()方法中被调用,在其super方法之前被调用
* 2.调用顺序:同onInterceptTouchEvent()方法的调用顺序
* 3.在上面onInterceptTouchEvent()提到的所有的behavior的onInteceptTouchEvent()方法都返回false,那么会遍历所有的child执行此方法,但是只有一个behavior的此方法
* 返回true,那么它后面的所有的view的behavior的onTouchEvent()都不会执行
* 4.CoordinatorLayout的onTouchEvent()返回值:如果child的behavior返回true,那么就返回true;如果child的behavior返回false,那么就有super.onTouchEvent()决定
* @param parent
* @param child
* @param event
* @return
*/
@Override
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) {
return super.onTouchEvent(parent, child, event);
}

Behavior的内嵌滑动事件的处理,其实CoordinatorLayout的内嵌滑动事件是被它子Nested view处理的(而两个核心方法dispatchNestedPreScroll()和 dispatchNestedScroll()是在子Nested view的onTouchEvent()中处理的;;核心的分析参考最上面我关于内嵌滑动事件的分析)。而Behavior的内嵌滑动事件的处理是在CoordinatorLayout中nested
scroll回调事件调用的,不会有截断的现象,具体代码就不分析,因为只要弄懂上面的关于内嵌滑动事件的处理就明白了。
但是有一个地方需要注意一下,就是onNestedPreScroll()方法的处理,下面看源码:

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;

final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}

final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);//mTempIntPair获取到每个子view的消耗的距离
//每次更新消耗的距离,但是获取的是最大的消耗距离(注意:并不是所有behavior消耗的距离的和,而是所有中的最大值)
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;
}
}
//最后将最大的消耗距离 作为CoordinatorLayout的 消耗的距离的值
consumed[0] = xConsumed;
consumed[1] = yConsumed;

if (accepted) {
dispatchOnDependentViewChanged(true);
}
}

4.总结

所有的分析就到此结束了,然后就是使用了,首先可以看看系统的实现:BottomSheetBehavior的源码实现(这个也分析过,如果需要,我可以写出来)。当然关于behavior的使用,网上有很多好的效果的源码可以参考。

Behavior的意义:其实没有CoordiantorLayout和Behavior,除了内嵌滑动意外,我们都可以通过自定义控件的方式实现的。而出现这个意义就在于,google帮我们做了很好的封装,在最外层的父布局。Behavior 就是在CoordinatorLayout的回调事件中处理的,这样就把变化的地方封装起来了,公开了Behavior来给我们扩展,耦合度降低了很多。并且扩展性很强。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息