Android布局原理探究

MeasureSpc类说明

MeasureSpec类的职责是把view的with,height和SpecMode封装成两个32位的整数,同时它也提供了把32位整数解包成相应的with和with对应的SpecMode或者是height和height对应的SpecMode。SpecMode有三种模式:

  • UNSPECIFIED(未指定):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;
  • EXACTLY(完全):父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小;它对应于LayoutParams中的match_parent和具体的数值这两种模式。
  • AT_MOST(最多):子元素至最多达到指定大小的值。它对应于LayoutParams中的Wrap_content

android如何绘制view

当Activity获取到焦点的时候,它就会去请求绘制整个android界面,framework会处理绘制过程,但是Activity必须要提供树结点,当调用者在setConentView的时候,android系统会根据xml生成相应的view,然后把view添加到Activity的树结点,如下图

从Activity的ViewRoot开始绘制整个view树的,绘制的过程就是遍历整个view树和绘制每个view的有效区域。ViewGroup的职责是负责去帮助它的子view去请求ondraw方法,而每个view的职责就是渲自身的内容。例如TextView(1)的职责是负责绘制自身的内容,而RelativeLayout(2)的职责是帮助ImageView和TextView(2)的渲染工作。从图中可以看出view的渲染顺序是自上而下的,就是说ViewRoot先渲染,然后再到它的子view.

在这里需要说明的是框架不会渲染不在显示区或的view,并且它也为你提供后台绘制视图的功能。通过调用invalidate(),你可以强制绘制视图

绘制布局分成两个过程:onMeasure和onLayout,onMeasure负责测量view的尺寸,如果有下一层的view,当前的view会把自己的测量值传到下一层的view中,这样不断的往下传下去测量出整个view树的大小;而onLayout也是自上而下的一个流程,通过onLayout来设置每个子view的大小和位置

当视图的onMeasure方法执行后,它的getMeasuredWidth()和getMeasuredHeight()必定是己经设置了的,视图测量后它的宽度和高度必须是受到父视图的约束,这就保证了测量结束时,所有的父视图可以接收子视图的测量值。举个例子,当父类使用了unspecified方式去布局时父类为了确定子类的大小就需要调用一次onMeasuere方法,然后父类会再次调用onMeasure方法来避免子类太大或者是太小(也就是说,如果子视图不满意它们获得的区域大小,那么父视图将会干涉并设置第二次测量规则)。

测量过程中使用了两个类来传递尺寸。ViewGroup.LayoutParams类是子view告诉父view子view应该测量子子view和怎样放置子view,ViewGroud的LayoutParams类只描述了视图的尺寸,它可以有以下的值:

  1. 具体的数值
  2. WRAP_CONTENT-适配显示的内容的大小
  3. MATCH_PARENT-和父view的大小一样大

如果想自定义view可以继承ViewGroup,并重写generateLayoutParams方法,提供自己的LayoutParams来砍认view放置的规则。为了说明View的测量过程,我们以RelativeLayout作为例子来分析RelativeLayout的布局过程。

RelativeLayout的布局过程分析

由于onMeasure代码比较多,我们把onMeasure分成几个部份一起分析,首先我们先看下水平方向的测量。applyHorizontalSizeRules方法负责测量水child的水平方向的位置,measureChildHorizontal则负责测量view的宽度,具体看以下代码引用的注释

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int myWidth = -1;
      int myHeight = -1;
    
      int width = 0;
      int height = 0;
    
      final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
      final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
      final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
      final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
      // 如果不是UNSPECIFIED模式 则将widthSize赋值于myWidth
      if (widthMode != MeasureSpec.UNSPECIFIED) {
          myWidth = widthSize;
      }
      // 如果不是UNSPECIFIED模式 则将heightSize赋值于myHeight
      if (heightMode != MeasureSpec.UNSPECIFIED) {
          myHeight = heightSize;
      }
      //如果是EXACTLY模式 则将myWidth和myHeight记录
      if (widthMode == MeasureSpec.EXACTLY) {
          width = myWidth;
      }
    
      if (heightMode == MeasureSpec.EXACTLY) {
          height = myHeight;
      }


    ....................................初始化代码....................................

    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);
            
            // 测量view的水平方向的位置(view的左右位置)
            applyHorizontalSizeRules(params, myWidth, rules);
            // 测量view的宽度
            measureChildHorizontal(child, params, myWidth, myHeight);

            if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                offsetHorizontalAxis = true;
            }
        }
    }
    
    ....................................待分析代码....................................
}

/**
 * 最终计算出child view距离左边和右边的边距
 */
