1. View的层级关系
在正式分析View的绘制流程之前,我们先来了解一下View的层级视图关系。
先看一下View层级关系图:
Activity在创建的同时会创建一个Window,在这个Window中我们所看到的DecorView就是最顶层的View,而且它其实是一个FrameLayout。在Android的源码中我们可以看到DecorView是继承FrameLayout实现的。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
// ......
}
在这个层级视图中我们还可以看到DecorView中包含了一个LinearLayout,这个LinearLayout又包含了两个FrameLayout:
上面部分的是TitleBar,可以通过在AndroidManifest.xml文件中application中设置无TitleBar的theme;也可以在Activity中单独设置某个Activity的theme。
<application
android:name=".xxApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:supportsRtl="true"
android:theme="@style/AppTheme">
下面的FrameLayout部分(android.R.id.content
)就是我们通常编写的layout内容,从代码上直观来说的话就是我们setContentView (layoutId)
添加的布局。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
从源码中我们可以看到setContentView()
这个方法其实就是将我们的布局加载到了名为:android.R.id.content
的ViewGroup中。
2. DecorView、WindowManager和ViewRoot三者之间的关系
上文已经我们已经知道了DecorView是最顶层view,承载着我们编写的layout。
WindowManager是一个继承ViewManager的系统服务,主要用来管理窗口的状态、属性、View的增加删除更新、消息处理。
package android.view;
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
//......
}
ViewRoot实现了ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的绘制是有ViewRoot来完成的。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
当Activity对象创建完毕后,DecorView会被添加到Window中并创建ViewRootImpl对象,然后将ViewRootImpl对象和DecorView关联在一起。
这里有涉及到Window的概念,在Android的源码中Window
是一个抽象类PhoneWindow
是它的唯一实现(源码中对Window的注释自己说的),它是窗口外观和行为策略的抽象基类。它提供了标准的UI策略,例如背景,标题区域,默认密钥处理等。
总的来说View的绘制流程可以用三个方法进行概括:
- measure:测量View的宽高
- layout:确定View的位置
- draw:绘制View的内容
View的绘制都是通过ViewRoot来负责的。每个应用程序窗口DecorView都有一个与之关联的ViewRoot对象,他们之间通过WindowManager建立关联。
View的绘制流程从ViewRoot的performTraversals
开始,performTraversals会依次调用performMeasure
、performLayout
和performDraw
。这三个方法会依次完成整个View树的measure、layout和Draw。
我们以performMeasure()
方法为例,该方法会调用measure
方法,measure
方法又会去调用onMeasure
方法;onMeasure
方法则会对所有子元素进行measure,其子元素又会重复这个过程。
performLayout和performDraw这两个方法的过程和performMeasure是一样的。
1. View的大小测量——measure
在View的大小测量中有一个关键的类MeasureSpec。MeasureSpec是一个32位的二进制数据,主要由两部分组成:
低30位,表示测量大小
size
。高2位(即,第31、32位),表示测量模式
mode
。
其中Mode分为三类:
UNSPECIFIED:父容器没有对View进行任何约束,View可以是任意大小,一般用于系统内部(如ListView、ScrollView
)。
EXACTLY:View的大小是确定的(如:match_parent
或具体的数值10dp
),它的最终大小就是SpecSize指定的大小。
AT_MOST:View的尺寸最多不能大于父容器的大小,对应于wrap_content
。
一个View大小的确定是通过其本身的LayoutPrarms
和父布局的MeasureSpec
共同决定的。
在View
中onMeasure
方法决定了View
的大小,其主要是依靠setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
来完成的。onMeasure
的默认实现如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在源码中我们还看到了getDefaultSize()
,在这个方法中根据specMode
重新确定了View
的大小。
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
的源码中,当 SpecMode
为 MeasureSpec.UNSPECIFIED
时View的大小使用的是getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
即为:视图应使用的建议最小宽度或高度。当 SpecMode
为 MeasureSpec.AT_MOST或MeasureSpec.EXACTLY
时都是直接使用specSize
。如果specMode
为EXACTLY
自然是没有问题的,但是当View的specMode
是AT_MOST
时,该View的宽或高会等于父容器的大小。
因此我们在自定义View时,可以根据情况重写onMeasure()
方法(如果自定义View的大小和父控件的测量规则一致,就不需要重写):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//不调用父类的方法 使用我们自定义的规则 确定控价的大小
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int width = 100;
int height = 100;
setMeasuredDimension(
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()
);
}
以上代码没有实际意义仅作示例使用,不同的自定义View
具有不同的规则,需要根据自己的情况来写。
2.View的位置确定 —— layout
从整个绘制流程上来说,layout的流程和measure是类似的。都是从performTraversals()
方法开始的,调用内部的performLayout()
然后在performLayout()
方法中调用layout()
,然后对子View进行遍历确定它们d的位置。
3.View的绘制 —— draw
和measure()
、layout()
一样,View的绘制也是从ViewRootImpl中的performTraversals()
方法中开始的,然后遍历绘制所有的子View。
整个绘制的过程,共分为六个步骤(其中第2和5步可以忽略),因此我们只需要关注其中四个步骤:
drawBackground(canvas)
;onDraw(canvas)
,每个View各不相同需要自己重写绘制的方法;dispatchDraw(canvas)
,如果没有子View就不需要绘制;onDrawScrollBars()
,在源码中我们可以看到其实任何View都有水平和垂直滚动条,只是没有显示而已。4.invalidate、postInvalidate和requestLayout的作用
invalidate
:执行draw的过程,重新绘制。
当View的appearance(外观、外形)发生改变时,比如状态(enable、focus)、背景改变、隐藏显示改变,这些都属于appearance,都会引起invalidate操作。
当我们需要改变View的界面内容时,可以通过调用invalidate。
View调用invalidate重新绘制自身,ViewGroup调用则会重绘整个View树。
postInvalidate
:在非UI线程中通过调用postInvalidate
完成重新绘制,在UI线程中可以直接调用invalidate
进行重新绘制。
requestLayout
:View的边界发生变化(宽高发生变化),可以调用requestLayout重新进行布局。
View调用
requestLayout
时,会向上递归到顶层View中,然后调用顶层View的requestLayout
方法,对整个界面进行重新测量和布局。可能会调用onMeasure和onLayout,不会调用onDraw。
如果你想更详细和深入的了解View的绘制原理,可以关注一下这篇博客——Android应用层View绘制流程与源码分析。