转载请注明出处:https://blog.youkuaiyun.com/dnntjc/article/details/82055393
心血来潮,看了一遍RelativeLayout的源码,并整理了一下,如有错误,欢迎指正。
onMeasure做的第一件事就是对子view进行排序:
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
sortChildren();
}
执行requestLayout()后,mDirtyHierarchy就为true了:
@Override
public void requestLayout() {
super.requestLayout();
mDirtyHierarchy = true;
}
sortChildren()就是对子view进行横向和纵向的排序,过程是这样的先把所有子view添加到节点中,并维持view的ID和节点的一个映射关系,之后解析子view的规则属性,根据子view的规则属性得到节点之间的依赖关系;最后通过节点之间的依赖关系,对view进行深度优先排序:
private void sortChildren() {
//...省略代码
//将所有子view结点添加到graph的mNodes中,同时在mKeyNodes中保存了view的id和view结点的映射关系。
for (int i = 0; i < count; i++) {
graph.add(getChildAt(i));
}
//对子view进行纵向和横向排序
//RULES_VERTICAL和RULES_HORIZONTAL是包含了纵向和横向规则的数组
//RULES_VERTICAL = {ABOVE, BELOW, ALIGN_BASELINE,ALIGN_TOP, ALIGN_BOTTOM}
//RULES_HORIZONTAL = {LEFT_OF,RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF,ALIGN_START, ALIGN_END};
graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
}
void getSortedViews(View[] sorted, int... rules) {
//roots存放的是所有的根结点,根结点指的是仅在横向上或者是仅在纵向上不依赖于其他子view的view结点
final ArrayDeque<Node> roots = findRoots(rules);
int index = 0;
Node node;
//这里是一个深度优先搜索,roots.pollLast()相当于出栈
while ((node = roots.pollLast()) != null) {
final View view = node.view;
final int key = view.getId();
sorted[index++] = view;
//dependents的key值是依赖于node的结点
final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
final int count = dependents.size();
for (int i = 0; i < count; i++) {
final Node dependent = dependents.keyAt(i);
//dependencies是dependent所依赖的所有节点
final SparseArray<Node> dependencies = dependent.dependencies;
//由于node节点的view已经添加到sorted中了,因此移除dependent所依赖的node节点
dependencies.remove(key);
//当denpendent所依赖的所有节点的view都已经在sorted中时,将dependent添加到roots中,即入栈
if (dependencies.size() == 0) {
roots.add(dependent);
}
}
}
if (index < sorted.length) {
throw new IllegalStateException("Circular dependencies cannot exist"
+ " in RelativeLayout");
}
}
private ArrayDeque<Node> findRoots(int[] rulesFilter) {
final SparseArray<Node> keyNodes = mKeyNodes;
final ArrayList<Node> nodes = mNodes;
final int count = nodes.size();
// 清空数据
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
node.dependents.clear();
node.dependencies.clear();
}
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
//rules的索引是规则,值是规则所对应的View的ID
final int[] rules = layoutParams.mRules;
final int rulesCount = rulesFilter.length;
//对纵向或者纵向规则进行遍历
for (int j = 0; j < rulesCount; j++) {
//rule是view规则对应的ID
//例如当前view有一个属性:layout_toStartOf="@+id/test“,则rule对应的是test的view ID
final int rule = rules[rulesFilter[j]];
if (rule > 0) {
//获取ID为rule的结点,即dependency是node所依赖的节点
final Node dependency = keyNodes.get(rule);
//跳过不存在的节点和自身节点
if (dependency == null || dependency == node) {
continue;
}
// Add the current node as a dependent
//将依赖于dependency的node作为key,当前graph作为value存入dependency的dependents中
//dependents是一个ArrayMap<Node, DependencyGraph>
dependency.dependents.put(node, this);
//将node所依赖的节点放入node的dependencies中,dependencies是一个SparseArray<Node>
node.dependencies.put(rule, dependency);
}
}
}
final ArrayDeque<Node> roots = mRoots;
roots.clear();
// Finds all the roots in the graph: all nodes with no dependencies
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
//当node不依赖于任何节点时
if (node.dependencies.size() == 0) roots.addLast(node);
}
return roots;
}
对子view排完序后,下一步就是对子view进行测量,从下面的代码可以看出,RelativeLayout会对子view进行两次测量, 因此不要嵌套太多层RelativeLayout,不然最里层的view的测量次数会以指数级增长(2^n),即使不是RelativeLayout,也不建议嵌套太多层
//在横向上经过排序的view
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
//获取view的规则,layoutDirection是为了适配一些国家从右到左(RTL)的习惯
int[] rules = params.getRules(layoutDirection);
//根据规则设置params的设置params的mLeft和mRight,但可能并不会对所有的mLeft和mRight进行设置
//需要注意的是这里的mLeft和mRight并不是最终的值,下面还会有可能进行修正
//该方法不对CENTER和默认情况(没有设置任何规则属性,即view在左上角)进行处理,对CENTER的处理在positionChildHorizontal中
//也就是说如果子view没设置规则属性或属性是CENTER_IN_PARENT或CENTER_HORIZONTAL,那么mLeft和mRight为VALUE_NOT_SET
applyHorizontalSizeRules(params, myWidth, rules);
//对子view进行横向上的测量,等到子view的width,为positionChildHorizontal做准备
measureChildHorizontal(child, params, myWidth, myHeight);
//对CENTER或默认情况进行处理,并且处理mLeft和mRight其中一个值为VALUE_NOT_SET的情况
//返回值代表的是是否需要进行偏移修正
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
// 与appyHorizontalSizeRules相似,只是多了baseline,这里不做阐述
applyVerticalSizeRules(params, myHeight, child.getBaseline());
// 对子view进行第二次测量,这次测量会得到正确的结果
measureChild(child, params, myWidth, myHeight);
// 与positionChildHorizontal类似
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
// 适配RTL,至于为什么要分版本处理margin,望大神告知!!
// ... 省略
}
// ...省略,设置height
// left,top,right和bottom代表的是所有子view中最左边,最上方,最右边和最上方的边界
// 也就是说这四个值代表的是包含了所有子view的最小矩形
// verticalGravity和horizontalGravity会在文章的后面做解释
// ignore是忽略RelativeLayout的gravity属性的view,对应的xml属性是ignoreGravity
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
applyHorizontalSizeRule这个方法解释了为什么要对view进行排序, 因为首先要确定根结点A的view的mLeft和mRight(在纵向上对应的是mTop和mBottom),然后子结点A1的view根据A确定mLeft和mRight,子结点A2的view再根据A或A1确定mLeft和mRight,以此类推,直至下一个根节点。现假设有两个根节点A和B,子结点分别为A1,A2...An;B1,B2...Bn,那么排序后的view的顺序是这样的:A,A1,A2...An,B,B1,B2...Bn(上面提过是根据深度优先进行的排序的),这样就能保证先确定根结点的view的mLeft或mRight,并根据依赖关系确定子结点的view的mLeft或mRight。
RelativeLayout.LayoutParams anchorParams;
childParams.mLeft = VALUE_NOT_SET;
childParams.mRight = VALUE_NOT_SET;
/*********************************************************************************/
// 获取对应规则的view的LayoutParams,对于根结点的view而言,anchorParams总是为null
anchorParams = getRelatedViewParams(rules, LEFT_OF);
if (anchorParams != null) {
childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
childParams.rightMargin);
// 如果layout_alignWithParentIfMissing为true,且有layout_toLeftOf的属性
// 但其对应的view以及该view在相同属性(这里对应的是LEFT_OF)上的依赖链的view的visibility都为GONE
} else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
if (myWidth >= 0) {
childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
}
}
/******************************下面是被省略的类似的代码******************************/
...
再来看看applyHorizontalSizeRule是如何获取anchorParams的,getRelativeViewParams的重点在于getRelativeView:
private View getRelatedView(int[] rules, int relation) {
int id = rules[relation];
if (id != 0) {
// 通过view的ID获取对应的节点, mKeyNodes是通过前面的graph.add(getChildAt(i))添加的
DependencyGraph.Node node = mGraph.mKeyNodes.get(id);
if (node == null) return null;
View v = node.view;
// Find the first non-GONE view up the chain
// 根据view的依赖关系,获取同样的relation规则的第一个不为GONE的view
while (v.getVisibility() == View.GONE) {
rules = ((LayoutParams) v.getLayoutParams()).getRules(v.getLayoutDirection());
node = mGraph.mKeyNodes.get((rules[relation]));
// ignore self dependency. for more info look in git commit: da3003
if (node == null || v == node.view) return null;
v = node.view;
}
return v;
}
return null;
}
measureChildHorizontal的主要代码是getChildMeasureSpec,
private void measureChildHorizontal(
View child, LayoutParams params, int myWidth, int myHeight) {
// 这里的getChildMeasureSpec并不是调用ViewGroup的,而是自身的
final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
myWidth);
// 下面的代码得出childHeightMeasureSpec给measure方法使用,这里就省略这些代码了
final int childHeightMeasureSpec;
//...省略代码
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
RelativeLayout的getChildMeasureSpec与ViewGroup的getChildMeasureSpec在逻辑上还是有一点像的。RelativeLayout的getChildMeasureSpec有这么几行代码:
else if (childSize == LayoutParams.MATCH_PARENT) {
childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
childSpecSize = Math.max(0, maxAvailable);
}
从上面的几行代码可以看出,如果RelativeLayout是wrap_content,而有一个子view是match_parent,那么RelativeLayout就相当于是match_parent。再来看positionChildHorizontal的代码,它主要是对未设置的mLeft和mRight进行设置:
private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
boolean wrapContent) {
final int layoutDirection = getLayoutDirection();
int[] rules = params.getRules(layoutDirection);
if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
// Right is fixed, but left varies
params.mLeft = params.mRight - child.getMeasuredWidth();
} else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
// Left is fixed, but right varies
params.mRight = params.mLeft + child.getMeasuredWidth();
} else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
// Both left and right vary
if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
// 如果不是wrapContent,居中处理;否则先做默认处理
// 对于wrapContent,到这我们还不知道RelativeLayout的width的真实值
// 因此只能先做默认处理,并在确定了width后再做偏移修正
if (!wrapContent) {
// 这时候并不需要进行偏移修正,这里虽然返回了true
// 但后面的代码还会对wrapContent进行判断,因此实际上并没有进行偏移修正
centerHorizontal(child, params, myWidth);
} else {
// 对于LTR(左到右)言,将child放在最左边;对于RTL,则将child放在最右边,里面的代码非常简单,就不进行解释了
positionAtEdge(child, params, myWidth);
}
return true;
// 默认情况,即子view没有设置横向上的任何属性
} else {
positionAtEdge(child, params, myWidth);
}
}
// 如果layout_alignParentEnd的值为true,则反回true
// 这时即使返回了true,也是只有在wrapContent为true的情况下才进行偏移修正
// 原因同上,即width的值还没有确定
return rules[ALIGN_PARENT_END] != 0;
}
接下来的是获取baselineView和进行偏移修正,仅在wrap_content的情况下需要进行修正,因为此时的width和height值还没有真正确定下来:
// 得到baselineView,这不是我们关注的重点
// ...省略获取baselieView的代码
// 在wrap_content的情况下,确定width值,并根据需要进行偏移修正
if (isWrapContentWidth) {
// Width already has left padding in it since it was calculated by looking at
// the right of each child view
// 下面几行代码确定了RelativeLayout的width值
width += mPaddingRight;
if (mLayoutParams != null && mLayoutParams.width >= 0) {
width = Math.max(width, mLayoutParams.width);
}
width = Math.max(width, getSuggestedMinimumWidth());
width = resolveSize(width, widthMeasureSpec);
// width值确定后,判断是否需要进行偏移修正
if (offsetHorizontalAxis) {
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
final int[] rules = params.getRules(layoutDirection);
// 对center和alignParentRight的view进行修正
// 注意这里并没有对依赖于这个view的其他view做偏移修正
// 也就是说在同一方向上(横向或纵向),如果RelativeLayout是wrap_content
// 依赖于具有center属性或alignParentRight为true的view的依赖是不生效的
if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
centerHorizontal(child, params, width);
} else if (rules[ALIGN_PARENT_RIGHT] != 0) {
final int childWidth = child.getMeasuredWidth();
params.mLeft = width - mPaddingRight - childWidth;
params.mRight = params.mLeft + childWidth;
}
}
}
}
}
// 这部分代码与上面的一样,只是方向变为了垂直方向,因此跳过
if (isWrapContentHeight) {
// ...省略
}
最后是对RelativeLayut的gravity属性进行处理和进行RTL适配,先来看几个相关的初始化值(相关代码在onMeasure方法的前面):
// 获取与水平方向相关的gravity的值
int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
// 如果在水平方向上设置了gravity的值且不为Gavity.START,则horizontalGravity为true
final boolean horizontalGravity = gravity != Gravity.START && gravity != 0;
// 获取与垂直方向相关的gravity的值
gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
// 如果在垂直方向上设置了gravity的值且不为Gavity.TOP,则verticalGravity为true
final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;
来看最后的代码,Gravity.apply做了两件事,第一件事是进行RTL适配,第二件事是设置contentBounds;contentBounds与right,left,bottom和top一样,也是代表了包含所有子view的最小矩形,不同的是,contentBounds是经过gravity处理,使其得到最终的正确位置;也就是说这里将所有的子view看成了一个整体,将这个整体根据gravity的值进行处理,最后得到contentBounds。apply方法里面涉及到较多的位操作,下面会有简单讲解,不过不建议在这过于纠结
// 对于gravity在水平方向上是START且在垂直方向上是TOP,是不需要进行偏移修正的
if (horizontalGravity || verticalGravity) {
// seftBounds是RelativeLayout的自身矩形区域
final Rect selfBounds = mSelfBounds;
selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight,
height - mPaddingBottom);
final Rect contentBounds = mContentBounds;
Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds,
layoutDirection);
// 根据contentBounds得到偏移值,并遍历所有子view进行修正
final int horizontalOffset = contentBounds.left - left;
final int verticalOffset = contentBounds.top - top;
if (horizontalOffset != 0 || verticalOffset != 0) {
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE && child != ignore) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
if (horizontalGravity) {
params.mLeft += horizontalOffset;
params.mRight += horizontalOffset;
}
if (verticalGravity) {
params.mTop += verticalOffset;
params.mBottom += verticalOffset;
}
}
}
}
}
// 还是适配RTL
if (isLayoutRtl()) {
// ...省略
}
setMeasuredDimension(width, height);
Gravity的apply(int gravity, int w, int h, Rect container,Rect outRect, int layoutDirection),调用了两个方法,第一个是getAbsoluteGravity(int gravity, int layoutDirection),用来适配RTL,里面也是涉及了位操作,这里就不进行解释了;来看第二个方法:
// AXIS_PULL_BEFORE与left/top有关,AXIS_PULL_AFTER与right/bottom有关
// 位偏移为AXIS_X_SHIFT(0)代表的是水平方面;位偏移为AXIS_Y_SHIFT(4)代表的是垂直方向
public static void apply(int gravity, int w, int h, Rect container,
int xAdj, int yAdj, Rect outRect) {
// 对gravity在水平方向上的情况进行处理
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
// 水平居中处理
case 0:
outRect.left = container.left
+ ((container.right - container.left - w)/2) + xAdj;
outRect.right = outRect.left + w;
// 如果设置了clip,且outRect超出了container的范围,则将超出的边界设置为container的边界
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
if (outRect.left < container.left) {
outRect.left = container.left;
}
if (outRect.right > container.right) {
outRect.right = container.right;
}
}
break;
// gravity在水平方向的值是left时,默认值是start,对于LTR,也就是left
case AXIS_PULL_BEFORE<<AXIS_X_SHIFT:
// ...略
break;
// gravity在水平方向的值是right时
case AXIS_PULL_AFTER<<AXIS_X_SHIFT:
// ...略
break;
default:
outRect.left = container.left + xAdj;
outRect.right = container.right + xAdj;
break;
}
// 对gravity在垂直方向上的情况进行处理
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
// 垂直居中处理
case 0:
// ...略,与水平居中类似
break;
// gravity在垂直方向的值是top时
case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT:
// ...略
break;
// gravity在垂直方向的值是bottom时
case AXIS_PULL_AFTER<<AXIS_Y_SHIFT:
// ...略
break;
default:
outRect.top = container.top + yAdj;
outRect.bottom = container.bottom + yAdj;
break;
}
}
希望大家能提出一些建议
博主心血来潮阅读并整理RelativeLayout源码。介绍了onMeasure中对子view排序,采用深度优先排序;会对子view进行两次测量,不建议多层嵌套。还分析了applyHorizontalSizeRule、getChildMeasureSpec等方法,以及对gravity属性处理和RTL适配等内容。
1324

被折叠的 条评论
为什么被折叠?



