【view绘制流程】理解
一、概述
View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。
二、View组成架构
1.Window
Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。在介绍这个类之前,我们先来看看究竟什么是窗口呢?
实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:
- 独立绘制,不与其它界面相互影响;
- 不会触发其它界面的输入事件;
在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。
android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。
这个抽象类包含了三个核心组件:
- WindowManager.LayoutParams: 窗口的布局参数;
- Callback: 窗口的回调接口,通常由Activity实现;
- ViewTree: 窗口所承载的控件树。
下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。
2.PhoneWindow
前面我们提到了,PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。
3.setContentView()
在分析setContentView()方法前,我们需要明确:这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。
当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); . . . }
getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:
public void setContentView(int layoutResID) { if (mContentParent == null) {
// mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成 installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
// mContentParent不为null,则移除decorView的所有子View mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 开启了Transition,做相应的处理,我们不讨论这种情况
// 感兴趣的同学可以参考源码 ........ } else {
// 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
// 填充布局也就是把我们设置的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
// cb即为该Window所关联的Activity
final Callback cb = getCallback(); if (cb != null && !isDestroyed()) {
// 调用onContentChanged()回调方法通知Activity窗口内容发生了改变 cb.onContentChanged(); } mContentParentExplicitlySet = true; }
4.LayoutInflater.inflate()
在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); . . . final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { . . . final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; View result = root; try { // Look for the root node. int type; // 一直读取xml文件,直到遇到开始标记 while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } // 最先遇到的不是开始标记,报错 if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); . . . // 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明 if (TAG_MERGE.equals(name)) { // 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错 if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } // 递归地填充布局 rInflate(parser, root, inflaterContext, attrs, false); } else { // temp为xml布局文件的根View final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { . . . // 获取父容器的布局参数(LayoutParams) params = root.generateLayoutParams(attrs); if (!attachToRoot) { // 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View temp.setLayoutParams(params); } } // 递归加载根View的所有子View rInflateChildren(parser, temp, attrs, true); . . . if (root != null && attachToRoot) { // 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来 root.addView(temp, params); } // 若root为空或是attachToRoot为false,则以根View作为返回值 if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { . . . } catch (Exception e) { . . . } finally { . . . } return result; } }
在上面的源码中,首先对于布局文件中的<merge>标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { // 获取当前标记的深度,根标记的深度为0 final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { // 不是开始标记则继续下一次迭代 if (type != XmlPullParser.START_TAG) { continue; } final String name = parser.getName(); // 对一些特殊标记做单独处理 if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); } else if (TAG_TAG.equals(name)) { parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } // 对<include>做处理 parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else { // 对一般标记的处理 final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs); // 递归地加载子View rInflateChildren(parser, view, attrs, true); viewGroup.addView(view, params); } } if (finishInflate) { parent.onFinishInflate(); } }
我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { rInflate(parser, parent, parent.getContext(), attrs, finishInflate); }
从源码中我们可以知道,rInflateChildren()方法实际上调用了rInflate()方法。
到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来,我们开始进入正题,分析View的绘制流程。
5.ViewRoot
在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们直入主题,分析一下ViewRoot是如何完成View的绘制的。
6.View绘制的起点
当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 检查发起布局请求的线程是否为主线程 checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
三、三个阶段
View的整个绘制流程可以分为以下三个阶段:
- measure: 判断是否需要重新计算View的大小,需要的话则计算;
- layout: 判断是否需要重新计算View的位置,需要的话则计算;
- draw: 判断是否需要重新绘制View,需要的话则重绘制。
这三个子阶段可以用下图来描述:
1.Measure流程
顾名思义,就是测量每个控件的大小。
调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { }
measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?
如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。
MeasureSpec的确定
先介绍下什么是MeasureSpec?
MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。
其中,Mode模式共分为三类
UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
那么MeasureSpec又是如何确定的?
对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。
这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。
对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。
这部分比较复杂。以下列图表表示不同的情况:
当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。
从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。
可能关于MeasureSpec如何确定View大小还有些模糊,篇幅有限,没详细具体展开介绍,可以看这篇文章
View的测量流程:
2.Layout流程
测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。
其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。
public void layout(int l, int t, int r, int b) { // 当前视图的四个顶点 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // setFrame() / setOpticalFrame():确定View自身的位置 // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout() if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该View所有的子View在父容器的位置 onLayout(changed, l, t, r, b); ... }
上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。
protected boolean setFrame(int left, int top, int right, int bottom) { ... // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点 // 即确定了视图的位置 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); }
确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。
如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。
由此便完成了一层层的的布局工作。
View的布局流程:
3.Draw过程
View的绘制过程遵循如下几步:
①绘制背景 background.draw(canvas)
②绘制自己(onDraw)
③绘制Children(dispatchDraw)
④绘制装饰(onDrawScrollBars)
从源码中可以清楚地看出绘制的顺序。
public void draw(Canvas canvas) { // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法) // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。 // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。 ... int saveCount; if (!dirtyOpaque) { // 步骤1: 绘制本身View背景 drawBackground(canvas); } // 如果有必要,就保存图层(还有一个复原图层) // 优化技巧: // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过 // 因此在绘制的时候,节省 layer 可以提高绘制效率 final int viewFlags = mViewFlags; if (!verticalEdges && !horizontalEdges) { if (!dirtyOpaque) // 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写 onDraw(canvas); ...... // 步骤3:绘制子View 默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法 dispatchDraw(canvas); ........ // 步骤4:绘制滑动条和前景色等等 onDrawScrollBars(canvas); .......... return; } ... }
无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
View绘制流程:
五、总结
从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。
onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
**onLayout()方法:**单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
**onDraw()方法:**无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法