##说在前面
CoordinatorLayout(下面简称CL)是Material Design中的明星控件了,学习它的源码不仅可以更好掌握这个控件的使用,而且可以更好地理解其他诸如AppbarLayout、FloatingActionButton等控件,还可以学习到一些有趣的思想。
可是当我像看其他ViewGroup(下面简称VG)的源码一样去看CL的源码的时候,我却发现了一个CL和其他VG最大的不同:几乎所有地方都有Behavior的参与,而CL本身并没有做什么事情。在大致了解了Behavior的作用之后,我决定从Behavior入手,聊一聊CL。
##Behavior是什么
Behavior(下面简称Bh)是什么?它是CL中的一个静态内部类。挑一些重要的接口来看一看,注释在下面。从整体上来看,对CL来说,每一个子View的Bh像是一个代理,CL的几乎所有工作都会先去询问子View的Bh如何决策。先记住这一点,在下面我们会逐步详细说明。
public static abstract class Behavior {
//事件分发和拦截相关
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {}
//View之间互动相关
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) {
}
//测量和布局相关
public boolean onMeasureChild(CoordinatorLayout parent, V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) { }
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { }
//嵌套滑动相关
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) { }
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY, boolean consumed) {}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {}
...
}
##Behavior的绑定
通过为CL的直接子View绑定一个Bh,你可以拦截CL的一切触摸事件,测量,布局和嵌套滑动等。Bh本身并不能发挥什么功能,它要被绑定到相应的子View上才能在合适的时机被调用。绑定Bh有三种方式:
在xml中指定为属性
这个属性中的Bh我们是自定义,或者使用系统的资源,假设我们是自定义的。
public class MyBehavior extends CoordinatorLayout.Behavior {
public MyBehavior() {
}
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
我们知道LayoutInflater在inflate整棵View树的时候,某个View的LayoutParams是通过root.generateLayoutParams(attrs)来获取的(也就是调用其父View的generateLayoutParams)。在这个时候它的LayoutParams就被初始化了,那么CL的直接子View就会初始化一个CoordinatorLayout.LayoutParams,这个Params是CL独有的,最大的特色就是包含了一个Bh,并在其初始化的时候会初始化这个Bh。
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
//这里通过反射调用对应Bh的2参构造器初始化一个Bh。
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
...
}
通过xml指定的Bh是在setContentView()中被初始化且被CL的直接子View持有的(存在LayoutParams中)。
程序中动态绑定
//可以使用无参构造器
MyBehavior myBehavior = new MyBehavior();
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) myView.getLayoutParams();
params.setBehavior(myBehavior);
为你的View添加注解
@CoordinatorLayout.DefaultBehavior(MyBehavior.class)
public class MyView extends FrameLayout {
}
这样的View在xml文件中app:layout_behavior=”.MyBehavior”为空,只有在运行时去赋值,而CL选择在onMeasure中。
//CoordinatorLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
....
}
private void prepareChildren() {
//检查每一个子View
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
//“解决LayoutParams中的mBehavior”
final LayoutParams lp = getResolvedLayoutParams(view);
...
}
}
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
Class> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null &&
//获取注解中的值
(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
//调用无参构造器创建一个Bh
result.setBehavior(defaultBehavior.value().newInstance());
}
}
}
return result;
}
通过对View类进行注解为View赋值Bh发生在CL的onMeasure中。
##Behavior与“依赖”效果
解决了"是什么","从哪来"两大哲学问题,接下来我们就可以看一下"到哪去"这个问题了。这个问题也就是Behavior到底有什么用?我们先来看它的第一个作用:形成View之间的依赖效果,即形成View之间的互动。
这个功能是唯一涉及到两个View功能。
首先我们依旧来看一下如何产生这样的效果,两种方式:
在xml布局中指定属性
假设一个FAB放在AppbarLayout的右下方,通过layout_anchor和layout_anchorGravity两个属性即可达到。
在其LayoutParams初始化的时候获得了Anchor的id和gravity
LayoutParams(Context context, AttributeSet attrs) {
....
mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
View.NO_ID);
this.anchorGravity = a.getInteger(
R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
Gravity.NO_GRAVITY);
...
}
然后我们查找mAnchorId和与之相关的方法,发现还是在prepareChildren()这个方法中确定了mAnchorView,即当前View所依赖的View。
private void prepareChildren() {
for (int i = 0, count = getChildCount(); i < count; i++) {
...
final LayoutParams lp = getResolvedLayoutParams(view);
//通过findViewById找到mAnchorView,并且找到mAnchorView的父辈中属于CL的直接子View的那一个(不清楚是干嘛的)
lp.findAnchorView(this, view);
...
}
...
}
在Bh的layoutDependsOn()方法中返回true
我们寻找layoutDependsOn()方法,在CL中发现了为数不多的几处,一个地方是在onChildViewsChanged()这个方法中,而onChildViewsChanged()又分别在Scroll和Fling以及preDraw(OnPreDrawListener中)、onChildViewRemoved(HierarchyChangeListener)这些场景中被调用。所以我们猜想只要View发生了变化,这些地方都是两个View进行互动的地方! 看一下onChildViewsChanged(),也基本印证了我们的猜想。
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
....
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
...
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();
//如果mDependencySortedChildren.get(j)是依赖于mDependencySortedChildren.get(i)。
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
...
switch (type) {
case EVENT_VIEW_REMOVED:
//执行如果mDependencySortedChildren.get(j)的Bh的onDependentViewRemoved方法。
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
...
}
}
}
}
这里我们也找到了上面通过xml方式确定一个mAnchorView后两个View是如何联动的答案,也是在这个方法中,也就是说View产生的一系列变动是两个View联动的触因。
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
//如果mDependencySortedChildren.get(i)的lp.mAnchorDirectChild和mDependencySortedChildren.get(j)相等
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
.....
}
}
void offsetChildToAnchor(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//如果mAnchorView不空
if (lp.mAnchorView != null) {
....
if (changed) {
final Behavior b = lp.getBehavior();
//根据mAnchorView进行移动
if (b != null) {
b.onDependentViewChanged(this, child, lp.mAnchorView);
}
}
}
}
看一个实例,一般最简单的布局是外层一个CL,里面一个AppbarLayout一个RecyclerView,此时我们会给RecyclerView添加这样的属性:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
它对应的就是AppBarLayout的一个静态内部类ScrollingViewBehavior,而它在“依赖”上所做的事情就是让当前View处于AppBarLayout的下面:
//ScrollingViewBehavior.java
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}
private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap and overlap
//让View处于AppBarLayout下面
final Behavior ablBehavior = (Behavior) behavior;
ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
+ ablBehavior.mOffsetDelta
+ getVerticalLayoutGap()
- getOverlapPixelsForOffset(dependency));
}
}
##说一下mDependencySortedChildren
上面频繁出现了mDependencySortedChildren这个成员,我说一下我的理解。在CL中搜索,第一次赋值出现的位置是onMeasure()的prepareChildren()中。
//CoordinatorLayout#prepareChildren()
mDependencySortedChildren.addAll(mChildDag.getSortedList());
从名字上来看好像是和依赖有关系,而且和mChildDag这个成员有关系,继续搜索mChildDag,它是一个DirectedAcyclicGraph,其注释是:代表了一个简单的不可循环的graph(…没看懂)。
/**
* A class which represents a simple directed acyclic graph.
*/
final class DirectedAcyclicGraph
不过没关系,我们继续搜索它被赋值的地方。mChildDag的addNode()和addEdge()都是操作的名为 mGraph的SimpleArrayMap,该SimpleArrayMap存放的是(T,ArrayList< T >)的键值对,也就是说存进mChildDag的每一个View都对应一个ArrayList< View > ,这个ArrayList< View > 保存的是依附于它的View的集合。
private void prepareChildren() {
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
...
//先把getChildAt(i)添加到mChildDag中
mChildDag.addNode(view);
for (int j = 0; j < count; j++) {
//只考虑i以外的View
if (j == i) {
continue;
}
final View other = getChildAt(j);
final LayoutParams otherLp = getResolvedLayoutParams(other);
//如果getChildAt(j)依赖于getChildAt(i)
if (otherLp.dependsOn(this, other, view)) {
if (!mChildDag.contains(other)) {
//添加getChildAt(j)到mChildDag中
mChildDag.addNode(other);
}
//添加getChildAt(j)到getChildAt(i)对应的ArrayList中
mChildDag.addEdge(view, other);
}
}
}
// 获取一个List添加到mDependencySortedChildren中
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);
}
然后调用了mChildDag.getSortedList()获取一个List并添加到mDependencySortedChildren中,并且最后还要把mDependencySortedChildren倒序一下,注释的意思是说我们开始的时候是想从没有dependency的View开始(从后面的方法实现可以看出,没有dependency的view后添加进List,也就是说产生变化的View放在前面,被动联动的View放在后面)。
mChildDag.getSortedList()这个List很有意思,获取的过程概括的说就是用 dfs+回溯 来返回一个基于依赖关系的列表。重要的都注释在下面了。
//用于存放结果
private final ArrayList mSortResult = new ArrayList<>();
//用于保护现场
private final HashSet mSortTmpMarked = new HashSet<>();
ArrayList getSortedList() {
mSortResult.clear();
mSortTmpMarked.clear();
// 开始dfs
for (int i = 0, size = mGraph.size(); i < size; i++) {
//三个参数分别是:一个View,一个用于存放结果的ArrayList,一个用于记录的HashSet
dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
}
return mSortResult;
}
//开始dfs收集List
private void dfs(final T node, final ArrayList result, final HashSet tmpMarked) {
//如果已经将这个View添加在了结果中
if (result.contains(node)) {
//直接返回
return;
}
//如果这个View已经在tmpMarked中了,代表循环依赖了,抛出异常(因为是在每个主View对应的ArrayList中进行dfs,而ArrayList中的View代表依赖了主View,如果两者相等就是循环依赖)
if (tmpMarked.contains(node)) {
throw new RuntimeException("This graph contains cyclic dependencies");
}
// 标记这个View为已经检查
tmpMarked.add(node);
// 取出这个View对应的ArrayList(代表依赖这个View的所有View)
final ArrayList edges = mGraph.get(node);
if (edges != null) {
//从这个View对应的ArrayList中继续递归
for (int i = 0, size = edges.size(); i < size; i++) {
dfs(edges.get(i), result, tmpMarked);
}
}
// 还原现场
tmpMarked.remove(node);
//很关键
// 先递归后添加,所以最先添加的是依赖其他View的View
result.add(node);
}
##Behavior与嵌套滑动
我们把比较复杂的两个View的依赖讲完了,Bh的作用还有很多,这里讲一下它作为嵌套滑动机制里CL的代理作用。嵌套滑动机制在我上一篇文章中已经讲过了,还不清楚的童鞋我强烈建议去看一下。
我们知道嵌套滑动里的child在onTouchEvent()的DOWN中会起调startNestedScroll(),假设parent是CL,那么会回调它的onStartNestedScroll(),这个方法里会做什么呢?它把回调交给感兴趣的直接子View去处理了!
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//交给某个View的Bh去处理
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
//只要有一个感兴趣就表示对接成功
handled |= accepted;
//在该Bh中记录是否接受了嵌套滑动
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
同理在onNestedPreScroll()、onNestedScroll()、onNestedPreFling()、onNestedFling()也都是交给直接子View去处理了。记住这么几点:你不必为你的RV或其他ScrollingView添加依赖,因为CL的每一个直接子View都有机会去处理嵌套滑动;滑动的起调者不一定是CL的直接子View,嵌套滑动事件会传到CL的直接子View罢了。
还有,从源码来看,一次嵌套滑动可以有多个parent也就是多个CL的直接子View响应,但是在嵌套滑动开始之后这个关系是不能改变的。
我们还是举出一个具体的栗子,看一看直接子View究竟是怎样帮助CL去处理嵌套滑动的。比如RecyclerView、CoordinatorLayout 、AppBarLayout的组合,RV是作为嵌套机制里面的child,CL是作为parent,AppBarLayout是用注解的方式获取了一个Bh的CL的直接子View,那么CL作为parent嵌套滑动是要交给AppBarLayout处理。
通常是要给RV的布局中加上app:layout_behavior="@string/appbar_scrolling_view_behavior 这样一个属性,这样RV就具有了Bh,这个Bh让RV依赖于AppBarLayout(一直让RV在其下方),但这里依赖关系不是重点!依赖关系和嵌套滑动机制是相互独立的!
嵌套滑动的流程我就不再重述了。首先是调用startNestedScroll()启动嵌套滑动时,贴一张图:
既然都说了AppBarLayout会帮助CL处理,那么我们有理由相信在其Bh的onStartNestedScroll()中会做相应判断:
//AppBarLayout$Behavior.java
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
//如果是 纵向 + 有可滑动的子View + 还有空间滑动 则承诺作为嵌套滑动中的parent
//(这里的directTargetChild是指嵌套滑动中的child向上寻找parent的倒数第二个parent...也可能是child本身)
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
...
return started;
}
然后是RV在onTouchEvent()的MOVE中调用dispatchNestedPreScroll()的时候以及AppBarLayout处理完RV也处理完之后继续调用ispatchNestedScroll()的时候,也贴一张图:
AppBarLayout也做了相应的处理,都和滚动相关,我就不展开了:
//AppBarLayout$Behavior.java
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
if (dy != 0 && !mSkipNestedPreScroll) {
int min, max;
if (dy < 0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
scroll(coordinatorLayout, child, dyUnconsumed,
-child.getDownNestedScrollRange(), 0);
// Set the expanding flag so that onNestedPreScroll doesn't handle any events
mSkipNestedPreScroll = true;
} else {
// As we're no longer handling nested scrolls, reset the skip flag
mSkipNestedPreScroll = false;
}
}
##Behavior与测量和布局
CL作为一个VG,测量和布局肯定是它的主要作用,这关系到系统如何绘制这个VG。
先来看一看onMeasure(),大致的逻辑就是先交给每个直接子View的Bh的onMeasureChild()去测量,如果返回false,调用CL的onMeasureChild(),最终调用的是VG的measureChildWithMargins。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//上面解析过,不仅让View获得Bh,anchorView,而且把View按照dependency的由少到多形成一个List
//即没有dependency的View在前面
prepareChildren();
ensurePreDrawListener();
//获取一些相关参数
....
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//keyLine相关,不懂什么意思
...
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
//适配insets,不懂什么意思,看到网上说是statusbar
....
final Behavior b = lp.getBehavior();
//如果Bh为空或者测量失败
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
//正常测量逻辑
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
...
}
final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & ViewCompat.MEASURED_STATE_MASK);
final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
setMeasuredDimension(width, height);
}
再看一下onLayout(),也是类似的逻辑。先交给Bh处理再由自身处理。
@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);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
//Bh测量
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
//
onLayoutChild(child, layoutDirection);
}
}
}
可是就这么粗略看一下似乎什么也体会不到,这里还是举一个栗子更直观地感受一下。比如这样一个布局CL中有一个AppBarLayout,一个RV,一个FAB。
每个View都有自己对应的Bh,对应着不同的测量和布局逻辑。
view | behavior |
---|---|
AppBarLayout | AppBarLayout.Behavior |
RecyclerView | AppBarLayout.ScrollingViewBehavior |
FloatingActionButton | AppBarLayout.Behavior |
这些Bh的继承图是这样的,可以看到都和一个叫ViewOffsetBehavior的类有关,从名字上看肯定和偏移有关。
[外链图片转存失败(img-PaPbEk6E-1563607218254)(http://obbna3lzo.bkt.clouddn.com/AppBar相关的Behaviorv3.png)]
那么我们就从测量开始,从上述对CL的onMeasure的整体分析,以及mDependencySortedChildren的顺序,我们知道测量肯定从AppBarLayout开始。
//AppBarLayout&Behavior
@Override
public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
// If the view is set to wrap on it's height, CoordinatorLayout by default will
// cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
// what we actually want, so we measure it ourselves with an unspecified spec to
// allow the child to be larger than it's parent
//如果高度是WRAP_CONTENT,传入MeasureSpec.UNSPECIFIED作为Mode进行测量
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
return true;
}
// 默认返回false
return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed);
}
看一下RV的测量,它是使用的ScrollingViewBehavior的父类HeaderScrollingViewBehavior的onMeasureChild方法,可以看到,RV的高度是减去了AppBarLayout的高度的。
//HeaderScrollingViewBehavior
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
//如果是MATCH_PARENT或者是WRAP_CONTENT
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
final List dependencies = parent.getDependencies(child);
//获取它所依赖的View
final View header = findFirstDependency(dependencies);
if (header != null) {
//和fitsWindow相关
....
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}
//减去了header的高度
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
// Now measure the scrolling view with the correct height
//测量
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
FAB的测量略。
接下来看一下布局的过程。首先还是AppBarLayout的布局,几经辗转最终还是调用到了CL的layoutChild,计算了一下statusbar的高度,并偏移到这个AppBarLayout的布局上。
//AppBarLayout&Behavior .java
@Override
public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
int layoutDirection) {
boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
//下面一大段和PendingIntentAction有关
....
return handled;
}
//ViewOffsetBehavior.java
@Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// First let lay the child out
layoutChild(parent, child, layoutDirection);
...
return true;
}
//ViewOffsetBehavior.java
protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// Let the parent lay it out by default
parent.onLayoutChild(child, layoutDirection);
}
//CoordinatorLayout.java
public void onLayoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
...
else {
layoutChild(child, layoutDirection);
}
}
//CoordinatorLayout.java
private void layoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//适配statusbar的高度
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), parent, out, layoutDirection);
child.layout(out.left, out.top, out.right, out.bottom);
}
再看RV的布局过程,注意最终调用的是ViewOffsetBehavior的onLayoutChild,但是注意看上面我给的Bh的继承图,HeaderScrollingViewBehavior是重写了layoutChild的方法的,所以最终调用的是HeaderScrollingViewBehavior的layoutChild(一开始我没注意,心想那最后AppBarLayout和RV同样的布局不就重叠了~)。最后RV的布局考虑了AppBarLayout的存在。
//ViewOffsetBehavior.java
@Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
//注意!这里是调用了HeaderScrollingViewBehavior#layoutChild
//而不是ViewOffsetBehavior#layoutChild
layoutChild(parent, child, layoutDirection);
...
return true;
}
//HeaderScrollingViewBehavior.java
@Override
protected void layoutChild(final CoordinatorLayout parent, final View child,
final int layoutDirection) {
final List dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
//计算了RV的可用空间
available.set(parent.getPaddingLeft() + lp.leftMargin,
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom()
- parent.getPaddingBottom() - lp.bottomMargin);
final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
//适配statusbar
...
//实际布局区域
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), available, out, layoutDirection);
//计算重叠
final int overlap = getOverlapPixelsForOffset(header);
//最终布局
child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
mVerticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
mVerticalLayoutGap = 0;
}
}
##Behavior与Touch事件
作为一个VG,它把Touch事件的管理都交给了子View的Bh。注意,这里和嵌套滑动没有关系,CL在嵌套滑动中的身份是一个NestedScrollingParent,可以看成是一个listener响应child的调用,而在事件分发流程中,CL作为一个VG,有拦截事件流和处理事件流的主动权。
我们看到在onInterceptTouchEvent()中会调用performIntercept()这个方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
...
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
...
return intercepted;
}
这个方法利用View的Bh进行了拦截或消耗(这个函数可以用于onInterceptTouchEvent或onTouchEvent中)。
private boolean performIntercept(MotionEvent ev, final int type) {
...
final List topmostChildList = mTempList1;
//以z-order排序
getTopSortedChildren(topmostChildList);
//让最上层的View先被检查
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();
//如果 被拦截 且不是DOWN,发送CANCEL事件
//也就是CL的某个直接子View决定拦截过后,要向其他View发送CANCEL事件
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
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) {
//如果是onInterceptTouchEvent函数中
case TYPE_ON_INTERCEPT:
//让该View决定是否拦截
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
//如果是onTouchEvent函数中(此时已经执行到了事件流向上传递的过程,CL依旧把事件的消耗交给某个View)
case TYPE_ON_TOUCH:
//让该View决定是否消耗
intercepted = b.onTouchEvent(this, child, ev);
break;
}
//不管是在下发过程拦截了,还是在上传过程中消耗了
if (intercepted) {
//记录下这个CL的直接子View
mBehaviorTouchView = child;
}
}
...
}
topmostChildList.clear();
return intercepted;
}
接着在onTouchEvent中,能执行到这,说明CL本身(有直接子View)拦截了事件下发或者没有View消耗了事件,事件又回传到了CL。
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
//如果有拦截的View 或者 ...
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
//交给这个View去处理
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
//调用super.onTouchEvent也就是View的
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
...
return handled;
}
我们还是以上一节Behavior的的测量和布局中举的栗子,AppBarLayout和RV对触摸事件会有怎样的动作呢?肯定要看它们对应的Bh:AppBarLayout&Behavior和AppBarLayout&ScrollingViewBehavior。
经过一番搜索,RV的ScrollingViewBehavior以及其父类都没有重写onInterceptTouchEvent,只有最顶层的父类CoordinatorLayout&Behavior的onInterceptTouchEvent返回了false。也就是说RV不会帮助CL拦截触摸事件。
继续看AppBarLayout的AppBarLayout&Behavior,其父类HeaderBehavior重写了onInterceptTouchEvent和onTouchEvent方法.
onInterceptTouchEvent()里面拦截的逻辑很简单啊,就是检查到是拖动就拦截。
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
...
//如果已经在拖动,拦截
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true;
}
switch (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_DOWN: {
//记录一些信息
...
break;
}
case MotionEvent.ACTION_MOVE: {
...
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
//如果大于临界值,拦截
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
}
break;
}
...
}
return mIsBeingDragged;
}
onTouchEvent()中对ABL做了相应的处理。
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
switch (MotionEventCompat.getActionMasked(ev)) {
...
case MotionEvent.ACTION_MOVE: {
final int y = (int) ev.getY(activePointerIndex);
int dy = mLastMotionY - y;
if (mIsBeingDragged) {
mLastMotionY = y;
// 拖动ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
}
break;
}
case MotionEvent.ACTION_UP:
if (mVelocityTracker != null) {
...
//fling ABL
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
....
}
....
return true;
}
看到这里其实我们明白了,当我们在滑动的时候ABL会首先拦截手势进行滚动,当它不拦截的时候或者说达到某些条件的时候(没仔细看源码ABL是何时放弃拦截的以及怎么放弃拦截的,清楚的朋友指点一下!!!),整个CL就不会拦截手势,事件会下发交给RV,进行嵌套滑动或者联动,那都是后话了。
##总结
好了,现在我们差不多对CL的Behavior有了一个总体的认识,CL作为一个VG本身并没有做太多事情,他把一切交给Behavior去完成,包括拦截和处理触摸事件、测量和布局、嵌套滑动、形成两个View之间的依赖等。CL的直接子View的Behavior,看起来就像CL的管家们。
Behavior的创建有三种方式:一种在xml中作为一个属性赋值,会在View树实例化的时候初始化;一种在代码中动态指定;一种用注解为类指定一个Behavior,在绘制流程的测量过程中,CL会去实例化直接子View的Behavior。
CL中的两个View的依赖效果可以靠在xml中指定一个anchorView属性或者在Behavior的layoutDependsOn()中去完成,在一切回调onChildViewsChanged()的地方(Scroll和Fling以及preDraw、onChildViewRemoved这些地方)完成两个View的互动,具体来说是通过Bh的onDependentViewRemoved()方法。
CL本身作为一个NestedScrollingParent,它把接收child的回调进行响应的任务交给了直接子View,直接子View如果想进行嵌套滑动,就在Bh的onStartNestedScroll()返回true,这个Bh就会被标记,接着onNestedPreScroll()就会选出感兴趣的直接子View的Bh去执行其onNestedPreScroll()方法…
CL的测量和布局也和Behavior紧紧相关,不仅是先交由直接子View的onMeasure和onLayout经手,而且拥有不同Bh的View在测量和布局上会表现出不同的特点。
CL的Touch事件分发和嵌套滑动是独立的,直接子View的Bh有拦截事件向下分发的权利,也能在CL决定自身消耗事件的时候进行具体的处理。
现在我对CoordinatorLayout有了还算不错的理解,希望在后面的使用中能不断加深对它的理解,也希望通过此文抛砖引玉,多少给大家一些帮助,或者在某一点上解答了大家的疑惑。有什么不足或者见解,希望多多交流!!