View的绘制流程

在Android中View的存在的方式一共有两种形式:

  1. 单一的View控件
  2. 可以包含其他View的ViewGroup

在了解View的绘制过程的时候,首先就要了解一下我们的Android的UI管理系统的层次关系:

如图所示:

UI层次关系.png

从源码中其实我们很容易就知道每个Activity都会创建一个最基本的窗口系统 PhoneWindow 。 PhoneWindow 是Activity与View 交互的接口。 从图中我们又看到 DecorView , 在事件传递机制下,事件会传递给这个 DecorView 吗,然后子View就能接受到事件了。 在 DecorView 中我们可以看到 TitleView 和 ContentView 。
TitleView 通常就是 ActionBar ,而 ContentView 就是我们最常接触的,就是平时在 Activity 中通过setContentView() 给Activity设置的View .

绘制的整体流程

当一个启动一个Activity的时候,Android系统会根据Activity的布局对它进行绘制。绘制会从根视图ViewRoot的 performTraversals() 方法开始 , 从上往下的遍历整个视图树。然而对于View控件来说,View控件只负责控制自己,而ViewGroup来说,他只是负责通知自己的子View进行绘制。

ViewRootImpl # performTraversals

  private void performTraversals(){
     
        .....
        //执行测量流程
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        .....
        //执行布局流程
        performLayout(lp, mWidth, mHeight);

        ......
        //执行绘制流程
        performDraw();
       
}

从ViewRootImpl 中可以看到的就是,视图的绘制会执行以下三个步骤,分别是 Measure (测量) 、Layout(布局)、Draw (绘制) 。

Measure

Measure 是用来计算View得到实际大小,由前面的分析可知,页面的绘制是从 performMeasure 方法开始的。

ViewRootImpl # performMeasure

 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    ...
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

从上面可以知道,performMeasure方法只是调用了 mView.measure(...) ,把具体的绘制交给了 View 。

View # measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
   ....
   onMeasure(widthMeasureSpec, heightMeasureSpec);
   ....
}

View # onMeasure

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

由上面可以得知到的一点就是performMeasure 最终会调用 View 或者 ViewGroup 的 measure 方法 ,而这里面实际上就是调用了 onMeasure 。

先对View分析

对于View来说,当调用到 onMeasure 的方法时候, 如果没有重写这个方法的话,那么默认的调用 getDefaultSize 来获取 View 的宽高。 源码如下:

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

从上面可以得知:对于View默认是测量很简单,大部分情况就是拿计算出来的MeasureSpec的size 当做最终测量的大小。

而对于一些派生出来的View ,如TextView 、ImageView 等,它们都对onMeasure方法系统了进行了重写。例如TextView 通常先去会先去测量字符的高度等,然后拿到View本身content这个高度,如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度。

再对ViewGroup分析

ViewGroup是特殊的View,然而在ViewGroup里面并没有实现 onMeasure 这个方法。而在不同的派生类中,各自实现了自身的 onMeasure 方法。对于DecorView 来说 ,其实就是一个FrameLayout,对于要测量时,一开始其实就是调用到了 FrameLayout 的 onMeasure 方法中 , 从 FrameLayout 中可以看到:

FrameLayout # onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    .....

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        if (mMeasureAllChildren || child.getVisibility() != GONE) {   
            ....
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ....
        }
    }

    ....
     setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    ....
}

从上面可以看到,其实 ViewGroup 的内部就是 遍历自己的子View,只要不是GONE的都会参与测量。然后等所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高。综上,父View是等所有的子View测量结束之后,再来测量自己。

Layout

Layout 过程用来确定View在父容器的布局位置,他是由父容器获取子View的位置参数后,调用子View的layout方法并将位置参数传入实现的,源码如下:

ViewRootImpl # performLayout

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    ....
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    .....
}

View # layout

public void layout(int l, int t, int r, int b) {
   .....
    onLayout(changed, l, t, r, b);
   ....
}

View # onLayout

  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

onLayout 实际上就是一个空方法,对于ViewGroup来说,就应该实现这个方法。对于 子ViewGroup 来说,例如LinearLayout、RelativeLayout等,均重写了这个方法。

Draw

Draw操作用来将控件绘制出来,源码如下:

ViewRootImpl # performDraw

private void performDraw() {
    ....
        draw(fullRedrawNeeded);
    ....
}

ViewRootImpl # draw

 private void draw(boolean fullRedrawNeeded) {
    ....
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
    .....
}

ViewRootImpl # drawSoftware

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
    ....
    mView.draw(canvas);
     ....
}

最会就调用子View 的 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
    .....
    drawBackground(canvas);
    ....
    // Step 2, save the canvas' layers
    .....
    saveCount = canvas.getSaveCount();
    .....

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    ....
    canvas.drawRect(left, top, left + length, bottom, p);
    ....
    canvas.restoreToCount(saveCount);

    ....

    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

从源码中我们很清晰的看到View绘制的流程

  1. 绘制View的背景
  2. 如果需要,保存canvas,为fading做准备
  3. 绘制View内容
  4. 绘制View的子View
  5. 如果需要的话,绘制View的fading边缘并恢复图层
  6. 绘制View的装饰(如滚动条)

measure(测量)方法的注意

从上面我们可以清楚了的明白了View的绘制过程了,从measure到layout再到Draw的一系列过程,最终View绘制了出来。然而有些时候我们想在Activity已启动的时候就做一件任务,这一件任务是获取某个View的宽/高。但是我们在onCreate或者onResume 获取View的宽和高却获取不了数值,测试如下:

 <TextView
    android:id="@+id/tv_main"
    android:layout_width="250dp"
    android:layout_height="35dp"
    android:gravity="center"
    android:text="Hello World!" />

MainActivity

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTvMain = (TextView) findViewById(R.id.tv_main);

    System.out.println("TextView 的高度为:"+mTvMain.getHeight());
    System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
}

运行结果如下:

TextView 的高度为:0
TextView 的宽度为:0

实际上在onCreate、onStart、onResume中均无法正确得到某
个View的宽和高信息,这是因为View的measure过程和Activity的生命周期方法不是同步
执行的因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量
完毕了。

如果想要拿取View的宽和高又应怎么做呢?下面介绍三种方法。

1. onWindowFocusChanged

onWindowFocusChanged 这个方法的含义是:View已经初始化完毕了,宽/高已经准备
好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调
用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity
继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume
和onPause,那么onWindowFocusChanged也会被频繁地调用。

代码如下:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        System.out.println("TextView 的高度为:"+mTvMain.getHeight());
        System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
    }
}

运行结果:

TextView 的高度为:70
TextView 的宽度为:500

2. view.post(runnable)

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

代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTvMain = (TextView) findViewById(R.id.tv_main);

    mTvMain.post(new Runnable() {
        @Override
        public void run() {
            System.out.println("TextView 的高度为:"+mTvMain.getHeight());
            System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
        }
    });
}

运行结果:

TextView 的高度为:70
TextView 的宽度为:500

3. ViewTreeObsener

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

代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTvMain = (TextView) findViewById(R.id.tv_main);
    ViewTreeObserver viewTreeObserver = mTvMain.getViewTreeObserver();
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            mTvMain.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            System.out.println("TextView 的高度为:" + mTvMain.getHeight());
            System.out.println("TextView 的宽度为:" + mTvMain.getWidth());
        }
    });
}

运行结果:

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

推荐阅读更多精彩内容