Android中View测量、布局及绘制原理

一、View绘制的流程框架

这里写图片描述
View的绘制是从上往下一层层迭代下来的。DecorView–>ViewGroup(—>ViewGroup)–>View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。
这里写图片描述

二、Measure流程

顾名思义,就是测量每个控件的大小。

调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用
setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

}

measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec,heightMeasureSpec这两个参数信息怎么获得?

如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

MeasureSpec的确定

先介绍下什么是MeasureSpec?
这里写图片描述
MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。

其中,Mode模式共分为三类

UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

那么MeasureSpec又是如何确定的???

子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。
这里写图片描述

我们来看getChildMeasureSpec()的源码分析:

//作用:
/ 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
//即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性 


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

 //参数说明
 * @param spec 父view的详细测量值(MeasureSpec) 
 * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
 * @param childDimension 子视图的布局参数(宽/高)

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     

    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  

    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  

    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


    // 当父view的模式为EXACITY时,父view强加给子view确切的值
   //一般是父view设置为match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0,即有确切的值  
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
    // 多见于ListView、GridView  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}

关于getChildMeasureSpec()里对于子View的测量模式和大小的判断逻辑有点复杂;
现总结如下表:
这里写图片描述
规律总结:(以子View为标准,横向观察)

  • 当子View采用具体数值(dp / px)时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;

  • 当子View采用match_parent时,子View的测量模式与父容器的测量模式一致,若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间

  • 当子View采用wrap_parent时,无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

这里写图片描述

从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。

关于MeasureSpec和View的Measure过程还可以看这篇文章

三、Layout流程

测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

layout()作用:确定View本身的位置,即设置View本身的四个顶点位置
源码分析如下:(仅贴出关键代码)

public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  

    // setFrame() / setOpticalFrame():确定View的位置
    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  (具体请看下面源码分析)
      boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果视图的大小和位置发生变化,会调用onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

        // onLayout():确定该View所有的子View在父容器的位置
        // 由于单一View是没有子View的,所以onLayout()是一个空实现(
        onLayout(changed, l, t, r, b);  
  ...

}

上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

protected void onLayout(boolean changed, int left, int top, intright, int bottom) {

}

如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

四、Draw过程

View的绘制过程遵循如下几步:

①绘制背景 background.draw(canvas)
②绘制自己(onDraw)
③绘制Children(dispatchDraw)
④绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序:

public void draw(Canvas canvas) {

// 特别注意:
// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
    ...

    /*
     * 绘制过程如下:
     *   1. 绘制view背景
     *   2. 绘制view内容
     *   3. 绘制子View
     *   4. 绘制装饰(渐变框,滑动条等等)
     */

    int saveCount;
    if (!dirtyOpaque) {
          // 步骤1: 绘制本身View背景
        drawBackground(canvas);
    }

        // 如果有必要,就保存图层(还有一个复原图层)
        // 优化技巧:
        // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
        // 因此在绘制的时候,节省 layer 可以提高绘制效率
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {

        if (!dirtyOpaque) 
             // 步骤2:绘制本身View内容
            onDraw(canvas);
        //  View 中:默认为空实现
        // ViewGroup中:自定义View时需要进行复写!!!!

..
        // 步骤3:绘制子View
        dispatchDraw(canvas);
       // 由于单一View没有子View,所以View 中:默认为空实现


        ...

        // 步骤4:绘制滑动条和前景色等等
        onDrawScrollBars(canvas);

        // we're done...
        return;
    }
    ...    
}

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

dispatchDraw()作用遍历子View并绘制

// 仅贴出重要代码

// 特别注意:
// ViewGroup中:由于 系统 已经为我们实现了该方法,所以我们一般都不需要重写该方法
// View中默认为空实现(因为没有子View可以去绘制)
protected void dispatchDraw(Canvas canvas) {
    ......

// 遍历子View
    final int childrenCount = mChildrenCount;
    ......

    for (int i = 0; i < childrenCount; i++) {
            ......
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
              // 绘制视图
              // 继续看下面源码分析
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            ......
    }
}


// drawChild()源码分析
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    // 最终还是调用了子 View 的 draw ()进行子View的绘制
    return child.draw(canvas, this, drawingTime);
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值