View的绘制流程

1 Activity 与 Window、PhoneWindow、DecorView 之间的关系

  • 每一个Activity都持有一个Window对象
  • Window是一个抽象类,提供了绘制窗口的一组通用API。可以理解为一个显示View的载体。
  • PhoneWindow是Window唯一的实现类。Activity 中的Window 实例就是一个 PhoneWindow 对象。
  • DecorView类是PhoneWindow类的内部类,每个PhoneWindow中持有一个DecorView对象,Activity中View相关的操作大都是通过DecorView完成的(DecorView是Activity界面的根View)。
  • DecorView继承FrameLayout(DecorView→FrameLayout→ViewGroup→View),通常DecorView包含一个竖直方向的LinearLayout,LinearLayout上面TitleView(标题栏),下面是ContentView(内容栏)。DecorView会获取Activity的setContentView()传递过来的布局id,通过inflater(布局填充器),将布局资源id加载成一个View,并添加到内容栏(R.android.id.content)。

2 View绘制的来龙去脉

  • 首先我们可以把所有需要绘制的内容看成一颗View树,一个树本质上就是一个根节点,View树的根节点不是View就是ViewGroup,ViewGroup继承于View,所以本质上这个根节点就是View。因此归结下来,我们要绘制的就是一个最外层的根View,也就是整颗View树的根节点。
  • Activity的根View就是DecorView,在Android层面来讲,View的显示是依赖与Window窗口的,因此DecorView的显示就依赖于Activity 中的的PhoneWindow对象。
  • 由于一个Window对象就对应了一个View对象,两者是一一对应关系,所以实际上在创建Window的过程中就会完成View的添加与显示
  • 那Activity是怎么创建自己Window对象的呢?实际上Window对象是由远程的WMS系统服务实现创建的,Activity作为本地应用只能通过WindowManager对象通过IPC(跨进程通信)的方式进行创建Window对象。如何获取WindowManager对象呢,Context提供一些方法可以获取WindowManager对象。
  • 具体方法调用流程:
    Activity:handleResumeActivity(该方法内使用Context.getWindowManager创建WindowManager对象)→
    WindowManager:addView(该方法内WindowManager委托代理给一个WindowManagerGLobal对象)→
    WindowManagerGLobal:addView(该方法内创建了ViewRootImpl对象)→
    ViewRootImpl:setView→requestLayout→scheduleTraversals→doTraversal→performTraversals(最终到达绘制的入口)
    • 其中从WindowManager.addView开始就是Activity创建Window的过程,最终在ViewRootImpl对象的performTraversals中完成View的绘制(一个Window对象对应了一个ViewViewRootImpl对象也对应了一个View对象,即DecorView)
    • performTraversals()是绘制的入口,它依次调用performMeasure()、performLayout()和performDraw()三个方法,三个方法内部分别调用了DecorView的measure()、layout()和draw方法。

结论:追根溯源后发现,DecorView的绘制就是调用了自己的measure()、layout()和draw()这三个方法,因此View的绘制可以概括成三个流程。

3 View绘制的三个流程

简单概括View绘制的三个流程,分别对应了View的三个方法:
①measure测量:测量出整个View树所有View的宽高,给出每个View的“测量宽高”(会先判断是否需要重新计算)
②layout布局:确定整个View树所有View的“最终宽高”和四个顶点的位置(会先判断是否需要重新计算)
③draw绘制:绘制整个View树的所有View(会先判断是否需要重新绘制)

3.1 measure测量

在此之前,我们需要了解MeasureSpec这个类,它在测量中取到了巨大作用,经常会作为参数传入。

  • MeasureSpec是View类的一个静态内部类,MeasureSpec表示的是一个 32 位的整数值,它的高 2 位表示测量模式SpecMode,低 30 位表示这种测量模式下的规格大小SpecSize,总共有三种测量模式。(MeasureSpec提供了一些将一个32位整数打包或解包成<mode,size>的二元组的API,使得这些特殊的32位整数有了特定的含义,从抽象意义上可以说它封装了从父级传递到子级的布局约束),三种SpecMode:
    • UNSPECIFIED(00):不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少使用到
    • EXACTLY(01):精确测量模式,当该视图的 layout_width 或者 layout_height 指定为具体数值或者 match_parent 时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值。
    • AT_MOST(10):最大值模式,当前视图的 layout_width 或者 layout_height 指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图运行的最大尺寸的任何尺寸。

三种模式暂时不理解也没关系,我们先简单了解完MeasureSpec,就可以从测量的入口performMeasure()开始了:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
	...
	//Activity中的mView就是DecorView
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

测量从根View的measure方法开始,但实际的测量工作是在measure中调用的onMeasure方法中执行,可以看到performMeasure传入两个来自父布局的宽和高的布局约束,这两个布局约束会传入根View的measure方法然后再传入onMeasure方法。因此我们知道了,测量会接受来自父布局的宽高的约束。

View.measure方法是一个final方法,无法被重写,而onMeasure即是实际测量的方法也是可以被重写的方法,另外View绘制的另外两个流程也类似,因此我们自定义View通常就是重写onMeasure、onLayout和onDraw这三个方法

