View的工作原理(一)测量过程

在Android体系中View作为视觉上的呈现,扮演着非常重要的角色。尽管Android提供了一套包含很多控件的GUI库。但是在大多数情况下,因为交互或展现的定制化要求,我们不是不能直接拿来使用的。怎么解决这一问题呢?

为了防止Android应用界面的同类化严重,我们需要通过自定义View来实现更友好的用户体验和更美观的呈现效果。

前言

为了更好的自定义View,我们应该掌握View的基本流程,如:View的测量流程、布局流程及绘制流程;

除了三大流程之外,View常见的回调方法也是需要熟练掌握的。

  1. 构造方法:
    基本属性和自定义属性的初始化和赋值;
  2. onAttach
  3. onVisibilityChanged
  4. onDetach

View的工作原理

ViewRoot对应于ViewRootImpl类,它是连接WindowManger和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。

在ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。参看下面源码(位于android-25/android/app/ActivityThread.java
):

                //该代码位于handleLaunchActivity->handleResumeActivity方法中
                ActivityClientRecord r = mActivities.get(token);
                ...
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }

View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout和draw三个过程才最终将一个View绘制出来。其中:

  1. measure:用来测量View的宽和高;
  2. layout:用来View在父容器放置的位置;
  3. draw:负责将View绘制在屏幕上;
performTraversals 的工作流程图

如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程;其中performMeasure中会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法则会对所有的子View进行measure过程,这时measure流程就从父容器传递到子元素中去。然后子元素重复父容器的measure过程,如此反复就完成了整个View树的遍历,这样就完成了依次measure过程。另外两个过程是类似的,不赘述。

Measure过程

measure过程决定了View的宽高,measure完成以后可以通过getMeasureWidth和getMeasureHeight方法来获取View测量后的宽高,几乎所有情况下这两个值都等同于View的最终宽高值,特殊情况下除外(onLayout时强制指定宽高)。

Layout过程

layout过程决定了View的四个顶点的坐标和实际的View宽高。方法完成后,可以通过getTop、getBottom、getLeft、getRight来拿到View四个顶点的位置,并可以通过getWidth、getHeight方法来拿到View最终宽高。

Draw过程

draw过程决定了View的显示内容,只有draw方法完成之后View的内容才能显示在屏幕上。

DecorView层级组成

DecorView作为顶级的View,一般情况下它内部会包含一个竖直方向的LinearLayout,这个LinearLayout里面有上下两部分(具体情况和系统版本及主题有关),上面是标题栏,下面是内容区。

Activity通过setContentView所设置的布局其实是添加到DecorView的内容区里面了。而内容区的id是content,因此可以理解制定布局的方式不叫setView而叫setContentView;确切的说,设置的布局是加在id为content的FrameLayout的布局中。

如图:

顶级View: DecorView 的 结构图

理解MeasureSpec

MeasureSpec通过将高2位的SpecMode和低30位的SpecSize打包成一个32位的int里面来避免过多的内存分配。为了方便操作,其同时提供了打包和解包方法。

SpecMode

  • UNSPECIFIED(未指明模式)
    父容器不对View做任何限制,要多大给多大,这种情况下一般用于系统内部,表示一种测量的状态。

  • EXACTLY(准确模式)
    父容器已经检测出View所需要的精确大小,这时View的最终大小就是低30位的SpecSize指定的值。这种情况对应于LayoutParams中的match_parent和具体的数值这两种模式。

  • AT_MOST(最大模式)
    父容器指定了一个可用大小即就是低30位的SpecSize的值,最终View的值不能大于这个值。它对应于LayoutParmas中的wrap_content。

MeasureSpec和LayoutParams的对应关系

Android系统内部是通过MeasureSpec来进行View测量的,但正常情况下我们使用View指定MeasureSpec;同时我们也可以给View设置LayoutParams。在View的测量时,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后在根据MeasureSpec来确定View测量后的宽高。

  • MeasureSpec不是仅由LayoutParams决定的,其需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。
  • 顶级View(DecorView)的MeasureSepc由窗口的尺寸和其自身的LayoutParams来共同决定。
  • 对于普通View,它的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同决定。
  • MeasureSpec一旦确定onMeasure中就可以确定View的测量宽、高。

