View的工作原理

2018-09-25  本文已影响0人  阿泽Leo

本文不再讨论自定义View,推荐阅读扔物线的HenCoder的自定义View系列文章。

View的流程简述

View的绘制流程是从ViewRoot的performTraversals方法开始的,主要是:measure、layout、draw。大概流程如下图:

View的绘制流程.jpg
  • measure方法确定View的测量宽、高
  • layout方法确定View的最终宽高四个顶点的位置。
  • draw方法将View绘制到屏幕上。
  • ViewRoot的实现类是ViewRootImpl,是连接WindowManager和DecorView的纽带。
  • 从ViewRoot的performTraversals开始,经过performMeasure()performLayout()performDraw()完成顶级View(DecorView)的绘制。其中performMeasure()会调用measure和onMeasure,在onMeasure中对所有子元素调用measure和onMeasure,如此循环则完成measure过程,layout、draw同理。
  • 顶级View,即DecorView是一个FrameLayout。内部有一个竖直方向的LinearLayout,包含了titleBar和contentView。如下图:
DecorView.png
  • 我们的setContentView就是将自己的View放进了content里面。获取自己的View可以这样做:
ViewGroup viewGroup = findViewById(android.R.id.content);
View view = viewGroup.getChildAt(0);

View的具体工作流程

1. measure过程

作用:确定View的测量宽高

  • MeasureSpec:一个32位的int值。高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。

SpecMode分为UNSPECIFIED、AT_MOST、EXACTLY。

  • UNSPECIFIED 父容器不对View做任何限制,常用于系统内部。
  • EXACTLY 父容器指定View的大小为SpecSize指定的值。对应match_parent具体数值
  • AT_MOST 父容器指定最大尺寸SpecSize。对应wrap_content。

引用一张网上的图片帮助理解:


MeasureSpec.png

其中parentLeftSize表示父容器剩下的可用大小,childSize为子View的指定大小。

1.1 View的measure过程

在View的measure方法中会去调用View的onMeasure方法:

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

setMeasuredDimension方法会设置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的自定义控件没有重写onMeasure,那么参数match_parent和wrap_content将为同一结果。处理这个问题的常用代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    //处理warp_content为自己指定默认值,其余为本身测量值
    if (withSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (withSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(withSpecSize, mHeight);
    }

}
1.2 ViewGroup的measure过程

ViewGroup是一个抽象类,其中没有重写View的onMeasure方法但是提供了measureChildren的方法来测量子类,具体的测量过程需要子类去实现。

在实际开发中,需要在Activity启动时获取某个控件的宽高,如果在onCreate、onStart中获取这个View的宽高,有可能不是正确的数值。在View测量完成前,获取到的数值都是0。

解决方法有4种:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}
view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
});

2. layout过程

作用:确定View的四个顶点的位置和最终宽高。
layout方法首先确定ViewGroup的位置,然后在onLayout中对子View遍历来确定子View的位置。
具体流程:


ViewGroup的layout过程.jpg

3. draw过程

作用:将View绘制到屏幕上。
View的过程如下几步:

  • 绘制背景:drawBackground()
  • 绘制自己:onDraw()
  • 绘制children:dispatchDraw()
  • 绘制装饰:onDrawForeground()

通过查看View的源码可以清晰看到:

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
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

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

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
    ...
}
上一篇 下一篇

猜你喜欢

热点阅读