在看onMeasure方法前,你可能会好奇,那performMeasure传进的这两个布局约束是哪来的呢,根View的父级传来的吗?那根View的父级是什么?这就得回到ViewRootImpl的performTraversals方法看看了。

private void performTraversals() {
	...
	//这里获取的MeasureSpec就是传递给DecorView的布局约束
	int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
	int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
	...
	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	...
	performLayout(lp, mWidth, mHeight);
	...
	performDraw();
	...
}

再来看看getRootMeasureSpec方法的参数名,以及对rootDimension的分支判断。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        //最终会走到这个分支
        case ViewGroup.LayoutParams.MATCH_PARENT:
        	measureSpec = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.EXACTLY);
        break;
        ...
    }
    return measureSpec;
}

相信你能推断出来了,Activity中lp.width=lp.height=ViewGroup.LayoutParams.MATCH_PARENT,mWidth是屏幕宽度,mHeight是屏幕高度。在getRootMeasureSpec方法中调用了View.MeasureSpec.makeMeasureSpec方法完成mode和size打包成一个32位整数,它就是传递给DecorView初始的两个布局约束(精确模式的屏宽和精确模式的屏高)。

终于可以安心进入onMeasure方法看看了,但是要注意,DecorView→FrameLayout→ViewGroup→View,所以调用的是哪一个onMeasure方法呢?实际上DecorView和FrameLayout都重写了onMeasure方法,**ViewGroup虽然自身没有重写onMeasure方法,ViewGroup内部提供了很多关于递归测量子View的方法,继承了ViewGroup的类都会重写onMeasure方法,在onMeasure方法中调用ViewGroup提供的方法来完成测量工作。**由于继承了ViewGroup的类的onMeasure方法核心思想都差不多,这里就拿AbsoluteLayout(ViewGroup子类)的源码举例。

显然ViewGroup和View的测量工作是不一样的,View只用测量自身。ViewGroup测量自身的同时还要让自己所有的子View(或ViewGroup)完成他们的测量。因此整颗View树的测量像是一个递归的过程。

来看看AbsoluteLayout重写过的onMeasure方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	...
    // 测量所有子View的入口
    measureChildren(widthMeasureSpec, heightMeasureSpec);
	...
    //所有子View都测量完毕后,会在resolveSizeAndState测量ViewGroup自身
    //调用setMeasuredDimension设置测量结果,一旦调用该方法意味着这个View的测量结束
    //PS:setMeasuredDimension方法必须在onMeasure方法中调用,不然会抛异常。
    setMeasuredDimension(
        resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
        resolveSizeAndState(maxHeight, heightMeasureSpec, 0)
    );
}

大部分ViewGroup子类最终都会调用resolveSizeAndState方法来测量自身的,我们晚点再来看看这个方法,先看看它是如何让完成每个子View的测量

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            //测量每个子View的入口
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
    //子View获取自身的LayoutParams
    final LayoutParams lp = child.getLayoutParams();
	//获取父级对每个子View的布局约束(或者说:计算传入每个子View的measure方法的MeasureSpec)
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);
    //子View调用自身的measure完成自身的测量工作
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

核心部分的来了,让我们来看看getChildMeasureSpec方法是如何使用父级的布局约束和子View的LayoutParams来获得父级对每个子View的布局约束(或者说:使用传入ViewGroup的MeasureSpec和每个子View的LayoutParams计算出传入每个子View的MeasureSpec)

在FrameLayout中:onMeasure→measureChildWithMargins→getChildMeasureSpec

再次说明:measureChildren、measureChild、measureChildWithMargins、getChildMeasureSpec都是ViewGroup中的方法,ViewGroup不同的子类很多都是重写onMeasure方法,然后调用ViewGroup提供的这些方法。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //这里的spec是父布局对ViewGroup的布局约束,我们要计算获得父级对子View的布局约束
    //获取spec的Mode和Size
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
	//获取减去父级padding后的Parent剩余的大小,不足0取0
    int size = Math.max(0, specSize - padding);

    //定义返回值
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    //当父布局约束是精确模式时
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {//LayoutParams中的常量都是<0的
            //当前子View有确切大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //当前子View布局是MATCH_PARENT
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //当前子View布局是WRAP_CONTENT
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    //当父布局约束是最大值模式时
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    //不指定测量模式,系统中使用,开发中很少会用到
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    
    //返回父级对子View的布局约束
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

如果子View是ViewGroup的话就会和上面一样一直递归调用,直到View树的叶子结点,也就是View。让我们看看View的onMeasure方法拿到父级对自己的布局约束会怎么完成测量。

View的onMeasure方法:

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

一样setMeasuredDimension是用来设置测量结果的,我们先看看简单的getSuggestedMinimumWidth()方法

//返回android:minWidth属性的值与背景图片宽度两者中较大的值,如果没有设置背景的话直接返回mMinWidth(默认0)
protected int getSuggestedMinimumWidth() { 
	return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); 
} 

