元老控件之RelativeLayout源码分析

再过几个月就要30了,给自己定了一个小目标, 三年后会是怎样呢......
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<Node> findRoots(int[] rulesFilter) {
                  final SparseArray<Node> keyNodes = mKeyNodes;
                  final ArrayList<Node> 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<Node> 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<Node> 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<Node, DependencyGraph> 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<Node> 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耗费性能, 它的逻辑概括起来分为四步:

    1. 横向的测量:定位,测量,计算最终位置;
    2. 纵向的测量: 定位,测量,计算最终位置;
    3. isWrapContentHeight, 修正高度
    4. 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取代啦。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342