View绘图原理总结

基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法(回调函数)。

整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘(draw)。

View绘制流程图

其中measure、layout、draw可以看做动词,measure(测量大小)后才能进行layout(确定位置),然后才能draw(绘制),他们都是public的方法,前两个还是final的,根本没打算被子类继承。真正有变数的是on...回调函数,继承View(ViewGroup)时可以Override,on可以理解为当...时。

具体流程如下:

1、measure

用于计算View Tree的大小,即视图的宽度和长度,回调onMeasure函数。

// View.java中measure的函数原型
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        // first clears the measured dimension flag
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;

        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
        }

        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

onMeasure函数需要两个参数:widthMeasureSpec和heightMeasureSpec(width and height measure specifications),这是两个int值,包含两部分:mode和size(高两位表示mode,低30位表示size,具体参见MeasureSpec类),在onMeasure计算出的widht和height应该尽量满足measure specifications,否则父容器可以选择如clipping,scrolling或者抛出异常,或者(也许是用新的specifications)再次调用onMeasure()。

一但width和height计算好了,就应调用View.setMeasuredDimension(int measuredWidth, int measuredHeight)方法对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,否则将导致抛出异常,而measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,一旦这两个变量被赋值,则意味着该View的测量工作结束。

// onMeasure的函数原型
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// setMeasuredDimension的函数原型
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= MEASURED_DIMENSION_SET;
}

View默认的onMeasure函数实现就只调用了一个setMeasureDimension函数,重载的话(ViewGroup必须重载实现onMeasure函数)大致流程应该如下所示:

// 重载的onMeasure过程
private void onMeasure(int widthMeasureSpec , int heightMeasureSpec) {
    // 设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)
    // 1、该方法必须在onMeasure调用,否者报异常。
    setMeasuredDimension(w, h)

    // 2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程
    int childCount = getChildCount()

    for(int i=0; i < childCount; i++) {
        // 2.1、获得每个子View对象引用
        View child = getChildAt(i)

        // 整个measure()过程就是个递归过程
        // 该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法
        measureChildWithMargins(child, h, i)

        // 其实,对于我们自己写的应用来说,最简单的办法是去掉框架里的该方法,直接调用view.measure(),如下:
        // child.measure(h, l)
    }
}

// measureChildWithMargins具体实现在ViewGroup.java里。
protected  void measureChildWithMargins(View v, int height, int width) {
    v.measure(h, l)
}

在自定义的View中需要重载onMeasure函数的话,关于MeasureSpec可能需要如下代码:

if (specMode == MeasureSpec.EXACTLY) {
    // We were told how big to be
    result = specSize;
} else { // MeasureSpec.UPSPECIFIED
    // Measure the text
    result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
            + getPaddingRight();
    if (specMode == MeasureSpec.AT_MOST) {
        // Respect AT_MOST value if that was what is called for by measureSpec
        result = Math.min(result, specSize);
    }
}

2、layout

用于设置视图在屏幕中显示的位置,有两个基本操作(1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;(2)onLayout(),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;

layout函数原型,位于View.java

/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
public final void layout(int l, int t, int r, int b) {
    boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }

        onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局
        mPrivateFlags &= ~LAYOUT_REQUIRED;
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}

如果该View是个ViewGroup类型,需要遍历每个子视图childView,调用该子视图的layout()方法去设置它的坐标值。

//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void onLayout(int left, int top, right, bottom) {

    //如果该View不是ViewGroup类型
    //调用setFrame()方法设置该控件的在父视图上的坐标轴

    setFrame(l ,t , r ,b) ;
    //--------------------------

    //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程
    int childCount = getChildCount() ;

    for(int i=0 ;i<childCount ;i++){
        //2.1、获得每个子View对象引用
        View child = getChildAt(i) ;
        //整个layout()过程就是个递归过程
        child.layout(l, t, r, b) ;
   }
}

3、draw

利用前两部得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些"需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。有三个基本操作(1)绘制背景;(2)如果要视图显示渐变框,这里会做一些准备工作;(3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,每个View都需要重载该方法。而ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法。(4)绘制子视图,即dispatchDraw()函数。在view中不需要重载该方法,值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。(5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;(6)绘制滚动条;

//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void draw(Canvas canvas){
    //该方法会做如下事情
    //1 、绘制该View的背景
    //2、为绘制渐变框做一些准备操作
    //3、调用onDraw()方法绘制视图本身
    //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。
        // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。
    //5、绘制渐变框	
}

//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
@Override
protected void dispatchDraw(Canvas canvas) {
    //其实现方法类似如下:
    int childCount = getChildCount() ;

    for(int i=0 ;i<childCount ;i++){
        View child = getChildAt(i) ;
        //调用drawChild完成
        drawChild(child,canvas) ;
    }	   
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
protected void drawChild(View child,Canvas canvas) {
    // ....
    //简单的回调View对象的draw()方法,递归就这么产生了。
    child.draw(canvas) ;

    //.........
}

从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法,自定义viewGroup的时候需要最少覆写onMeasure()和onLayout()方法。

整个界面的更新依次执行measure、layout、draw操作,从ViewRoot按照从根到叶子的顺序绘制view。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值