ViewGroup绘制流程-测量和布局

ViewGroup绘制流程-测量和布局

绘制流程

分三步:

  • onMeasure(): 测量当前控件的大小,在正式布局时提供建议(注意:只是建议,用不用要看onLayout函数)
  • onLayout():对子控件进行布局
  • onDraw():根据布局位置绘图

onMeasure()

 void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

注意参数widthMeasureSpec和heightMeasureSpec,他们实际是MeasureSpec

MeasureSpec

父类对当前view的建议值,由Mode+Size组成

提取Mode和Size

Android提供了方法直接获取

 利用MeasureSpec获取系统建议得到数值和模式
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
Mode分类
  • UNSPECIFIED(未指定):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
  • EXACTLY(完全):父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小
  • AT_MOST(至多):子元素至多达到指定的大小
Mode作用

基本规则:

  • 不管父View是何模式,若子View有确切数值,则子View大小就是其本身大小,且mode是EXACTLY
  • 若子View是match_parent,则模式与父View相同,且大小同父View(若父View是UNSPECIFIED,则子View大小为0)
  • 若子View是wrap_content,则模式是AT_MOST,大小同父View,表示不可超过父View大小(若父View是UNSPECIFIED,则子View大小为0)

以下面XML举例(假设MyFlowLayout的父布局宽高都是match_parent):

<com.topband.viewstudy.demo_12.MyFlowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
   ...
</com.topband.viewstudy.demo_12.MyFlowLayout>

我们宽度定义的是match_parent,对应的模式时EXACTLY,高度定义的是wrap_content,对应的模式时AT_MOST,因此我们在onMeasure()的时候可以直接使用宽度的值,但是我们高度是不确定的所以需要我们自己计算,最后通过setMeasuredDimension()保存计算结果

类似下面代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取父控件的宽高建议值
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        int measureHightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureHightSize = MeasureSpec.getSize(heightMeasureSpec);

        //测量逻辑
        int width = 0;
        int height = 0;
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            width = Math.max(childWidth, width);
            height += childHeight + lp.topMargin + lp.bottomMargin;
        }
        //设置测量建议值
        setMeasuredDimension(
                (measureWidthMode == MeasureSpec.EXACTLY) ? measureWidthSize : width,
                (measureHightMode == MeasureSpec.EXACTLY) ? measureHightSize : height
        );
    }

onLayout()

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int top = 0;
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            top += lp.topMargin;
            child.layout(lp.leftMargin, top, lp.leftMargin + childWidth, top + childHeight);
            top += childHeight + lp.bottomMargin;
        }
    }

这个没啥好讲的, 就是通过layout()函数设置子控件的位置
注意点:
getMeasuredWidth是在measure()过程结束就可以获取到数值,getWidth()是在layout()过程结束后才能获取数值,一般是相同的,如果在layout()中传入的数值和getMeasuredWidth的数值不同,那么结果就不同了,也可以看出onMeasure()只是提供建议

onDraw()

根据布局位置绘图

实现一个FlowLayout

结合上面的知识点,就可以自定义一个类似FlowLayout效果了
先看效果:
ViewGroup绘制流程-测量和布局

1.测量

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //1 利用MeasureSpec获取系统建议得到数值和模式
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        //2 计算FlowLayout占用空间大小
        int lineWidth = 0;//记录每一行的宽度
        int lineHeight = 0;//记录每一行的高度
        int width = 0;//记录整个FlowLayout宽度
        int height = 0;//记录整个FlowLayout高度

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //测量子view
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            //处理换行
            if (lineWidth + childWidth > measureWidth) {
                width = Math.max(lineWidth, childWidth);
                height += lineHeight;
                lineWidth = childWidth;
                lineHeight = childHeight;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            //处理最后一行
            if (i == count - 1) {
                width = Math.max(lineWidth, childWidth);
                height += lineHeight;
            }

        }
        //3 设置计算结果
        setMeasuredDimension(
                (measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
                (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
        );
    }

2.布局

    /**
     * 布局所有子控件
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int lineWidth = 0;//行的宽
        int lineHeight = 0;//行的高
        int left = 0;
        int top = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (childWidth + lineWidth > getMeasuredWidth()) {
                left = 0;
                top += lineHeight;
                //换行后重新初始化
                lineHeight = childHeight;
                lineWidth = childWidth;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            //子控件布局
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();
            child.layout(lc, tc, rc, bc);
            //移动到下一个子控件起点
            left += childWidth;
        }
    }

3.注意点

获取margin需要重写

    /***
     * 提取margin值
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

  //使用时强转成MarginLayoutParams,就可以读取xml中的属性值了 
  MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

完整代码

/**
 * 作者: chenhao
 * 创建日期: 2020-06-23
 * 描述:自定义flowLaout
 */
public class MyFlowLayout extends ViewGroup {
    public MyFlowLayout(Context context) {
        super(context);
    }

    public MyFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //1 利用MeasureSpec获取系统建议得到数值和模式
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        //2 计算FlowLayout占用空间大小
        int lineWidth = 0;//记录每一行的宽度
        int lineHeight = 0;//记录每一行的高度
        int width = 0;//记录整个FlowLayout宽度
        int height = 0;//记录整个FlowLayout高度
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //测量子view
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            //处理换行
            if (lineWidth + childWidth > measureWidth) {
                width = Math.max(lineWidth, childWidth);
                height += lineHeight;
                lineWidth = childWidth;
                lineHeight = childHeight;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            //处理最后一行
            if (i == count - 1) {
                width = Math.max(lineWidth, childWidth);
                height += lineHeight;
            }

        }
        //3 设置计算结果
        setMeasuredDimension(
                (measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
                (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
        );
    }

    /**
     * 布局所有子控件
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int lineWidth = 0;//行的宽
        int lineHeight = 0;//行的高
        int left = 0;
        int top = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (childWidth + lineWidth > getMeasuredWidth()) {
                left = 0;
                top += lineHeight;
                //换行后重新初始化
                lineHeight = childHeight;
                lineWidth = childWidth;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            //子控件布局
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();
            child.layout(lc, tc, rc, bc);
            //移动到下一个子控件起点
            left += childWidth;
        }
    }

    /***
     * 提取margin值
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }
}
上一篇:布局ViewGroup原理解析(三):RelativeLayout


下一篇:android中ViewGroup的addViewInLayout()的行为