美文网首页
View的绘制过程

View的绘制过程

作者: EmanLu | 来源:发表于2016-11-30 17:22 被阅读51次

最近在学习View的绘制流程,这里就把学习到的内容做个记录吧。

首先,View的绘制基本上是测量、布局和绘制三个步骤。而View对应这些步骤有measure()、layout()和draw()三个方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

“测量视图及其内容,以确定测量的宽度和测量的高度。”,注意这个方法是final的,不可以重写。

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

“分配一个视图和它的所有子View的大小和位置。”

public void draw(Canvas canvas)

“手动将此视图(和所有的子视图)渲染给给定的画布。”,注意视图必须在这个函数被调用之前已经做了一个完整的布局,也就是在layout完成后才可以调用。

新建一个继承ImageView的自定义View,代码如下:

public class MyImageView extends ImageView{

   ...

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d("MyImageView", "onMeasure");
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d("MyImageView", "OnLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("MyImageView", "onDraw");
    }
}

再布局文件中使用这个自定义ImageView,运行,可以看到Log:

Log打印结果截图

这里可以看到视图绘制过程中,三个方法的执行顺序是:
onMeasure() → onLayout() → onDraw()

那么也按照这个顺序一个个方法看下去。

measure()

直接先上源码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec); 
        ...
        }

这就是为什么重写onMeasure()的原因,因为measure()不可重写,但实际上它的测量在onMeasure()中完成。

来看一看measure()的两个参数,为什么不直接叫width和height,其实是因为这两个int值包含了MeasureSpec对象的信息,MeasureSpec对象由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。

int值的高2位表示MODE,MODE定义在MeasureSpec中,有三种类型:
UNSPECIFIED 父视图没有做任何约束,视图可以是希望的任何大小;
AT_MOST 视图可以是设定的任意大小,但最大值受到specMode的限制;
EXACTLY 视图是给定确定的大小。

而specMode呢,是int值的低30位。

那么它们从哪里来,在哪里构造MeasureSpec对象呢?,查看源码发现:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  

其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。

private int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
}  

在MATCH_PARENT和WRAP_CONTENT的时候,spceSize就是windowSize,所以这就是根布局是全屏的原因。UNSPECIFIED在什么情况下触发呢?这个很少,但还是有的,比如scrollView控件。

measure()够清晰了,让我们直接去看View的onMeasure()。

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

可以看到其实里面调用了setMeasuredDimension()这个方法,这就是为什么在重写onMeasure()时,要么调用超类的onMeasure(),要么调用setMeasuredDimension()的原因。而这之后就能通过getMeasureWidth()和getMeasureHeight()获得measureWidth和measureHeight了。

setMeasuredDimension()默认会调用getDefaultSize()方法来获取视图的宽高。

 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

第一个参数是Android建议的size,通过getSuggestedMinimumWidth()和getSuggestedMinimumHeight()来获取,这个不深究,看注释可以知道是返回View应该使用的最小宽高,也就是View的默认大小,都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的

第二个参数就是上面说到可以构建MeasureSpec对象的int值。看上面的代码可以知道,如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。

简单来说View的测量过程就是measure()调用onMeasure(),onMeasure()调用setMeasuredDimension()。

我们知道了View的测量的调用过程,默认值,赋值等,但只是View的测量,那么能进行嵌套的ViewGroup呢?ViewGroup里面包含一个或者多个子View,每个子View需要measure,ViewGroup是怎么做到的?

ViewGroup中定义了一个measureChildren()方法来去测量子View的大小,如下所示:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
    final int size = mChildrenCount;  
    final View[] children = mChildren;  
    for (int i = 0; i < size; ++i) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
}  

这里首先会去遍历当前布局下的所有子View,然后逐个调用measureChild()方法来测量相应子视图的大小,看一下measureChild():

protected void measureChild(View child, int parentWidthMeasureSpec,  
        int parentHeightMeasureSpec) {  
    final LayoutParams lp = child.getLayoutParams();  
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
            mPaddingLeft + mPaddingRight, lp.width);  
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
            mPaddingTop + mPaddingBottom, lp.height);  
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
}  