private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
    RelativeLayout.LayoutParams anchorParams;
    
    // 获取到childview的toLeftOf所对应的view的LayoutParams
    anchorParams = getRelatedViewParams(rules, LEFT_OF);
    if (anchorParams != null) {
        // 计算child view的右边的位置
        childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
                childParams.rightMargin);
        // child view如果有layout_alignWithParentIfMissing和layout_toLeftOf
        // 相对参考值为空时,就以当前的Relative的边界为参考,即child的
        // toLeftof对应的view为visible为gone时,就以当前类的右边界为参考点,
        // 即child view会居右显示
    } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
        if (myWidth >= 0) {
            // 计算child view的右边的位置
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }

    // 获取toRightOf所对应的view的LayoutParams
    anchorParams = getRelatedViewParams(rules, RIGHT_OF);
    if (anchorParams != null) {
        // 计算child view的左边的位置
        childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin +
                childParams.leftMargin);
        // child view如果有layout_alignWithParentIfMissing和layout_toRightOf
        // 相对参考值为空时,就以当前的Relative的边界为参考,即child的
        // toRightof对应的view为visible为gone时,就以当前类的左边界为参考点,
        // 即child view会居左显示
    } else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) {
        // 居左显示
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }
    
    // child view的layout_alignLeft左边和参照的view的左边对齐
    anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);
    if (anchorParams != null) {
        childParams.mLeft = anchorParams.mLeft + childParams.leftMargin;
    } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) {
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }
    
    // child view的右边和参照view的右边对齐
    anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT);
    if (anchorParams != null) {
        childParams.mRight = anchorParams.mRight - childParams.rightMargin;
    } else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) {
        if (myWidth >= 0) {
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }
    
    // layout_alignParentLeft优先级最高,如果有值就直接居左
    if (0 != rules[ALIGN_PARENT_LEFT]) {
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }

    // layout_alignParentRight优先级最高,如果有值就直接居右
    if (0 != rules[ALIGN_PARENT_RIGHT]) {
        if (myWidth >= 0) {
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }
}

/**
 * 计算出view的水平宽度
 */
private void measureChildHorizontal(
        View child, LayoutParams params, int myWidth, int myHeight) {
    // 计算出child view的宽度
    final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
            params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
            myWidth);

    final int childHeightMeasureSpec;
    // mAllowBrokenMeasureSpecs是对4.2版本以下的系统的兼容,在这里不介绍
    // 如果小于0,证明是WRAP_CONTENT或者是MATCH_PARENT
    if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
        if (params.height >= 0) {
            // child view有具体的高度值,把值包装成32位的整数
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    params.height, MeasureSpec.EXACTLY);
        } else {
            // Negative values in a mySize/myWidth/myWidth value in
            // RelativeLayout measurement is code for, "we got an
            // unspecified mode in the RelativeLayout's measure spec."
            // Carry it forward.
            // child view没有指定高度,子类可以得到想要的高度值
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
    } else {
        final int maxHeight;
        // mMeasureVerticalWithPaddingMargin是大于等于4.3时的支持
        // 这里计算出最大的高度值
        if (mMeasureVerticalWithPaddingMargin) {
            maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
                    - params.topMargin - params.bottomMargin);
        } else {
            maxHeight = Math.max(0, myHeight);
        }

        // 计算出height mode
        final int heightMode;
        if (params.height == LayoutParams.MATCH_PARENT) {
            heightMode = MeasureSpec.EXACTLY;
        } else {
            heightMode = MeasureSpec.AT_MOST;
        }
        // 把高度和mode合起来生成一个32位的整数
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
    }

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

由于垂直方向的测量过程和水平方向的测量过程是一样的,在这里不会做详细的分析,有兴趣的童鞋可以自行查阅相应的代码。

下面我们测量出所有child view的上下左右四个位置后, 我们就可以测出当前当前RelativeLayout的真实宽度,知道真实宽度后就可以对依懒RelativeLayout的child view的布局进行重新修正,详细请看以下的代码

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 计算出了当前view的真实宽度后,需要重新去设置那些基于父类布局的view的水平方向的真实位置
    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++) {
                final View child = views[i];
                if (child.getVisibility() != GONE) {
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    final int[] rules = params.getRules(layoutDirection);
                    // 如果child view是否有用到水平布局方式,就重新计算它的左边水边的值
                    if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
                        centerHorizontal(child, params, width);
                    } else if (rules[ALIGN_PARENT_RIGHT] != 0) {
                        // 居右显示时,重新计算child view的左右边缘
                        final int childWidth = child.getMeasuredWidth();
                        params.mLeft = width - mPaddingRight - childWidth;
                        params.mRight = params.mLeft + childWidth;
                    }
                }
            }
        }
    }

    // 计算出了当前view的真实高度后,需要重新去设置那些基于父类布局的view的垂直方向的真实位置
    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++) {
                final View child = views[i];
                if (child.getVisibility() != GONE) {
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    final int[] rules = params.getRules(layoutDirection);
                    // 判断child view是否有垂直方向的布局方式,如果有就重新修正布局方式
                    if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
                        centerVertical(child, params, height);
                        判断child view是否是在RelativeLayout的底部,如果是重新计算child的上下边缘
                    } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
                        final int childHeight = child.getMeasuredHeight();
                        params.mTop = height - mPaddingBottom - childHeight;
                        params.mBottom = params.mTop + childHeight;
                    }
                }
            }
        }
    }

    // 根据当前RelativeLayout的gravity来修正child view的上下左右的位置
    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);
                
        // 根据gravity计算上下左右的水平修正值
        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;
                    }
                }
            }
        }
    }
    
    // 最后调用setMeasuredDimension设置当前RelativeLayout的宽度和高度,最后可以通过getMeasureHeight和getMeasureWidth可以拿到
    // RelativeLayout的真实高度
    setMeasuredDimension(width, height);
}
复制代码

最后我们看下onLayout方法,onLayout方法很简单,只是把LayoutParams的上下左右的位置传到字类中来确定子view的位置

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //  The layout has actually already been performed and the positions
    //  cached.  Apply the cached values to the children.
    final int count = getChildCount();

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            RelativeLayout.LayoutParams st =
                    (RelativeLayout.LayoutParams) child.getLayoutParams();
            child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
        }
    }
}
复制代码

RelativeLayout的布局总结

从源码我们可以看到,Relative在布局的过程中至少是调用了所有child的onMeasure方法两次,而在布局的过程中也会涉及到view的排序,相对位置的计算,这样就导致relativeLayout在布局过程中耗时比较长的原因,所以如果非必机的情况下不要用RelativeLayout。

综上所述View的测量过程是先通过onMeasure测量出所有view的位置和大不小,然后通过onLayout来把view放到相应的位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值