Android View的工作原理

一、绘制流程

View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout、draw三个过程才能最终将一个View绘制出来,其中measure是用来测量View的宽高,layout是用来确定View在父容器的位置,draw则负责将View绘制在屏幕上,大致流程如下:

绘制流程.png

二、measure过程

1、MeasureSpec

从上图可以了解到View在绘制过程中会调用到View的measure()方法,measure()方法接收两个参数:widthMeasureSpecheightMeasureSpec,分别用于确定视图的宽度和高度的规格。
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)
SpecMode有三类:

  • UNSPECIFIED
    未指定模式,父容器不对View有任何限制,一般用于系统内部,开发过程中不太会用到。
  • EXACTLY
    精确模式,父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体的数值这两种模式。
  • AT_MOST
    最大模式,父容器指定了一个可用大小,即SpecSize,View的大小不能大于这个值。它对应LayoutParams中的wrap_content。
子视图的MeasureSpec

widthMeasureSpecheightMeasureSpec这两个参数的值通常是由父视图传递给子视图,再经过计算得出来的,说明父视图会在一定程度上决定子视图的大小。观察ViewGroup的measureChildWithMargins方法如下:

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

其中childWidthMeasureSpec 与childHeightMeasureSpec 都是通过getChildMeasureSpec的计算得出的,并且与父容器的MeasureSpec和子元素本身的LayoutParams有关,再看看getChildMeasureSpec方法的代码:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

以上的代码可以用一个表格来表示:

普通View的MeasureSpce创建规则.png

总结如下:

  • 当View采用固定宽/高时,不管父容器的Measure是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams中的大小。
  • 当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽/高是wrap_content时,不管父容器是最大模式还是精确模式,View的模式总是最大模式,并且其大小不会超过父容器的剩余空间。
  • UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说,不需要关注此模式。
根视图的MeasureSpec

最外层的根视图的widthMeasureSpec和heightMeasureSpec是在performTraversals()方法中获取到:

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

其中的lp.width和lp.height在创建ViewGroup实例的时候就被赋值为MATCH_PARENT了,getRootMeasureSpec的代码如下:

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

由此可见,当rootDimension等于MATCH_PARENT时,MeasureSpec的SpecMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT时,MeasureSpec的SpecMode就等于AT_MOST,当rootDimension为具体数值时,MeasureSpec的SpecMode就等于EXACTLY,与前面描述的一致。且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。

2、View的measure过程

View的measure过程由其measure方法来完成,而measure方法是一个final方法,这意味着子类不能重写此方法,而measure方法中调用的onMeasure方法才是真正去测量并设置View大小的地方,默认会调用getDefaultSize方法来获取视图的大小:

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

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

这里的MeasureSpec是由measure方法传递下来的,测量后调用setMeasuredDimension方法来设定测量后的大小,这样一次measure过程就结束了,这是系统的默认测量方式,实际上我们可以重写这个方法来改变测量方式,从而实现自定义View的测量。
值得注意的是,在重写onMeasure方法的时候,需要注意设置好View的warp_content情况,按照自身情况来测量出实际所需大小,否则在布局中使用wrap_content就相当于使用match_parent,从代码可以看出,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,则宽/高等于specSize,从上面的“普通View的MeasureSpce创建规则”表中可知,这种情况下View的specSize是parentSize,即父容器当前剩余空间大小,与使用match_parent效果一致。因此需要根据需求来判断解决这个问题,例如使用默认大小等。

3、ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。与View不同的是,ViewGroup是一个抽象类,并没有定义其测量的具体过程,毕竟不同ViewGroup的子类有不同的布局特性,如RelativeLayout和LinearLayout,因此需要子类自己去实现ViewGroup提供了一个叫measureChildren的方法:

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

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

measureChild与measureChildWithMargins不同的地方在于,measureChild没有测量自己的margin属性,而measureChildWithMargins有,当需要使用到margin属性时,还是需要使用measureChildWithMargins来测量。

4、测量结束

measure完成后,通过getMeasuredWidth/getMeasuredHeight方法就可以正确地获取到View的测量宽/高,但是在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的,因为View需要多次measure才能确定自己的宽/高,前几次测量过程中,得出的测量结果可能与最终结果不一致,因此最好还是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

三、layout过程

measure结束后,视图的大小就已经测量好了,接下来就是layout过程了。layout的作用是给视图进行布局的,也就是确定视图的位置。ViewRootd的performTraversals方法会在measure结束后继续执行,并调用layout方法来执行此过程:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

layout方法接收四个参数,分别代表着相对于当前视图的父视图而言的左、上、右、下的坐标,在layout中会调用onLayout方法,但是,View的onLayout是一个空方法,因为View的位置应该由父视图ViewGroup来决定的,而ViewGroup中的onLayout方法是一个抽象方法,这是由于每个ViewGroup的布局方式不同,因此需要重写这个方法来确定子元素的位置。
layout结束后,就可以通过getWidth和getHeight来得到其最终宽/高:

public final int getWidth() {
        return mRight - mLeft;
    }

public final int getHeight() {
        return mBottom - mTop;
    }

四、draw过程

draw过程比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

  • 绘制背景background.draw(canvas)
  • 绘制自己(onDraw)
  • 绘制children(disptchDraw)
  • 绘制装饰(onDrawScrollBars)
    首先绘制背景,其实就是在XML中通过android:background属性设置的图片或颜色,当然也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值;
    接下来是绘制自己,调用onDraw方法,使用画布来绘制自己的内容,自定义View的时候主要就是重写这一个方法;
    接下来是绘制children,调用disptchDraw来绘制所有的子元素;
    最后是绘制装饰,这一步的作用是对视图的滚动条进行绘制,每一个View其实都有滚动条,只是有些控件没有显示出来。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容