RelativeLayout的onMeasure源码详细解析

博主心血来潮阅读并整理RelativeLayout源码。介绍了onMeasure中对子view排序,采用深度优先排序;会对子view进行两次测量,不建议多层嵌套。还分析了applyHorizontalSizeRule、getChildMeasureSpec等方法,以及对gravity属性处理和RTL适配等内容。

转载请注明出处: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;
        }
    }

希望大家能提出一些建议

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值