View的绘制过程

最近在学习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()。

整个流程就先到这~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容