View的三大流程

View的三大流程

基本概念

在了解View的三大流程之前,需要先了解一些和基础概念,才能更好的了解View的measurelayoutdraw过程。
  以下先了解ViewRootDecorView的概念。

ViewRoot

ViewRoot对应ViewRootImpl类,它是连接WindowManagerDecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImplDecorView建立关联。代码大致如下。

root = new ViewRootImpl(view.getContext(), diaplay);
root.setView(view,wparams, panelParentView);

View的绘制流程是从ViewRootperformTraversals方法开始的,它经过measurelayoutdraw三个过程才能最终将一个View绘制出来,而measure用来测量View的宽和高,layout用于测定View在父容器中的放置位置,而draw则负责将View绘制到屏幕上,而performTraversals的大致流程如下。

performTraversals的鬼办公桌椅流程图

如上图所示,performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,这三个方法会分别完成顶级View的measurelayoutdraw这三大流程。其中,performMeasure会调用measure方法,而在measure方法中又会调用onMeasure方法,在onMeasure中则会对所有的子元素进行measure过程,这个时候measure流程就从父元素传递到子元素中,这样就完成依次measure过程。接着子元素会重复父元素的measure过程,如此反复便完成了整个View树的遍历。同理,performLayoutperformDraw的传递过程也是类似的,而唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw完成的,不过这没有本质区别。
  measure过程决定了View的宽高,Measure完成后,可以通过getMeasureWidthgetMeasureHeight来获取View测量后的宽高,而在几乎所有的情况下它都等同于View最终的宽高,但也有特殊情况。
  layout过程决定了View的四个顶点的坐标和实际的View宽高,Layout完成之后,可以通过getTopgetBottomgetLeftgetRight来拿到View四个顶点的位置,可以通过getWidthgetHeight来拿到View的最终宽高。
  draw过程决定了View的显示,只有draw方法完成了之后View的内容才能显示在屏幕上。

DecorView

DecorView的结构

DecorView作为一个顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两部分(具体情况要看Android的版本及其应用的主题),上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是android.R.id.content,所以Android的设置布局是setContentView而不是setView。而通过源码可知,DecorView其实是一个FrameLayout,View层的所有事件都经过DecorView,之后才传递给我们的View。

理解MeasureSpec

在了解View的测量过程前,还需要理解MeasureSpecMeasureSpec在很大的程度上决定了一个View的尺寸规格,而这个过程还同时受到父View的影响,因为父容器会影响子View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转化为对应的MeasureSpec,然后再根据这个measureSpec来测出对应的view宽高,而这个宽高是测量宽高,不一定等同View的最终宽高。

MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,其中SpecMode代表的是测量模式,而SpecSize是指在某种测量模式下的规格大小。以下是MeasureSpec内部的一些常量的定义,便于理解MeasureSpec的工作原理。

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

@MeasureSpecMode
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

从以上代码中可以看见,MeasureSpec通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配工作,并为了方便操作,提供了打包和解包方法。SpecModeSpceSize也是一个int值,一组SpecModeSpceSize可以打包为MeasureSpec,也可以通过解包获取。需要注意的是这里指的是MeasureSpec的int值而并非其本身。

SpecMode

SpecMode有三类,每一类都代表着特殊的含义,如下所示。

UNSPECIFED

父容器不对View有任何限制,想要多大就给多大,一般用于系统内部,表示一种测量的状态。

EXACTLY

父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpceSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值两种模式。

AT_MOST

父容器指定一个可用大小的即SpecSize,View的大小不能超过这个值,具体值要根据View的情况来判定,它对应于LayoutParams中的wrap_content

Measure与LayoutParams的关系

系统内部是通过MeasureSpec来进行View的测量,但是正常情况下,我们使用View指定MeasureSpec。尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。需要注意速度是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。另外,对于DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸及其自身的LayoutParams共同决定;而对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的的LayoutParams共同决定。
  而MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽高了。
  对于DecorView来说,在ViewRootImplmeasureierarhy有一段代码中有如下的一段代码,它展示了DecorViewMeasureSpec的创建过程,其中baseSizedesiredWindowHeight是屏幕的尺寸。

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

以下是getRootMeasureSpec的实现。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

上诉代码中,DecorViewMesureSpec产生过程就很明确了,根据LayoutParams中的宽高参数来划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不能超过窗口大小。
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通View来说,这里指的是我们布局中年的View,View的measure过程由ViewGroup传递而来,以下是ViewGroupmeasureChildWithMargins方法的代码。

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

上面的方法中会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec来获取子元素的MeasureSpec。从代码上看,子元素的MeasureSpec与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的marginpadding有关。具体的ViewGroupgetChildMeasureSpec代码如下。

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

上述代码中,主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器已经占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下。

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

getChildMeasureSpec清楚地展示了普通View的MeasureSpec的创建规则,以下是getChildMeasureSpec的逻辑表。其中parentSize是指父容器可用的大小。

普通View的MeasureSpec的创建规则

结合之前分析和上表,其中,对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec。简而言之,当View采用固定宽高的时候, 不管其父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小是父容器的剩余空间;其他情况下,如果其父容器是最大模式,那么View也是最大模式并且不会超过父容器的剩余空间。当View的宽高是wrap_content时,不过父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。
  UNSPECIFIED模式主要用于系统内部多次Measure的情形。

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

推荐阅读更多精彩内容