mBackgroud是Drawable对象。这里就不深入Drawable了,来看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:
        //UNSPECIFIED模式返回getSuggestedMinimumWidth()方法的返回值
    	result = size;
    	break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        //另外两种模式都返回specSize
    	result = specSize;
    	break;
  }
  //返回View的测量大小
  return result;
}

可以看到,两种模式都是返回父布局的大小,也就是自定义View的话即使你是wrap_content,也会按match_parent处理。因此根据需要可以在自定义View中增加对AT_MOST这种情况的处理。

最后我们再回过头来看看当子View完成测量后,ViewGroup是如何完成对自身的测量的。即前面的resolveSizeAndState方法。

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    //解包父级对ViewGroup的约束条件
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    //定义返回值
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            //父级wrap_content情况下
            if (specSize < size) {
                //父级约束小于ViewGroup想要的大小,取父级约束的大小
                //多出来的一些位可以设置一些标志
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                //取ViewGroup想要的大小
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            //父级约束有确切大小情况下,ViewGroup取父级约束的大小
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    //返回ViewGroup的测量大小
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

测量过程总结:

  • ViewGroup接受来自父级的布局约束(即ViewGroup的MeasureSpec)让子View们计算自己的MeasureSpec(根据ViewGroup的MeasureSpec和自身的LayoutParams),然后再递归调用测量的方法。

  • 直到传递到View树的叶结点,在View.onMeasure方法中,根据自己的MeasureSpec得到自己的测量大小

  • 待ViewGroup的所有子View都得到自己的测量大小后,ViewGroup再计算自己的测量大小。

3.2 layout布局

layout流程的基本思想和measure差不多,也是由根View开始,递归地完成整个View树的布局工作。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
	...
	final View host = mView;
	if (host == null) {
		return;
	}
	...
	host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
	...
}

View的layout方法时可以被重写,ViewGroup简单封装了一点点,基本还是调用View的layout方法,DecorView和FrameLayout都没有重写。因此主要还是调用了View的layout方法。

public void layout(int l, int t, int r, int b) {
    // l为本View左边缘与父View左边缘的距离
    // t为本View上边缘与父View上边缘的距离
    // r为本View右边缘与父View左边缘的距离
    // b为本View下边缘与父View上边缘的距离
    ...
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
		...
    }
    ...
}

setFrame()方法会设置View的mLeft、mTop、mRight和mBottom四个参数,分别描述了View相对父View的位置,setFrame()方法还会判断是否需要重新布局。然后进入onLayout方法。

View的onLayout方法是一个空实现,ViewGroup 则是一个抽象方法,要求其子类必须重写onLayout函数。ViewGroup的之类重写的onLayout的思路都差不多,递归遍历子View的onLayout方法。

//基本逻辑
protected void onLayout(boolean changed,int l, int t, int r, int b) {
    int childCount = getChildCount();
    for (int i = 0;i < childCount; i++){
        View child = getChildAt(i);
        child.layout(l, t, r, b);
    }
}

简单看一下FrameLayout的实现:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

	//父级的上下左右
    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

	//计算每个子View相对于父View的位置
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

			//子View测量的宽高
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

			//结合特定布局的属性,像这里FrameLayout就结合layout_gravity属性等。
            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

			//计算完后,交给子View的layout方法中的setFrame方法来设置布局,最终完成布局流程
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}
3.3 draw绘制

从performDraw()开始一路跳转

private void performDraw() {
    ...
    mFullRedrawNeeded = false;
    boolean canUseAsync = draw(fullRedrawNeeded);
    ...
}
private boolean draw(boolean fullRedrawNeeded) {
    ...
    drawSoftware(surface, mAttachInfo, xOffset, yOffset,scalingRequired, dirty, surfaceInsets))
    ...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
	...
	mView.draw(canvas);//调用DecorView的draw方法
    ...
}

调用DecorView的draw方法,实际是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);

    // skip step 2 & 5 if possible (common case)
    ...
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);
		...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
		...
        	return;
        }
    ...
	//这后面是一个更完整的流程,但是一般不会走到这。
}

一路跳转到draw(Canvas canvas)方法才开始绘制,绘制一共有6个步骤(注释是六步,其实现在有七步),一般情况下会跳过2和5。我们来看剩下的5个步骤。

第一步:绘制背景
//调用Drawable的draw() 把背景图片画到画布上
background.draw(canvas); 

第二步 保存canvas层

第三步:绘制View自身的内容
// Step 3, draw the content
onDraw(canvas);

View.onDraw(canvas) 方法是个空实现,ViewGroup也没有重写。通常我们自定义View就可以重写onDraw方法来绘制自己想要的内容。

第四步 对所有子View进行绘制
// Step 4, draw the children
dispatchDraw(canvas);

View没有子View,因此其dispatchDraw(canvas)方法是空实现,ViewGroup中则重写了此方法。具体的代码逻辑就是遍历子View,调用子View.draw()方法。

第五步 如果需要,绘制View的褪色边缘

第六步 绘制装饰,比如对View的滚动条进行绘制

调用View的onDrawForeground绘制装饰

第七步 绘制默认的焦点高光
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值