此篇文章需要用到之前的知识点,即我之前写的这两篇博客
事件分发机制原理分析
NestedScrolling机制原理分析
一.CoordinatorLayout基本介绍
1.使用场景
一般作为应用的顶层布局,同时也作为一个管理容器,管理与子view
或者 子view
之间的交互。那么都具体管理啥呢?可以分成四个部分
2.作用
- ①处理子控件之间依赖下的交互
- ②处理子控件之间的嵌套滑动
- ③处理子控件的测量与布局
- ④处理子控件的事件拦截与响应
以上四个功能,都建立于CoordainatorLayout
中提供的一个叫做Behavior
的**“ 插件”**之上。Behavior内部也提供了相应方法来对应这四个不同的功能。那么都有哪些方法呢?
3.Behavior中的常用方法
4.为什么把这些方法都放到Behavior
中呢?
答案:解耦。当用的时候就集成进去,不用的时候就remove
。即可插拔。
- 在这里可以把
CoordinatorLayout
比喻成Android Studio,把子View
比喻成我们的项目。我们知道,AS
可以使用Plugins
为项目引入各种插件,从而实现不同的功能,同样的道理,CoordinatorLayout
可以使用Behavior
为子View
引入各种行为。Behavior
也是**“可插拔”**
二.CoordinatorLayout四个功能的原理
1.CoordinatorLayout下依赖交互功能原理(观察者模式)
当CoordainatorLayout
中子控件depandency
的位置、大小等发生改变的时候,那么在CoordainatorLayout
内部会通知所有依赖depandency
的控件,并调用对应声明的Behavior
,告知其依赖的depandency
发生改变。
- 那么如何判断依赖是哪个
View
呢?layoutDependsOn
方法 -
接受到通知后如何处理呢?
onDependentViewChanged
/onDependentViewRemoved
方法
原理图dependency
也是一个child
,和child1
、child2
在布局上可以是并列的。后面我们把dependency
统一称为DepandedView
2.CoordinatorLayout内部嵌套滑动原理
CoordinatorLayout
实现了NestedScrollingParent2
接口。所以当事件(scroll
或fling
)产生后,内部实现了NestedScrollingChild
接口的子控件会将事件传递给CoordinatorLayout
,CoordinatorLayout
又会将事件传递给所有的Behavior
。然后在Behavior
中实现子控件的嵌套滑动。
具体流程图
相对于NestedScrolling
机制(参与角色只有子控件和父控件),CoordainatorLayout
中的交互角色玩出了新高度,在CoordainatorLayout
下的子控件可以与多个兄弟控件进行交互。即从1:1
变成了1:N
3.CoordinatorLayout子控件的测量与布局
在特殊的情况下,如子控件需要处理宽高和布局的时候,那么交由Behavior
内部的onMeasureChild
与onLayoutChild
方法来进行处理
4.CoordinatorLayout子控件的事件拦截与响应
对于事件的拦截与处理,如果子控件需要拦截并消耗事件,那么交由给Behavior
内部的onInterceptTouchEvent
与onTouchEvent
方法进行处理
三.源码分析CoordinatorLayout子控件依赖交互功能原理
由于View
的生命周期的开始是在onAttachedToWindow
方法中,所以我们进入此方法寻找
1.我们在CoordinatorLayout
类中找到onAttachedToWindow
方法
发现它调用getViewTreeObserver
,获得ViewTreeObserver
,然后调用了addOnPreDrawListener
- 关于
ViewTreeObserver
:ViewTreeObserver
注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver
都会收到通知,ViewTreeObserver
不能被实例化,可以调用View.getViewTreeObserver
()来获得 - 关于
dispatchOnPreDraw
:通知观察者绘制即将开始,如果其中的某个观察者返回true
,那么绘制将会取消,并且重新安排绘制,如果想在View Layout
或Viewhierarchy
还未依附到Window
时,或者在View
处于GONE
状态时强制绘制,可以手动调用这个方法
2.接下来我们看一下它添加的监听者是个啥,追踪addOnPreDrawListener
,找到OnPreDrawListener
类
当View
发生变化的时候会调用onChildViewsChanged
方法。
3.我们追踪onChildViewsChanged
它有一个类型,即type
也就是DispatchChangeEvent
注解。我们点进去看一下
一个代表绘制之前,一个代表嵌套滑动,一个代表View
移除。意思是说这三种类型的事件发生的时候会调用onChildViewsChanged
方法。
我们深究一下这个方法
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
。。。
//--------------------------------------
//--------------------------------------
//得到子View的数目
//--------------------------------------
//--------------------------------------
final int childCount = mDependencySortedChildren.size();
。。。
//--------------------------------------
//--------------------------------------
//取出每一个子View,即child
//--------------------------------------
//--------------------------------------
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
。。。
//--------------------------------------
//--------------------------------------
//然后再嵌套一个for循环,再依次重新取出子View
//--------------------------------------
//--------------------------------------
for (int j = i + 1; j < childCount; j++) {
//--------------------------------------
//--------------------------------------
//首先得到子View即checkChild ,然后得到子View的LayoutParams,再根据它得到Behavior
//--------------------------------------
//--------------------------------------
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//--------------------------------------
//--------------------------------------
//判断一下此时得到的checkChild是不是要依赖child
//换句话说,看一下这个child是不是被依赖的
//--------------------------------------
//--------------------------------------
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
//--------------------------------------
//--------------------------------------
//如果type是EVENT_PRE_DRAW(这个type上面说过),则调用resetChangedAfterNestedScroll
//--------------------------------------
//--------------------------------------
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
//--------------------------------------
//--------------------------------------
//如果type是EVENT_VIEW_REMOVED,则调用onDependentViewRemoved
//--------------------------------------
//--------------------------------------
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
//--------------------------------------
//--------------------------------------
//如果type是其他类型,则调用onDependentViewChanged
//--------------------------------------
//--------------------------------------
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
//--------------------------------------
//--------------------------------------
//如果type是EVENT_NESTED_SCROLL,则调用setChangedAfterNestedScroll
//--------------------------------------
//--------------------------------------
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
...
}
通过上面的源码,我们了解了在需要依赖其他控件的控件中设置一个behavior
,那么被依赖的控件也就是DepandedView
发生变化的时候就能通知对方的原因,主要是onChildViewsChanged
方法。
-
那么为什么可以调用
remove
相应的方法(即onDependentViewRemoved
)呢?
①我们在CoordinatorLayout
的构造方法中发现它调用了setOnHierarchyChangeListener
我们看它传入的参数是
②追踪进入,发现HierarchyChangeListener
类实现了OnHierarchyChangeListener
接口。
③我们再追踪OnHierarchyChangeListener
发现它是一个接口,定义了两个方法。
当EVENT_VIEW_REMOVED
类型事件发生的时候,就会调用onChildViewsChanged
方法,然后参数传入EVENT_VIEW_REMOVED
,然后就会回调Behavior
的onDependentViewRemoved
,也就是上面源码分析的那部分
四.源码总结
总结一波:
当CoordinatorLayout
的某一DepandedView
发生变化的时候,必然会导致重绘,然后就会调用onChildViewsChanged
方法,如图
在这个方法里面根据type
,再去调用Behavior
的相应方法。
- 这也是把
OnPreDrawListener
作为监听者的原因。因为有些时候onMeasure
或者onLayout
可能不会调用,但是关于draw
的方法是肯定会调用的。这样把onChildViewsChanged
方法放在监听者的onPreDraw
方法中,就可以在DepandedView
发生变化的时候,及时调用onChildViewsChanged
,通知其他依赖DepandedView
的子View
们发生相应的变化。当然这些变化的方法是由Behavior
调用的
一个问题:mDependencySortedChildren
是啥
ok,让我们思考一个问题。不知道大家发现没有,在onChildViewsChanged
源码中,CoordinatorLayout
得到子View
的时候,不是调用的我们之前学过的getChildAt
方法,而是调用的mDependencySortedChildren
的相应方法。
那么mDependencySortedChildren
是个啥?我们点进去看看
我们看到了它下面有一个DirectedAcyclicGraph<View>
类型的数据。
- 我先解释下原因吧。在这里,因为
CoordinatorLayout
管理的不仅仅是子View
,还有子View
之间的关系(依赖关系)。也就是说不能用一个简单的集合,而是用图这个数据结构,而DirectedAcyclicGraph
就是一个图(具体来说是有向无环图,用邻接表来实现的),表示1对多的关系。当我们找到一个View
,以及其与其他View
之间的依赖关系时,就把这个View
和相应依赖关系(图中叫做边)存入这个DirectedAcyclicGraph
中,然后再将其某种形式存入mDependencySortedChildren
中。这就是mDependencySortedChildren
第二个问题,mDependencySortedChildren
如何赋值的
mDependencySortedChildren
的赋值和DirectedAcyclicGraph
的赋值有很大关系,我们先看DirectedAcyclicGraph
在onMeasure
方法中
我们发现了prepareChildren
,追踪进入
这里面就是具体赋值的流程,都写在注释中了
private void prepareChildren() {
//--------------------------------------
//--------------------------------------
//首先对两者进行清空操作
//--------------------------------------
//--------------------------------------
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
//--------------------------------------
//--------------------------------------
//然后得到子View,即view
//--------------------------------------
//--------------------------------------
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
//--------------------------------------
//--------------------------------------
//把这个子view加入图中,作为图的节点
//--------------------------------------
//------------------------------------
mChildDag.addNode(view);
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
//--------------------------------------
//--------------------------------------
//然后再嵌套for循环,得到子View,即other
//--------------------------------------
//------------------------------------
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);
}
//--------------------------------------
//--------------------------------------
//如果view依赖other,或者说other是DepandedView时
//就画一条从other到view的边,并加入图中,表明view和other存在依赖关系
//--------------------------------------
//------------------------------------
mChildDag.addEdge(other, view);
}
}
}
//--------------------------------------
//--------------------------------------
//调用addAll方法,将图的getSortedList加入到mDependencySortedChildren中
//--------------------------------------
//------------------------------------
mDependencySortedChildren.addAll(mChildDag.getSortedList());
//--------------------------------------
//--------------------------------------
//这里做了一个反转,无需理解,知道就行
//--------------------------------------
//------------------------------------
Collections.reverse(mDependencySortedChildren);
}
到这我们知道了CoordinatorLayout
是通过图里面的边来区分谁依赖于谁的了