【转载】CoordinatorLayout源码解析
CoordinatorLayout 源码分析
CoordinatorLayout
有一些很有意思的特性,设置anchor、NestedScroll配合Toolbar/TabLayout的显隐or伸缩、Fab的移动等。今天咱就来一探究竟!
1. 从LayoutParam开始
CoordinatorLayout.LayoutParam
中有一些不太一样的属性和元素,在此先进行介绍。
1.1 特殊属性
属性 | 对应xml属性 | 用途 |
---|---|---|
AndchorId |
layout_anchor &layout_anchorGravity |
布局时根据自身gravity 与 layout_anchorGravity 放置在被anchor的View中 |
Behavior |
layout_behavior |
辅助Coordinator对View进行layout、nestedScroll的处理 |
KeyLine |
layout_keyline &keylines |
给Coordinator设置了keylines (整数数组)后,可以为子View设置layout_keyline="i" 使其的水平位置根据对应keylines[i] 进行layout。 |
LastChildRect |
无 | 记录每一次Layout的位置,从而判断是否新的一帧改变了位置 |
注:
keyline
是一个非常奇怪的属性,我在看源码时才第一次看到到这玩意,网上的资料也非常之少。分析下来,就是如果设置了keyline,那么gravity就会被无视,直接放置在对应的水平位置keyline上。CoordinatorLayout里面也没有其他的特性是根据keyline实现的,个人认为没卵用,本文对它的分析基本都会略过。
1.2 依赖关系
假设此时有两个View: A 和B,那么有两种情况会导致依赖关系:
- A的
anchor
是B ; - A的
behavior
对B有依赖(比如FloatingActionButton
依赖SnackBar
)。
依赖关系建立的前提是两个View在同一个Coordinatorlayout
中。
CoordinatorLayout
中维护了一个mDependencySortedChildren
列表,里面含有所有的子View,按依赖关系排序,被依赖者排在前面。我们可以看一下用来排序的Comparator
:
final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() {
@Override
public int compare(View lhs, View rhs) {
if (lhs == rhs) {
return 0;
} else if (((LayoutParams) lhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, lhs, rhs)) {
return 1;
} else if (((LayoutParams) rhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, rhs, lhs)) {
return -1;
} else {
return 0;
}
}
};
注意,在建立
mDependencySortedChildren
并排序完成之后(在measure的第一步处理完成),每次对子View的遍历都是通过它进行顺序遍历,保证了被依赖的View最先被处理。
1.3 Behavior
在CoordinatorLayout
中定义了Behavior
类,它是用来辅助layout的工具。如果一个CoordinatorLayout的直接子View设置了Behavior
(或者通过类注解@DefaultBehavior
指定Behavior
),则该Behavior会储存在该View的LayoutParam
中。
注意:不是CoordinatorLayout的直接子View,设置Behavior是无效的。你可以看到任何一处对于Behavior的处理都是直接
getChildCount()
遍历。
在Behavior中有几类功能,我们一一进行介绍:
1.3.1 触摸响应类
Behavior
中有两个函数:onInterceptTouchEvent
、 onTouchEvent
。在CoordinatorLayout每次触发对应事件的时候会选择一个最适合的子View的Behavior执行对应函数。我们来看一下CoordinatorLayout是怎么分发和处理Touch事件的:
intercept
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null; final int action = MotionEventCompat.getActionMasked(ev); // 重置响应的Behavoir
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
} // 在这里选择一个最佳Behavior进行处理
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT); if (cancelEvent != null) {
cancelEvent.recycle();
} // 重置响应的Behavior
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
} return intercepted;
}
在performIntercept
去选择一个最适合的Behavior来进行处理,这个方法不仅用于onInterceptTouchEvent
,并且也用于onTouchEvent
,根据传入type
不同来识别对应方法。我们来看看它的逻辑:
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; // API>=21时,使用elevation由低到高排列View;API<21时,按View添加顺序排列
getTopSortedChildren(topmostChildList); 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(); // ...(省略代码) 如果此次判定intercept,则对上次的Behavior发送CANCEL事件。 // 根据传入type不同调用不同的方法
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;
}
} //...(省略代码) 如果Behavior.blocksInteractionBelow()返回true,则不处理后续的事件。
} topmostChildList.clear(); return intercepted;
}
1.3.2 依赖关系类
这部分比较简单,就俩函数: layoutDependsOn
:返回true
则表示对另一个View有依赖关系;onDependentViewChanged
&onDependentViewRemoved
:如果被依赖的View在正常layout之后仍有size/position上的变化,或者被remove掉,都会触发对应方法。
那么问题来了,CoordinatorLayout是怎么监听这个被依赖的View改变的事件的呢?
原来它里面有一个ViewTreeObserver.OnPreDrawListener
,它在onMeasure
的时候被添加到了ViewTreeObserver
中,这样每一帧被绘制出来之前都会调用这个回调。
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged(false);
return true;
}
}
这个dispatchOnDependentViewChanged
里面代码比较多,就不放上来了,总结下来就是这样:
根据依赖关系遍历子View,对每一个View做如下操作
- 判断一下新的布局边界与lastChildRect是否相同,是则记录新的布局边界为lastChildRect,并继续后续流程,否则跳过;
- 对于之后每一个View,如果它依赖于本View,则调用它的
Behavior.onDependentViewChanged
(如果有Behavior的话)。
至于onDependentViewRemoved
,是在初始化的时候就会调用ViewGroup.setOnHierarchyChangeListener()
方法设置一个OnHierarchyChangeListener
,这样每次add和remove子View的时候就会接收到回调,同时对相应依赖关系的View进行处理。
1.3.3 布局类
onMeasureChild
&onLayoutChild
:如果重写了该方法并返回true
,则CoordinatorLayout会使用Behavior对这个子View进行measure/layout。具体的可以见下面的Measure&Layout
1.3.4 嵌套滑动类
CoordinatorLayout实现了NestedScrollingParent
,当CoordinatorLayout内有一个支持NestedScroll的子View时,它的嵌套滑动事件通过NestedScrollingParent
的回调分发到各直接子View的Behavior处理。虽然Behavior
类没有实现NestedScrollingParent
,但是实际上它的方法都有。有兴趣的同学可以去看看这个类,我们这里重点讲CoordinatorLayout的分发过程。
各个事件的分发过程类似,此处就举一个例子:
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) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted; lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
非常简单吧,就遍历一下直接子View,每个都调一下对应的回调方法,只要有任何一个子View的behavior消耗了这个事件,就算消耗了这个事件。
2. Measure&Layout
我们知道,ViewGroup要把子View准确地放置到屏幕上都是要走onMeasure
onLayout
的,那么我们看看CoordinatorLayout
在这里干了什么。
在看懂时请确保你明白measure/layout
的意义以及基本用法,否则可能会导致身体不适=。=
2.1 Measure
最直接的就是看代码,如果不喜欢,可以跳过代码看总结。:
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) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted; lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
总结下来,onMeasure干了这么几件事:
- 根据依赖关系对所有子View进行排序
- 保证OnPreDrawListener被添加
- 按依赖关系遍历子View:更新本身的Measure尺寸。
- 如果子View有Behavior,并且它的
onMeasureChild
返回true,则使用Behavior进行measure;否则直接使用measureSpec对子View进行measure; - 取子VIew最大的measure尺寸为已使用的measure尺寸。
- 如果子View有Behavior,并且它的
2.2 Layout
在onLayout
中,我们可以看到CoordinatorLayout
会对每一个子View依照以下判断顺序进行layout:
- 如果子View设置了
Behavior
,并且该Behavior
的behavior.onLayoutChild
返回true
,则使用behavior.onLayoutChild
对该子View进行layout; - 如果Behavior不进行layout,则进入自身的
onLayoutChild()
,内部依次进行如下判断:
- 如果子View设置了
Anchor
,则调用layoutChildWithAnchor
(根据anchor进行layout); - 如果子View含有
keyline
,则调用layoutChildWithKeyline
(根据keyline进行layout); - 如果以上判断都不符合,则直接将View根据padding/margin/measure结果按照
Gravity
放置。
我们一一来看一下这些过程。
2.2.1 使用Behavior进行layout
默认的Behavior的onLayoutChild
都是返回false
的,那么我们看看FloatingActionButton
的默认Behavior是怎么处理的吧:
@Override
public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
int layoutDirection) { // 检查该FAB是否依赖AppBarLayout
final List<View> dependencies = parent.getDependencies(child);
for (int i = 0, count = dependencies.size(); i < count; i) {
final View dependency = dependencies.get(i);
if (dependency instanceof AppBarLayout
&& updateFabVisibility(parent, (AppBarLayout) dependency, child)) {
break;
}
} // 调用CoordinatorLayout的onLayoutChild对FAB进行layout
parent.onLayoutChild(child, layoutDirection);
// 在API < 21时,需要手动offset来让出阴影的位置
offsetIfNeeded(parent, child);
return true;
}
这里主要是处理了如果FAB设置了AppBarLayout
为anchor时(此时会对AppBarLayout
有依赖),则当AppBarLayout
的高度不足以显示FAB时将其隐藏)。
之后它会手动调用CoordinatorLayout自身的onLayoutChild
方法进行layout,即上述判断的第二步,那我们继续往下看。
2.2.2 使用Anchor进行layout
如果View设置了anchor,那么都会调用layoutWithAnchor
进行layout,代码与解释如下:
private void layoutChildWithAnchor(View child, View anchor, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect anchorRect = mTempRect1;
final Rect childRect = mTempRect2; /* 1. 找到被anchor的View的布局边界 */
getDescendantRect(anchor, anchorRect); /* 2. 获取到被anchor的View布局边界之后,配合layout_anchorGravity与自身的gravity获取到最终要layout到的边界 */
getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, childRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
这里用到的两个关键函数就是getDescendantRect
与getDesiredAnchoredChildRect
,它们的目的在我添加的注释中进行了解释,为保证文章的可读性就不再把代码放上来了,有兴趣的同学可以再自己去挖掘相应代码~~
2.2.3 直接layout
如果之前的都不符合,就会走到这一步,我们看看它是怎么layout的:
private void layoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect parent = mTempRect1;
parent.set(getPaddingLeft() lp.leftMargin,
getPaddingTop() lp.topMargin,
getWidth() - getPaddingRight() - lp.rightMargin,
getHeight() - getPaddingBottom() - lp.bottomMargin); //...(省略代码) 处理由于fitsSystemWindows带来的inset // 按照Gravity与measure尺寸在父控件里面找到自己的位置,并进行layout。
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);
}
3 总结
CoordinatorLayout的特性总结下来就是两个方面:
- 可以设置anchor,被依赖的View变化自身也会变化;
- 可以设置behavior,当内部有支持嵌套滑动的控件时处理NestedScroll事件;
这两个特性导致的子View之间的依赖关系让界面的交互更有意思。有兴趣的同学可以再去看AppBarLayout
、FloatingActionButton
、SnackBar
的源码~~
【转载】CoordinatorLayout源码解析的更多相关文章
- 【转载】FloatingActionButton源码解析
原文地址:https://github.com/Rowandjj/my_awesome_blog/blob/master/fab_anlysis/README.md loatingActionButt ...
- jQuery整体架构源码解析(转载)
jQuery整体架构源码解析 最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性, ...
- 转载:Bootstrap 源码解析
Bootstrap 源码解析 前言 Bootstrap 是个CSS库,简单,高效.很多都可以忘记了再去网站查.但是有一些核心的东西需要弄懂.个人认为弄懂了这些应该就算是会了.源码看一波. 栅格系统 所 ...
- 【转载】Xutils3源码解析
Github源码地址:https://github.com/wyouflf/xUtils3 原文地址 :http://www.codekk.com/blogs/detail/54cfab086c476 ...
- 【转载】okhttp源码解析
转自:http://www.open-open.com/lib/view/open1472216742720.html https://blog.piasy.com/2016/07/11/Unders ...
- 【转载】Scroller源码解析
原文地址:https://github.com/Skykai521/AndroidSdkSourceAnalysis/blob/master/article/Scroller%E6%BA%90%E7% ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
随机推荐
- 图论:LCA-树上倍增
BZOJ1602 求最近公共祖先有三种常用的方法,在线的有两种,分别是树上倍增算法和转化为RMQ问题 离线的有一种,使用Tarjan算法 这里,我们介绍复杂度优异并且在线的倍增算法,至于后续的两种方法 ...
- Jmeter-7-在命令行中运行Jmeter.
jmeter -n -t D:\Jmeter_result\Script_baidu.jmx -l D:\Jmeter_result\Script_baidu.txt jmeter -n -t D:\ ...
- 【Codeforces370E】Summer Reading [构造]
Summer Reading Time Limit: 20 Sec Memory Limit: 512 MB Description Input Output Sample Input 7 0 1 ...
- 【BZOJ】3790 神奇项链
[算法](manacher+贪心)||(manacher+DP+树状数组/线段树) [题解] manacher求回文串,后得到线段,做一点计算映射回原串线段. 然后问题转化为可重叠区间线段覆盖问题,可 ...
- Hadoop和大数据:60款顶级开源工具(山东数漫江湖)
说到处理大数据的工具,普通的开源解决方案(尤其是Apache Hadoop)堪称中流砥柱.弗雷斯特调研公司的分析师Mike Gualtieri最近预测,在接下来几年,“100%的大公司”会采用Hado ...
- 【洛谷 P3899】 [湖南集训]谈笑风生 (主席树)
题目链接 容易发现\(a,b,c\)肯定是在一条直链上的. 定义\(size(u)\)表示以\(u\)为根的子树大小(不包括\(u\)) 分两种情况, 1.\(b\)是\(a\)的祖先,对答案的贡献是 ...
- github删除文件夹
git rm -rf dirgit add .git commit -m 'remove dir'git push origin master //dir是要删除的文件夹路径
- 为什么Windows7打开项目的方式是灰的不能修改
http://jingyan.baidu.com/article/d3b74d64a964691f77e60900.html 进入组策略编辑器,即运行gpedit.msc,进入“用户配置”-“管理模板 ...
- 【Android framework】am命令启动Activity流程
源码基于Android 4.4. am start -W -n com.dfp.test/.TEstActivity -W:等目标Activity启动后才返回 -n:用于设置Intent的Comp ...
- 使用 ftrace 调试 Linux 内核,第 1 部分【转】
转自:http://www.ibm.com/developerworks/cn/linux/l-cn-ftrace1/index.html ftrace 是 Linux 内核中提供的一种调试工具.使用 ...