View的onMeasure()、onLayout()、onDraw()总结

自定义View是android开发中常有的一项工作,要想自定义View,你就必须熟练掌握View的测量、布局及绘制,了解其原理并会使用。

View视图绘制需要搞清楚两个问题,一个是从哪里开始绘制,一个是怎么绘制?

先说从哪里开始绘制的问题:我们平常在使用Activity的时候,都会调用setContentView来设置布局文件,没错,视图绘制就是从这个方法开始的;

再来说说怎么绘制的:

在我们的Activity中调用了setContentView之后,会转而执行PhoneWindow的setContentView,在这个方法里面会判断我们存放内容的ViewGroup(这个ViewGroup可以是DecorView也可以是DecorView的子View)是否存在。不存在的 话则会创建一个DecorView出来,并且会创建出相应的窗体风格,存在的话则会删除原先ViewGroup上面已有的View,接着会调用 LayoutInflater的inflate方法以pull解析的方式将当前布局文件中存在的View通过addView的方式添加到 ViewGroup上面来,接着在addView方法里面就会执行我们常见的invalidate方法了,这个方法不只是在View视图绘制的过程中经常 用到,其实动画的实现原理也是不断的调用这个方法来实现视图不断重绘的,执行这个方法的时候会调用他的父View的invalidateChild方法, 这个方法是属于ViewParent的,ViewGroup以及ViewRootImpl中都对他进行了实现,invalidateChild里 面主要做的事就是通过do while循环一层一层计算出当前View的四个点所对应的矩阵在ViewRoot中所对应的位置,那么有了这个矩阵的位置之后最终都会执行到 ViewRootImpl的invalidateChildInParent方法,执行这个方法的时候首先会检查当前线程是不是主线程,因为我们要开始准 备更新UI了,不是主线程的话是不允许更新UI的,接着就会执行scheduleTraversals方法了,这个方法会通过handler来执行 doTraversal方法,在这个方法里面就见到了我们平常所熟悉的View视图绘制的起点方法performTraversals了;

一、总体流程

视图的测量、布局、绘制都是按照视图树从上到下的,大致可分为DecorView-->ViewGroip-->View 这样三个层级

当Activity对象被创建完成,会将DecorView添加到Window中(显示),同时创建ViewRoot的实现对象ViewRootImpl与之关联。ViewRootImpl会调用performTraversals来进行View的绘制过程。经过measure,layout,draw三个流程才能完成一个View的绘制过程,分别是用于测量宽、高;确定在父容器中的位置;绘制在屏幕上三个过程。而measure方法会调用onMeasure函数,这其中又会调用子元素的measure函数,如此反复就能完成整个View树的遍历过程。其他两个流程也同样如此。

         measure决定了View的宽和高,测量之后就可以根据getMeasuredWidth和getMeasuredHeight来获取View测量后的宽和高,几乎等于最终的宽和高,但有例外;layout过程决定了View四个顶点的位置和实际的宽和高,完成之后可以根据getTop,getBottom,getLeft,getRight来获得四个顶点的位置,并且可以使用getWidth和getHeight来获取实际的宽和高;draw过程就决定了View的显示,完成draw才能真正显示出来。

二、onMeasure()

我们先来看看方法

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

里面有两个重要的参数 一个是widthMeasureSpec,一个是heightMeasureSpec,他们叫做MeasureSpec,它是系统将View的参数根据父容器的规则转换而成的,之后根据它来测量出View的宽和高。它实际上是一个32位的int值,高二位表示SpecMode,就是测量的模式;低30位表示SpecSize,即在某种测量模式下的规格大小。

MeausureSpec有三种模式,常用的由两种:EXACTLY和AT_MOST。EXACTLY表示父容器已经检测出View所需要的精确大小(即父容器根据View的参数已经可以确定View的大小了),这时View的最终大小就是SpecSize的值,它对应于View参数中的match_parent和具体大小数值这两种模式;AT_MOST表示父容器指定了一个可用大小的数值,记录在SpecSize中,View的大小不能大于它,但具体的值还是看View的具体实现。它对应于View参数中的wrap_content。

DecorView的测量由窗口的大小和自身的LayoutParams决定,具体逻辑由getRootMeasureSpec决定,如果是具体值或者是match_parent,就是精确模式;如果是wrap_content就是最大模式;普通View的measure实际上是由父元素进行调用的(遍历),父元素调用child的measure之前使用getChildMeasureSpec来转换得到子元素的MeasureSpec(具体代码:艺术探索P180-181),总结而来就是与自身的参数以及父元素的SpecMode有关:1、如果View的参数是具体值,那么不管父元素的Mode是什么,子元素的Mode都是精确模式并且大小就是参数的大小;2、如果View的参数是match_parent,如果父元素的mode是精确模式那么View也是精确模式并且大小是父元素剩余的大小;如果父元素的mode是最大模式,那么View也是最大模式;3、如果View的参数是wrap_content,那么View的模式一定是最大化模式,并且不能超过父容器的剩余空间。看图,清楚!

View自身的onMeasure方法就是把MeasureSpec的Size设为最终的测量结果,这样的测量问题就是match_parent和wrap_content是一样的结果(因为wrap_content的Size是最大可用Size),所以如果自定义View直接继承自View,就需要对wrap_content进行处理,ImageView等都对wrap_content进行了特殊处理。

