前言
MD设计风格引入了CoordinatorLayout布局,它能够协调子控件之间的操作,使得子控件相互能够做一些复杂的交互操作,这些交互主要通过Behavior对象来实现,这里就来查看一下Behavior和CoordinatorLayout布局实现源代码。
代码分析
首先查看Behavior的源码,它是CoordinatorLayout的一个内部抽象类,主要的接口可以大致上分成三组:依赖处理、嵌套滑动和事件派发相关,要注意的是依赖处理和嵌套滑动这两种机制是相互独立的,它们之间没有必然的联系。
接口分组 | 接口 |
---|---|
依赖处理 | public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) |
嵌套滑动 | onStartNestedScroll onStartNestedScroll …. |
事件派发 | public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) |
需要记住这三组回调接口,在后面的代码分析中会在不同的处理方法中调用这些回调。写过MD的AppBarLayout布局和RecyclerView视差实现都记得在定义界面的XML中会写上app:layout_behavior这样的属性,这个属性其实是在CoordinatorLayout为子控件生成LayoutParams时解析的,查看这个函数的代码:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends MarginLayoutParams {
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
// 如果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方法其实就是利用反射获取到Behavior的构造函数,通过构造函数生成Behavior对象,这样CoordinatorLayout.LayoutParams里的Behavior对象就成功的被初始化了。
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
// 解析Behavior字符串为类名
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) {
// 加载Behavior类并且获取它的构造函数放到缓存中,下次不必在使用反射初始化
final Class<Behavior> clazz = (Class<Behavior>) context.getClassLoader()
.loadClass(fullName);
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
// 生成Behavior对象
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
除了在xml中指定还可以通过CoordinatorLayout.LayoutParams的setBehavior方法用代码设置某个子控件的Behavior对象,还可以通过注解的方式注入,这里就不再赘述了。接着来查看成功初始化布局参数里的Behavior之后它又是如何起作用的,首先看子控件的依赖实现源码。
我们知道View子视图之间的相互依赖则是由用户自己定义,为了能够高效的查找到View之间的相互依赖关系,在onMeasure方法中首先调用了prepareChildren方法,这个方法会遍历所有的子控件(不包含子控件内部的控件),通过调用指定View的CoordinatorLayout.LayoutParams的dependOn方法判断它们之间是否存在依赖关系,如果有就将被依赖的控件加入到有向无环图中,之后再把有向无环图排序后添加到mDependencySortedChildren属性中,这样从一个控件开始之后的一段控件都是它依赖的子控件。
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);
lp.findAnchorView(this, view);
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);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// Finally add the sorted graph list to our list
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
Collections.reverse(mDependencySortedChildren);
}
// LayoutParams
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency == mAnchorDirectChild
|| shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}
LayoutParams的dependsOn方法就会调用mBehavior.layoutDependsOn方法判断当前遍历的子控件是否是child依赖View控件。接着查看如何监视某个子控件被添加或移除,ViewGroup提供了接口OnHierarchyChangeListener每当有子控件添加删除就会做回调操作。
public interface OnHierarchyChangeListener {
void onChildViewAdded(View parent, View child);
void onChildViewRemoved(View parent, View child);
}
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mOnHierarchyChangeListener = listener;
}
在CoordinatorLayout中定义了自己的监听器,并且会将子控件移除操作发出通知,这里对应调onChildViewsChanged(EVENT_VIEW_REMOVED)方法,在该方法内部会遍历所有的依赖它的子控件并且调用onDependentViewRemoved方法。
private class HierarchyChangeListener implements OnHierarchyChangeListener {
HierarchyChangeListener() {
}
@Override
public void onChildViewAdded(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
// 提示子控件发生了删除事件
onChildViewsChanged(EVENT_VIEW_REMOVED);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 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:
// 如果是移除事件就调用onDependentViewRemoved回调方法
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 否则调用onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
}
除了前面的监控控件树是否发生改变还有dispatchDependentViewsChanged方法,它会在CoordinatorLayout内部的子控件发生变化是自动调用,这时会将依赖该控件的所有View都查找处理并且调用onDependentViewChanged方法。
public void dispatchDependentViewsChanged(View view) {
final List<View> dependents = mChildDag.getIncomingEdges(view);
if (dependents != null && !dependents.isEmpty()) {
for (int i = 0; i < dependents.size(); i++) {
final View child = dependents.get(i);
LayoutParams lp = (LayoutParams)
child.getLayoutParams();
Behavior b = lp.getBehavior();
if (b != null) {
b.onDependentViewChanged(this, child, view);
}
}
}
}
上面的代码逻辑就是CoordinatorLayout和Behavior共同实现的控件依赖实现,接着再继续查看通过CoordinatorLayout实现内部嵌套滑动交互操作。在之前学习嵌套滑动原理时我们提到内部的子View会先查找需要和它做交互的外部滑动父布局,很显然嵌套滑动的双方必须是直系祖先对象,如果希望和其他的旁系祖先或兄弟交互无法实现。CoordinatorLayout布局在这种情况下会作为中间对象,嵌套的子控件和CoordinatorLayout交互,CoordinatorLayout再和子控件实际需要交互的兄弟控件做交互,实现CoordinatorLayout内部的控件的嵌套滑动互动操作。
嵌套滑动需要回调的接口有很多,详细的交互过程在嵌套滑动机制源码阅读已经讨论过,这里不再赘述。现在值查看最简单的onStartNestedScroll方法,这个方法会返回是否需要拦截子控件的滑动。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@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) {
// 检查View是否需要嵌套滑动
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
在onStartNestedScroll方法中会遍历内部的所有子控件通过Behavior.onStartNestedScroll来判断当前遍历的View是否需要嵌套滑动,这个Behavior内部可以覆盖onStartNestedScroll方法的默认实现,如果需要定义Behavior的控件自己先滑动可以返回true代表包含Behavior的布局需要自己先滑动,其他的嵌套布局代码类似这里不再赘述。
最后的onInterceptTouchEvent和onTouchEvent专门负责事件派发机制的回调,这两个接口在事件派发机制代码阅读也详细讲述过,CoordinatorLayout布局内部会定义mBehaviorTouchView字段,它会在某些特殊情况拦截MotionEvent事件,确认拦截时mBehaviorTouchView不为空那么就会执行它所包含的Behavior回调接口。
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// 如果决定拦截事件,那么就会调用 b.onTouchEvent回调方法
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
....
return handled;
}
总结
CoordinatorLayout布局通过使用Behavior实现内部子控件的协调交互,Behavior主要包含控件依赖、嵌套滑动和事件派发三种功能的接口。控件依赖会在CoordinatorLayout的onMeasure执行时生成有向无环图确定子控件的依赖关系,通过监听控件树的改变将被依赖View改变或移除事件派发到依赖控件;默认的嵌套滑动只支持存在直系父子祖孙关系的两个控件之间,CoordinatorLayout将自己作为中间对象,需要嵌套交互的子控件通过CoordinatorLayout中转嵌套回调;事件派发功能会在CoordinatorLayout的事件回调方法中调用Behavior接口实现拦截默认事件派发功能。