可以看到,先调用getChildMeasureSpec()去计算子View的MeasureSpec,计算的依据是父View的MeasureSpec,子View的padding值等等。然后调用子view的measure()方法,并把计算出的MeasureSpec传递进去,measure()就是上面说到的过程了。

measure完成之后,第二步就是layout,接下来看View的layout()。

layout()

也是先看一下源码:

 public void layout(int l, int t, int r, int b) {
      ...
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ...
                }
            }
        }
        ...
    }

可以看到首先调用setFrame()方法来判断视图的大小是否发生过变化判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout。

如果需要layout,调用的其实是onLayout(),所以当我们需要自定义布局的时候,重写的就是onLayout(),看一下onLayout()的源码:

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

这是一个空方法!重载onLayout的目的就是安排其children在父View的具体位置,所以看一下父View,通常在布局中都是ViewGroup包含着View,所以看一下ViewGroup的onLayout()。

protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

是一个抽象方法~意味着ViewGroup的子类都必须重写这个方法。

重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

那么按照这个思路,继承ViewGroup去自定义一个LinearLayout,代码如下:

public class MyLinearLayout extends LinearLayout {
    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {  
            View childView = getChildAt(0);  
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());  
        }  
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

那么很简单这里就是把MyLinearLayout里面包含的第一个子View按子View本身的宽高进行在MyLinearLayout里面布局。

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<core.flexible.activity.recyclerview.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">

   <core.flexible.activity.recyclerview.MyImageView
       android:id="@+id/img"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@mipmap/ic_launcher" />

</core.flexible.activity.recyclerview.MyLinearLayout>

运行截图:


自定义LinearLayout效果图

那么看一下绘制逻辑,首先是像上面measure过程一样,onMeasure()方法会在onLayout()方法之前调用,所以现在在onMeasure()方法中判断LinearLayout中是否有包含一个子View,有则调用measureChild()测量出子View的大小。

然后在MyLinearLayout的onLayout()中判断是否有包含子View,然后调用子View的layout()方法来确定它在MyLinearLayout布局中的位置,传入参数分别代表着子View在MyLinearLayout中左上右下四个点的坐标,然后layout完成,draw(具体过程后面draw()里面再说)。
那么如果想改变子View的位置只需要改变这四个坐标就可以了。

这里要说一下,在onLayout()执行之后,我们可以通过调用getWidth()方法和getHeight()方法来获取视图的宽高。这里又有一个宽高!
上面说到onMeasure()(实际上只要setMeasuredDimension()被调用之后)就可以通过getMeasureWidth()和getMeasureHeight()获得measureWidth和measureHeight。
那么两者的区别是什么呢?查找源码:

 public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
 public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }
 public final int getWidth() {
        return mRight - mLeft;
    }
 public final int getHeight() {
        return mBottom - mTop;
    }

可以清楚发现,getMeasuredWidth()和getMeasuredHeight()的值是measure时算好的宽高,getWidth()和getHeight()的宽高是layout(l, t, r, b)里面参数的计算后的值,所以onMeasure()之后得到的宽高值有可能和onLayout()之后得到的宽高不一样。

接下来就到了绘制的最后一步,draw()

draw()

源码:

public void draw(Canvas canvas) {
       ...

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

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

代码很长,看注释明显分为了6步,其中step2和step5可以跳过不管,来看其它。

step1:对背景进行绘制。

step3:对视图的内容进行绘制。可以看到,这里调用了onDraw(),进去一看发现,又是个空方法!。因为视图的内容不一定一样,所以具体的绘制实现就交由视图自己实现。

step4:当前视图如果存在子View,对所有子View进行绘制。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

step6:,对视图的滚动条进行绘制。View都有(水平垂直)的滚动条,一般不显示。

那么整个draw过程也很清晰了,也明白了为什么是重写onDraw()。

整个流程就先到这~

相关文章

网友评论

      本文标题:View的绘制过程

      本文链接:https://www.haomeiwen.com/subject/mpttmttx.html