ViewGroup的measure过程:

         ViewGroup不同于View,它是一个抽象类,没有实现onMeasure方法(因为具体的Layout布局特性各不相同),但它measure时会遍历children调用measureChild,执行getChildMeasureSpec进行子元素的MeasureSpec创建,创建过程之前已经了解了,就是利用自身的Spec与子元素参数进行创建。

三、onLayout()

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在父容器的位置
        onLayout(changed, l, t, r, b);
    }
    ...
}

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);
}

layout的作用是ViewGroup来确定子元素的位置,当ViewGroup的位置被确定了之后,它就在自己的onLayout函数中遍历所有的子元素并调用其layout方法,确定子元素的位置,对于子元素View,layout中又会调用其onLayout函数。View和ViewGroup中都没有真正实现onLayout方法。但View和ViewGroup的layout方法是一致的,作用都是用于确定自己的位置,layout方法会调用setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom四个值,这样就确定了View在父元素中的位置。

问题:getMeasuredHeight和getHeight有什么区别(同Width)?

在measure之后就可以使用getMeasuredHeight来进行获取测量的宽和高,而layout过程是晚于measure的,ViewGroup的setChildFrame会调用child的layout来确定child的真实位置,源代码中也可以看出layout的bottom和top就是利用getMeasuredHeight和getMeasuredWidth来计算的,所以说如果child的layout不重写,那么就是一样的!如果child的layout函数被重写,就会有不一样的结果。

—》getMeasuredHeight是控件的实际高度,与屏幕无关。

而onDraw则是在view绘画的时候使用的。 getHeight既然是在绘画的时候调用,那么必然是显示多少绘画多少,所以这个高度会随着view在屏幕的显示情况来onDraw,所以getHeight是随着view在屏幕的显示而不同的。

—》getHeight得到的是view的显示高度,跟view在屏幕的显示情况有关。

四、onDraw()

public void draw(Canvas canvas) {
    // 所有的视图最终都是调用 View 的 draw ( ) 绘制视图( ViewGroup 没有复写此方法)
    // 在自定义View时, 不应该复写该方法, 而是复写 onDraw(Canvas) 方法进行绘制。
    // 如果自定义的视图确实要复写该方法, 那么需要先调用 super.draw(canvas)完成系统的绘制, 然后再进行自定义的绘制。
    ...
    int saveCount;
    if (!dirtyOpaque) {
        // 步骤1: 绘制本身View背景
        drawBackground(canvas);
    }
    // 如果有必要, 就保存图层( 还有一个复原图层)
    // 优化技巧:
    // 当不需要绘制 Layer 时, “保存图层“和“复原图层“这两步会跳过
    // 因此在绘制的时候, 节省 layer 可以提高绘制效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {
        if (!dirtyOpaque)
            // 步骤2: 绘制本身View内容 默认为空实现, 自定义View时需要进行复写
            onDraw(canvas);
        ......
        // 步骤3: 绘制子View 默认为空实现 单一View中不需要实现, ViewGroup中已经实现该方法
        dispatchDraw(canvas);
        ........
        // 步骤4: 绘制滑动条和前景色等等
        onDrawScrollBars(canvas);
        ..........
        return;
     }
     .......

}

View的绘制会分为四步:

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

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

OnDraw中进行绘制自己的操作。使用canvas进行绘制等等,简单来说就是利用代码画自己需要的图形。

绘制过程中的旋转以及save和restore

Android中的旋转rotate旋转的是座标系,也就是说旋转画布实际上画布本身的内容是不动的,不会直接把画布上已经存在的内容进行移动,只是旋转座标系,这样旋转之后的操作全部是针对这个新的座标系的。

save就是保存当前的的座标系,之后再调用restore时,座标系复原到save保存的状态,这两个函数restore的次数不能大save,否则会引发异常。

常用优化技巧:

1.onDraw()中不要创建新的局部对象

onDraw()会被频繁调用,如果方法内部创建了局部对象,则会一瞬间产生大量的临时对象,这使得占用过多内存,系统频繁GC,降低了程序执行效率。

2.避免onDraw()中执行大量耗时操作

View的最佳绘制频率为60fps,因为LCD的频率是60Hz,显示每一帧的间隔是16ms,所以每一个VSync信号的时间间隔是16ms,接收到该信号时视图会进行刷新,如果你绘画时间过长就会导致View绘制不流畅,可以使用多线程来解决。

3.避免Overdraw

在同一个地方绘制多次肯定是浪费资源的,也避免浪费资源去渲染那些不必要和看不见的背景。你可以在手机的开发者设置中开启起调试GPU过度绘制选项来查看视图绘制的情况。

 

五、总结

从View的测量、 布局和绘制原理来看, 要实现自定义View, 根据自定义View的种类不同, 可能分别要自定义实现不同的方法。 但是这些方法不外乎: onMeasure()方法, onLayout()方法, onDraw()方法。
onMeasure()方法: 单一View, 一般重写此方法, 针对wrap_content情况, 规定View默认的大小值, 避免于match_parent情况一致。 ViewGroup, 若不重写, 就会执行和单子View中相同逻辑, 不会测量子View。 一般会重写onMeasure()方法, 循环测量子View。
onLayout()方法:单一View, 不需要实现该方法。 ViewGroup必须实现, 该方法是个抽象方法, 实现该方法, 来对子View进行布局。View测量、 布局及绘制原理
onDraw()方法: 无论单一View, 或者ViewGroup都需要实现该方法, 因其是个空方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章