DecorView中对应关系

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

普通View中的对应关系

  1. 当View采用固定模式宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec是精确模式且其大小为当前View要求的大小。
  2. 当View宽高采用match_parent时,那么View的MeasureSpec是精确模式且其大小为父容器剩余的空间大小;如果父容器为最大模式,那么View的MeasureSpec是最大模式且其大小不超过父容器剩余的空间大小。
  3. 当View的宽高采用wrap_content时,不论父容器的模式是精确模式还是最大模式,View的MeasureSpec是最大模式且其大小不超过父容器剩余的空间大小。
普通 View 的 MeasureSpec的创建规则
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的工作流程

View的工作流程主要是指measure(测量)、layout(布局)、draw(绘制)这三大流程。
其中measure确定了View的测量宽高,layout确定了View的最终宽高和四个顶点的位置,draw则将View绘制到屏幕上。

measure过程

measure过程要分两种情况来分析。如果是原始的View,那么通过measure方法就完成了View的测量过程;如果是一个ViewGroup除了完成自己的测量过程外,还要遍历调用所有子元素的measure方法,各个子元素再递归的执行这个流程。

原始View的measure过程

View的measure过程使用measure方法完成的,measure方法是一个final修饰的方法,意味着子类不能重写此方法,在measure方法中会调用其onMeasure方法。onMeasure方法源码如下:

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

onMeasure方法很简洁,setMeasuredDimension方法会设置View的宽高的测量值。我们同时需要关注getDefaultSize这个方法,这个方法逻辑很简单在AT_MOST和EXACTLY这两种情形下,getDefaultSize大小就是MeasureSpec的specSize的值。

而在UNSPECIFIED(位指明模式)的情形下,一般用于系统内部的测量过程,getSuggestedMinimumWidth、getSuggestedMinimumHeight这两个方法的返回值。源码如下:

   protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
   }

   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
   }

这种情形下,可以得出如下结论:
如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回minWidth和背景的最小宽度、高度之间的最大值。

结论

直接继承View的自定义空间需要重写onMeasure方法,并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用了match_parent。

View在布局中使用wrap_content,那么他的specMode为AT_MOST,这种模式下宽高等于specSize,根据之前的表格可以得知,specSize就是parentSize即父容器当前剩余的空间大小;很显然这种效果等同于布局中使用match_parent。

如何解决这个问题?很简单,只需要给View指定一个默认的内部宽高,并在wrap_content是设置此宽高即可。代码如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_ MOST && heightSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

查看TextView、ImageView等的源码可以知道,针对wrap_content的情形,在onMeasure方法中都做了特殊处理,可以自行阅读查看。

ViewGroup的measure过程

和View不同的是ViewGroup是一个抽象类,因此它没有重写onMeasure方法,而是提供了一个叫做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);
    }

上述代码可以看出ViewGroup在measure时,会对每一个子元素进行measure,measureChild方法的实现就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来获取子元素的MeasureSpec,接着讲MeasureSpec传递给依次调用子元素的measure方法进行测量。

总结

ViewGroup作为一个抽象类并没有定义其测量的具体过程,代表测量过程的onMeasure方法需要各个子类进行具体的实现,比如LinearLayout、RelativeLayout等。ViewGroup的实现类有不同的布局特性,导致系统无法统一实现,因此才决定交给扩展类区实现。


View的测量过程是三大流程中最复杂的一个,measure完成以后,通过getMeasureWidth/Height方法就可以正确获取到View的测量宽、高。需要注意的是,极端的情况下,系统可能需要多次调用measure才能最终确定测量宽、高,这种情形下onMeasure拿到的测量宽高可能并不准确。一个比较好的习惯是在onLayout方法中获取View的测量宽高,或者最终宽高。

参考一:《安卓开发艺术探究》实体书

参考二:《安卓开发艺术探究》Kindle电纸书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容