View的工作原理

这篇主要是我认为《Android开发艺术探索》第四章的重点,所以建议结合任老师的书来看,否则可能会觉得不知所云,没写的并不是说明不重要,而是我没有意识到重要性或者是我已经掌握的知识。

View的流程主要包括测量流程(measure)、布局流程(layout)、绘制流程(draw)。

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

理解MeasureSpec

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这时 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理, perforLayout 和 performDraw 的传递流程和 perforMeasure 是类似的,唯一不同的是,perforDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的。

SpecMode 有三类

  • UNSPECIFIED

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

  • EXACTLY

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

  • AT_MOST

    父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

对于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同确定;对于普通 View ,其 MesaureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽/高。

对于普通 View 的测量过程,可以查看 ViewGroup 的 getChildMeasureSpec 方法,如果不知道怎么找的话,可以在AndroidStudio写一下,然后跳转过去就可以了。如果不方便看(就是懒得看源码),可以参考下面的表格,是根据 getChildMeasureSpec 整理的。

普通 View 的MeasureSpec 创建规则.PNG

View 的工作流程

measure过程

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

答:如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于 specSize,而此时 view 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小,即 View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。

解决方法: 给 View 指定一个默认的内部宽/高( mWidth,mHeight ),并在 wrap_content 时设置此宽/高即可,对于非 wrap_content 情形,沿用系统的测量值。

    @Override
    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);

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

    }

建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

问:在 Activity 已启动的时候获取某个 View 的宽/高。

答:
Activity/View # onWindowFocusChanged
onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = mContainer.getMeasuredWidth();
            int height = mContainer.getMeasuredHeight();
        }
    }

mContainer 为需要测量的 View。

view.post(runnable)

通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候, View 也已经初始化好了。

    @Override
    protected void onStart() {
        super.onStart();
        
        mContainer.post(new Runnable() {
            @Override
            public void run() {
                int width = mContainer.getMeasuredWidth();
                int height = mContainer.getMeasuredHeight();
            }
        });
    }

ViewTreeObserver

使用 ViewTreeObserver 的众多回调都可以完成这个功能,例如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见效发生改变时, onGlobalLayout 方法将被回调,因此这是获取 View 的宽/高一个很好的时机,伴随着 View 树的状态改变等, onGlobalLayout 会被调用多次。


    @Override
    protected void onStart() {
        super.onStart();

        ViewTreeObserver treeObserver = mContainer.getViewTreeObserver();
        treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mContainer.getMeasuredWidth();
                int height = mContainer.getMeasuredHeight();
            }
        });
    }

view.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对 View 进行 measure 来得到 View 的宽/高,需要根据 View 的 LayoutParams 来分:

  • match_parent
    无法 measure 出具体的宽/高。根据 view 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,即父容器的剩余空间,而此时我们无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小。

  • 具体数值(dp/px)
    比如宽高都是100px,如下 measure:

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        mContainer.measure(widthMeasureSpec,heightMeasureSpec);

  • wrap_content
    如下measure:
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        mContainer.measure(widthMeasureSpec,heightMeasureSpec);

注意到 (1 << 30) - 1 ,通过分析 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是说最大时30个1,即2^30 -1,也就是(1 << 30) - 1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的。

layout 过程

Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。

问:View 的 getMeasuredWidth 和 getWidth的区别

答:在 View 的默认实现中, View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。

draw 过程

draw 的作用是将 View 绘制到屏幕上面,View 的绘制过程遵循如下步骤:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己 (onDraw)
  3. 绘制 children (dispatchDraw)
  4. 绘制装饰 (onDrawScrollBars)

View 中有一个特殊的方法 setWillNotDraw,如果一个 View 不需要绘制任何内容,那么设置这个标记为为 true 以后,系统会进行相应的优化。默认情况下, View 没有启动这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们需要显式地关闭 WILL_NOT_DRAW 这个标记位。

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

推荐阅读更多精彩内容