1. 核心成员与标记
1. LEFT_OF, RIGHT_OF,ABOVE,BELOW: 声明某个子child相对于另外一个子child的位置。
2. ALIGN_BASELINE:某个child和另一个child的基准线对齐。
3. ALIGN_LEFT,ALIGN_TOP,ALIGN_RIGHT,ALIGN_BOTTOM:某个child和另外一个child的对齐方式,左对齐,右对齐,上对齐,下对齐。
4. ALIGN_PARENT_LEFT,ALIGN_PARENT_TOP,ALIGN_PARENT_RIGHT,ALIGN_PARENT_BOTTOM:某个child和RelativeLayout的父容器的对齐方式。左,右,上,下对齐。
5. CENTER_IN_PARENT:child位于parent的正中间布局。
6. CENTER_HORIZONTAL:child位于parent的水平中间布局。
7. CENTER_VERTICAL:child位于parent的垂直中间布局。
8. START_OF,END_OF,ALIGN_START,ALIGN_END, ALIGN_PARENT_START,ALIGN_PARENT_END:相对于上面的left/end, align_left/align_right等考虑了相对方向,也就是从右到左的布局。
9. RULES_VERTICAL:包含了ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM,他的意思是子child之间互相依赖来定位的标记。特指纵向哦。
10. RULES_HORIZONTAL:包含了LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END,他的意思是子child之间需互相依赖来定位的标记,特指横向哦。
11. gravity属性:可以指定子view为left,top, right, bottom等位置摆放。
2. 内部类
-
RelativeLayout.LayoutParams
这是一个给RelativeLayout的子child提供的布局参数。
- mRules: 在LayoutParams的构造函数中记录了left, right, align, start等各种标记过的view元素的id.
- resolveRules(): 将设定的start, end等布局方向相关的属性转换成对应的lef, right。注意的是如果start和left同时设定了,那么优先考虑start.end如right同理。这里还考虑到targetversion是低于17, 那么start, end等类似的属性是无效的,将他们直接转换成left, right.施加的布局方向是没有作用的呢。
- resolveLayoutDirection:给LayoutParams设定布局方向,同时他还会调用上面的resolveRules重新解析Relativelayout的布局策略。
-
RelativeLayout.DependencyGraph
主要目的是为RelativeLayout确定view的定位布局的顺序,即先布局哪个,后布局哪个元素。将这些确定顺序的view放置到一个容器中,这里面有依赖的关系线。
mNodes:记录了所有的view。
mKeyNodes: 和上面的一样记录了所有的view, 但是以key-value的方式,key为view的id。
mRoots: 本意是记录一些不需要依赖其他view的位置来定位自己的view. 经过内部处理后和依赖关系的移除之后,里面就是mNodes中所有的view了。
-
DependencyGraph.findRoots方法
该方法的意图是根据纵向或者横向的依赖标记来找出不依赖其他child定位的child
private ArrayDeque
findRoots(int[] rulesFilter) { final SparseArray keyNodes = mKeyNodes; final ArrayList nodes = mNodes; final int count = nodes.size(); // Find roots can be invoked several times, so make sure to clear // all dependents and dependencies before running the algorithm for (int i = 0; i < count; i++) { final Node node = nodes.get(i); node.dependents.clear(); node.dependencies.clear(); } // Builds up the dependents and dependencies for each node of the graph for (int i = 0; i < count; i++) { final Node node = nodes.get(i); final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams(); final int[] rules = layoutParams.mRules; final int rulesCount = rulesFilter.length; //1.这里是关键的地方,根据rulesFilter选择的标记,来找到当前view依赖的child-view。找到了依赖之后。把当前view添加到他的附属集合里面去,把依赖view添加到当前view的附主集合里面去。 for (int j = 0; j < rulesCount; j++) { final int rule = rules[rulesFilter[j]]; if (rule > 0) { // The node this node depends on final Node dependency = keyNodes.get(rule); // Skip unknowns and self dependencies if (dependency == null || dependency == node) { continue; } // Add the current node as a dependent dependency.dependents.put(node, this); // Add a dependency to the current node node.dependencies.put(rule, dependency); } } } final ArrayDeque roots = mRoots; roots.clear(); //遍历所有的view, 把那些没有附主依赖项的添加到root集合。也就是说这些view可以是根view一样,可以首先定位。 // 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); if (node.dependencies.size() == 0) roots.addLast(node); } return roots; }
-
DependencyGraph.getSortedViews方法
填充mRoots集合,他会将那些依赖其他兄弟view定位的view的陆续填入进去,它会先将被依赖的view放入到root中表示被依赖方已经稳定定位, 然后放入自己,确定这样的定位顺序。如果知道拓扑排序的话,就很容易理解这个过程啦, 拓扑排序就是按照依赖关系去排序,让排在前面的内容不会依赖排在后面的内容,这和我们的mRoot之中的依赖图谱是一致的哦.
void getSortedViews(View[] sorted, int... rules) { //找到第一批不需要依赖其他child定位的view集合。 final ArrayDeque
roots = findRoots(rules); int index = 0; Node node; // 遍历root. while ((node = roots.pollLast()) != null) { final View view = node.view; final int key = view.getId(); //root的节点直接添加进去,他们是无依赖的。 sorted[index++] = view; //获取当前节点的附属项,假如B,C依赖A定位。那么A的的dependents就是B,C final ArrayMap dependents = node.dependents; final int count = dependents.size(); //遍历B, C附属。 for (int i = 0; i < count; i++) { final Node dependent = dependents.keyAt(i); //获取B/C的附主方,这里就是A. final SparseArray dependencies = dependent.dependencies; //移除A附主方,因为A已经sort排好序了,这样B/C就可以说依赖的项就已知了。 dependencies.remove(key); //如果这里的附主方清0了,那么就可以作为root节点添加到root里面了。 //比如B除了依赖A, 可能还依赖D,那么要等下一次定位D之后才能将A-add进去哦。 if (dependencies.size() == 0) { roots.add(dependent); } } } graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL); graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL); - 这两行代码意图是将所有的子child从横纵两个方向来计算view的定位顺序,也即mSortedHorizontalChildren, mSortedVerticalChildren。他里面的元素是按照定位顺序来排列的。
- Node
- dependents: 附属内容,如果b依赖a定位, 那么b是a的附属。a里面的dependents就是b这类元素。
- dependencies: 附主内容,如果b依赖a定位,那么a是b的附主。这个属性集合里面就是b所有依赖的附主方。
- View: 就是当前的节点的view。
3. 核心方法分解
onLayout: 因为RelativeLayout都是子child相对性地布局,所以只要计算出子child的坐标, 就可以知道child放置在什么位置。
sortChildren:生成两个集合,分别是纵行和横向的view集。他们都是RelativeLayout的子child全集,按照定位的顺序,排列成一个集合。后面的测量就从中取出一个个地测量啦。
-
onMeasue
RelativeLayout的核心算法,它分横向,纵行两个方向来测量RelativeLayout和子view, 这也是为什么说relativeLayout相对比较LinearLayout耗费性能, 它的逻辑概括起来分为四步:
- 横向的测量:定位,测量,计算最终位置;
- 纵向的测量: 定位,测量,计算最终位置;
- isWrapContentHeight, 修正高度
- isWrapContentWidth,修正宽度
注意:MeasureSpec.UNSPECIFIED,这个是测量模式在系统中使用构建的时候,一般和0一起组合使用,他表明当前view的测量规格是未知的。父容器没有给我明确的限制所以是UNSPECIFIED,我自己也不知道自己该多大所以是0,具体我的大小到我的具体实现中再去计算吧。这在relativelayout横竖测量的时候,对测量另外一个相对的数值有作用。
if (params.width == LayoutParams.MATCH_PARENT) { //这里解释了为什么当RelativeLayout是wrap的时候,子child是match的时候,得到的高度是精确模式,且尺寸为父容器最大。 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); }
-
onMeasue方法主体
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mDirtyHierarchy) { mDirtyHierarchy = false; //计算纵横两个集合。 sortChildren(); } ...... //横向测量 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(); int[] rules = params.getRules(layoutDirection); applyHorizontalSizeRules(params, myWidth, rules); measureChildHorizontal(child, params, myWidth, myHeight); 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++) { View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); //应用规则,找到child的top或者bottom位置。 applyVerticalSizeRules(params, myHeight); //测量child的宽与高。 measureChild(child, params, myWidth, myHeight); if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { offsetVerticalAxis = true; } //如果是wrap的宽,计算出RelativeLayout的最大宽度1。 if (isWrapContentWidth) { if (isLayoutRtl()) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, myWidth - params.mLeft); } else { width = Math.max(width, myWidth - params.mLeft - params.leftMargin); } } else { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, params.mRight); } else { width = Math.max(width, params.mRight + params.rightMargin); } } } //如果是wrap的高,计算出RelativeLayout的最大高度1。 if (isWrapContentHeight) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { height = Math.max(height, params.mBottom); } else { height = Math.max(height, params.mBottom + params.bottomMargin); } } 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); } } } ....... //wrap的宽,矫正前面positionChildHorizontal返回true的child的left,right属性。 if (isWrapContentWidth) { // Width already has left padding in it since it was calculated by looking at // the right of each child view width += mPaddingRight; if (mLayoutParams != null && mLayoutParams.width >= 0) { width = Math.max(width, mLayoutParams.width); } width = Math.max(width, getSuggestedMinimumWidth()); width = resolveSize(width, widthMeasureSpec); if (offsetHorizontalAxis) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); 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; } } } } } //wrap的高,矫正前面positionChildVertical返回true的child的top,bottom属性。 if (isWrapContentHeight) { // Height already has top padding in it since it was calculated by looking at // the bottom of each child view height += mPaddingBottom; if (mLayoutParams != null && mLayoutParams.height >= 0) { height = Math.max(height, mLayoutParams.height); } height = Math.max(height, getSuggestedMinimumHeight()); height = resolveSize(height, heightMeasureSpec); if (offsetVerticalAxis) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { centerVertical(child, params, height); } else if (rules[ALIGN_PARENT_BOTTOM] != 0) { final int childHeight = child.getMeasuredHeight(); params.mTop = height - mPaddingBottom - childHeight; params.mBottom = params.mTop + childHeight; } } } } } //默认这个都是false,但是如果relativelayout设置了Gravity.END, Gravity.Bottom, //那么就会将内容右边或者底部顶边,这时候就需要矫正child的left, child的right, top, bottom属性呢。因为前面的left, right,top, bottom属性都是从start, top这个顶边位置开始计算的。 if (horizontalGravity || verticalGravity) { 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); final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; if (horizontalOffset != 0 || verticalOffset != 0) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE && child != ignore) { LayoutParams params = (LayoutParams) child.getLayoutParams(); if (horizontalGravity) { params.mLeft += horizontalOffset; params.mRight += horizontalOffset; } if (verticalGravity) { params.mTop += verticalOffset; params.mBottom += verticalOffset; } } } } } if (isLayoutRtl()) { final int offsetWidth = myWidth - width; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); params.mLeft -= offsetWidth; params.mRight -= offsetWidth; } } } //设定RelativeLayout的最终的宽与高。 setMeasuredDimension(width, height); }
getChildMeasureSpec: 这个和viewGroup中的同名方法的实现不一样哦,google工程师这里重新实现了一套逻辑。但是感觉这些逻辑有些乱啊,简单总结下他的基本逻辑。
如果childStart, childEnd都设定了,childSpecMode=EXACTLY, childSpecSize=childEnd-childStart
否则,childSize>0, childSpecMode=EXACTLY, childSpecSize是maxAvailable和childSize中最小的一个。
-
否则,childSize == LayoutParams.MATCH_PARENT:
childSpecMode = MeasureSpec.EXACTLY; childSpecSize = maxAvailable; - 这里也可以看出不关心父容器本身的模式,只要我是MATCH_PARENT,结果就是EXACTLY,这个和viewGroup的计算是大不同的哦。
-
childSize == LayoutParams.WRAP_CONTENT:
childSpecMode = MeasureSpec.AT_MOST; childSpecSize = maxAvailable;
-
applyHorizontalSizeRules:计算child的left或者right位置。
- 找到它依赖定位的view,然后根据rulues规则计算出他的left, 或right的数值。 - 当规则是ALIGN_PARENT_RIGHT时候,计算出的childParams.mRight是根据myWidth来的,这个时候的myWidth并不一定是确定的,如果是RelativeLayout是Wrap_conent,那么在后面确定了width之后要重新再计算一次childParams.mRight。
-
measureChildHorizontal: 测量child的宽度,高度也测量但不是最终的结果。
- 根据params.mLeft,params.mRight或者childWith来计算出child的宽度测量规格childWidthMeasureSpec - 根据myHeight来和params.width的模式得出高度的测量规格childHeightMeasureSpec 注意:这时候的childHeightMeasureSpec时候不准确的,只是为了方便测宽度,大概出的一个值。 - 测量child.
-
positionChildHorizontal:根据left/right以及前面测量的宽度来计算child的位置。
- 因为之前经过了宽度的测量,那么宽度是已知的了,然后通过这个宽度来得出child的left, right的位置。方便后续的layout. - 如果是CENTER_IN_PARENT,CENTER_HORIZONTAL,ALIGN_PARENT_END的三种该方法返回true.表示left,right的位置计算依赖了不确定的父容器,所以等到父容器确定之后需要再定一次left,right.
-
applyVerticalSizeRules:计算child的top或者bottom位置。
- 和measureChildHorizontal同理,计算出纵向的top, bottom
-
measureChild: 测量child的最终的宽与高。
- 得出宽度的计算规格是childWidthMeasureSpec, 宽度其实前面已经计算好了。 - 得出高度的计算规格, 高度是childHeightMeasureSpec,经过前面的定位辅助,以及考虑child的height. - child.measure(childWidthMeasureSpec, childHeightMeasureSpec);最终的测量宽与高。
positionChildVertical:根据left/right以及前面测量的宽度来计算child的位置。
-
有了前面的宽,高,然后就可以定位child, 可以获取到他的top,bottom属性。
- 如果是CENTER_IN_PARENT,CENTER_HORIZONTAL,ALIGN_PARENT_END的三种该方法返回true.表示left,right的位置计算依赖了不确定的父容器,所以等到父容器确定之后需要再定一次top,bottom.
4. 总结
- RelativeLayout的重点在测量处,缺点也是在这里。对于子控件都要横竖测量两遍,这是一个较为耗费性能的地方,所以平时如果可以尽量不要用这个控件哦, 用LinearLayout取代啦。