View的绘制流程

2017-04-06  本文已影响109人  锐_nmpoi

在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
上一篇下一篇

猜你喜欢

热点阅读