欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(二)UI绘制流程-绘制过程源码分析

程序员文章站 2024-03-24 09:54:34
...

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

从上一篇可以可以知道,UI的绘制流程的起点在 ViewRootImpl 下的performTraversals()方法。并且按顺序调用了 performMeasure(),performLayout()和 performDraw()这三个方法。

一、MeasureSpec 测量规格

位于 View 类的内部类,由于在安卓布局中有自适应尺寸,而不是全部都是固定的宽高,所以在这里引入 MeasureSpec 来进行测量。MeasureSpec 主要包含的信息是 测量模mode 和 测量大小size 。MeasureSpec 是一个 32 位的数据,高2位表示 mode, 低30位表示 size 。

测量模式分三种:

EXACTLY :父容器已经测量出所需要的精确大小,这也是 childview 的最终大小
            ------match_parent,精确值
ATMOST : child view 最终的大小不能超过父容器的给的
            ------wrap_content 
UNSPECIFIED: 不确定,没有限制
            -------一般在 ScrollView,ListView 

点击查看 MeasureSpec 下的 makeMeasureSpec() 方法,返回基于 size 跟 mode 的一个测量规范。

public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
           return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

安卓绘制的时候是从最外层开始绘制,然后再一层层往下绘制子控件。这时候父容器的布局就会影响到子容器的布局,MeasureSpec 在这里传递父容器布局对子容器布局的一些限制。

二、Measure()

1.在 performTraversals()下调用 performMeasure() 方法会传进去两个参数,这两个参数在调用的上面初始化。

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

我们点击查看 getRootMeasureSpec() 方法。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

getRootMeasureSpec()是用于获取测量跟节点的测量规范,由上一篇我们知道这里的根节点就是我们的 DecorView,并且 DecorView 的布局为 MATCH_PARENT,所以走第一个分支。

2.点击查看我们的 performMeasure()方法。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

这时候我们的 mView 是 DecorView,DecorView 本身没有 measure()方法,最终会调到 View 里面的 measure()方法。首先会去判断该容器是否有光影的效果(边沿阴影),有的话会加上去光影的宽高。

boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int oWidth  = insets.left + insets.right;
    int oHeight = insets.top  + insets.bottom;
    widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
    heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

在 measure()方法里面没有去进行具体的测量,具体的测量是在 onMeasure()这个方法里面进行。

这是 View 中的 onMeasure(),调用了一个 setMeasuredDimension()方法, setMeasuredDimension() 又直接调用 setMeasuredDimensionRaw(),在这里面进行宽高的直接赋值。getMeasuredWidth()获取的值就是在这里的赋值,所以 getMeasuredWidth()必须在 onMeasure()之后。

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

当 DecorView 调用 onMeasure()的时候会调用到 FrameLayout 中的 onMeasure()方法。

在 FrameLayout 中的 onMeasure()中,先通过一个 for 循环对所有的子容器进行遍历。其中有个判断 child.getVisibility() != GONE 的时候才进行测量,这也是为什么 Visibility 设置为 GONE 的时候不占位置的原因。

for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        maxWidth = Math.max(maxWidth,
                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
        maxHeight = Math.max(maxHeight,
                child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        childState = combineMeasuredStates(childState, child.getMeasuredState());
        if (measureMatchParentChildren) {
            if (lp.width == LayoutParams.MATCH_PARENT ||
                    lp.height == LayoutParams.MATCH_PARENT) {
                mMatchParentChildren.add(child);
            }
        }
    }
}

每个子布局会调用 measureChildWithMargins()这个方法,在 measureChildWithMargins()最后又调用 measure()这个方法,如果子容器是 ViewGroup 的话重复上述布局,如果是非 ViewGroup 的 View 容器最终调到前面讲的 View 的 onMeasure()方法,从而结束。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这是一个递归的过程,跟上一篇中加载布局是一样的。测量完成之后一样会调用 setMeasuredDimension() 方法进行测量大小的设置。
(二)UI绘制流程-绘制过程源码分析

查看 getChildMeasureSpec()这个方法,getChildMeasureSpec()是根据当前父容器的测量规格(里面有测量模式和测量大小)以及子容器的一些属性来确定子容器的测量规格。子容器的测量规格受父容器的测量规格影响。

非 ViewGroup 的 View 的测量:onMeasure 方法里面调用 setMeasuredDimension()确定当前View的大小。
ViewGroup 的测量:1、通过 measureChildWithMargins()、measureChild() 或 measureChildren()三个方法来遍历测量 Child。2、setMeasuredDimension 确定当前ViewGroup的大小。

三、Layout()

performTraversals()下调用完 performMeasure() 后续调用 performLayout()方法。主要的内容在 Layout()方法里,在 Layout()方法里,确认了容器布局的左上右下四个边沿的值,然后调用容器的 onLayout()方法。
我们获取容器的宽度 getWidth() 方法是直接返回 右边沿 - 左边沿,所以 getWidth() 方法要在 Layout()方法之后使用。

可以看见在 View 中 onLayout()是一个空方法,什么都没做。在 ViewGroup 中 onLayout()是一个抽象方法,我们继续查看他的实现类 FrameLayout 下 onLayout()的实现。

FrameLayout 下 onLayout()直接调用 layoutChildren(),在这里面也是一个 for 循环对子容器的遍历,根据 Gravity 属性对子容器的上下左右边沿进行处理。这是 Gravity 属性生效的地方。最后调用各个子容器的 layout(),子容器是一个 ViewGroup 的话一样又进行了递归。

四、Draw()

performTraversals()下调用完 performLayout() 后续调用 performDraw()方法。一样的主要在 draw()方法里,在 draw()方法里真正开始绘制是在最后面调用的 drawSoftware()方法里。在这里有会调用的 DecorView 的 draw()方法,由于 DecorView 本身没有这个方法,从而就调用到了 View 的 draw()方法。

这是 View 的 draw()方法,很只是很清楚的写出了主要要做的事情。在这里,主要是对背景进行绘制。

 public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * 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);

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // we're done...
            return;
        }