Android 开发中经常需要用一些自定义 View 去满足产品和设计的脑洞,所以 View 的绘制流程至关重要。网上目前有非常多这方面的资料,但最好的方式还是直接跟着源码进行解读,每日一问系列一直追求短平快,所以本文笔者尽量精简。

想必大多数 Android 开发都知道自定义 View 需要关注的几个方法:onMeasure()onLayout()onDraw(),这其实也是每个 View 至关重要的绘制流程。

基本绘制都是会从根视图 ViewRootperformTraversals() 方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。performTraversals() 的核心代码如下:

private void performTraversals() {
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}

measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

每个 View 都有自己的大小,所以基本自定义 View 的时候都需要重写 onMeasure() 这个方法,以定制化我们的 View 的宽高。如果不重写这个方法,我们通常会出现 wrap_contentmatch_parent 是一样的显示效果。至于原因,其实一探源码便知。

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

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

可以看到,View 默认是会使用 getDefaultSize() 方法进行设置宽高的,在 AT_MOSTEXACTLY 两种情况下都会直接使用测量规格里面的尺寸。在 UNSPECIFIED 模式下会直接取getSuggestedMinimumWidth() 的返回值。

ViewGroup 中,并没有去重写 ViewonMeasure() 方法,而这都需要它的子类根据自己的逻辑去实现,比如 LinearLayoutRelativeLayout 明显测量逻辑是不一样的。不过,ViewGroup 倒是提供了一个 measureChildren() 方法来依次遍历每个子 View 对其进行测量。

在经过 onMeasure() 操作后,getMeasureWidth()getMeasureHeight() 方法就可以拿到正确的返回值了。

layout()

public void layout(int l, int t, int r, int b)

我们可以重写的 onLayout() 方法主要作用是确定子 View 的显示位置,由于 View 已经是最小的层级,所以我们在自定义 View 的时候通常不需要管这个方法,而在自定义 ViewGroup 的时候就不得不注意这个方法了。

经过 onLayout() 流程后,我们的 leftrighttopbottom 得以赋值,所以这时候可以通过 getWidth()getHeight() 方法来获取 View 的实际宽高了。

draw()

public void draw(Canvas canvas)

绘制的流程也就是通过调用 View 的 draw() 方法实现的。draw() 方法里的逻辑看起来更清晰,我就不贴源码了。一般是遵循下面几个步骤:

  • 绘制背景 – drawBackground()
  • 绘制自己 – onDraw()
  • 绘制孩子 – dispatchDraw()
  • 绘制装饰 – onDrawScrollbars()

由于不同的控件都有自己不同的绘制实现,所以V iew 的 onDraw() 方法肯定是空方法。而 ViewGroup 由于需要照顾子 View 的绘制,所以肯定在 dispatchDraw() 方法里遍历调用了child的 draw() 方法。

参考:
Android View的绘制流程
https://blog.csdn.net/yisizhu/article/details/51527557

06-12 13:18