Android 之 View 绘制流程

Android 之 View 绘制流程

记忆点:

Android 界面是怎么显示的? -> setContentView()涉及到 LayoutInflater -> View 的绘制流程

setContentView 与 LayoutInflater

getWindow().setContentView()

Window 类的一些关系

Activity 中有一个成员为 Window, 其实例化对象为 PhoneWindow, PhoneWindow 为抽象类 Window 的实现类:

  1. Window 是一个抽象类,提供了绘制窗口的一组通用 API
  2. PhoneWindow 是 Window 的具体继承实现类,而且该类内部包含了一个 DecorView 对象,该 DecorView 对象是所有应用窗口(Activity 界面)的根 View
  3. DecorView 是 PhoneWindow 的内部类,是 FrameLayout 的子类,是所有应用窗口的根 View

窗口 PhoneWindow 的setContentView()

Activity 中三个重载的setContentView():setContentView(int layoutResID)setContentView(View view)setContentView(View view, ViewGroup.LayoutParams params)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public voids setContentView(int layoutResID){
if(nContentParent == null){
// 是否是第一次调用
installDecor();
}else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 默认为 false
mContentParent.removeAllViews();
}

if(hasFeature(FEATURE_CONTENT_TRANSITIONS)){
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
transitionTo(newScene);
}else {
// mLayoutInflater 是在 PhoneWindow 的构造函数中得到实力对象的 LayoutInflater.from(context);
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if(cb != null && !isDestroy()){
cb.onContentChanged();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void setContentView(View view, ViewGroup.LayoutParams params){
if(mContentParent == null){
// 配置样式、Activity 主题等
installDecor();
}else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if(hasFeature(FEATURE_CONTENT_TRANSITIONS)){
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
}else {
mContentParent.addView(view, params);
}
final Callback cb = getCallback();
if(cb != null && !isDestroy()){
cb.onContentChanged();
}
}

setContentView(View view)内部也是调用setContentView(View, ViewGroup.LayoutParams params),只是 LayoutParams 设置为了 MATCH_PARENT 而已。在程序中可多次调用setContentView(),因为会removeAllViews();流程可概括为:

  1. 创建一个 DecorView 的对象 mDecor,该对象作为整个应用窗口的根视图。
  2. 根据 Feature 等 style theme 创建不同的窗口修饰布局文件,并通过findViewById()获取 Activity 布局文件存放的地方(窗口修饰布局文件 id 为 content 的 FrameLayout 内)。

当启动 Activity 掉运完 ActivityThread 的main()方法之后,接着调用 ActivityThread 类的performLaunchActivity()来创建需要启动的 Activity 组件,在创建 Activity 组件的过程中,还会为该 Activity 组件创建窗口对象和视图对象,接着 Activity 组件创建完成后,通过调用 ActivityThread 的hadnleResumeActivity()将它激活,在方法中r.activity.makeVisible()显示我们上面setContentView()创建的 mDecor 视图族。

LayoutInflater 机制原理

获取 LayoutInflater 实例的两种写法:

1
2
LayoutInflater lif = LayoutInflater.from(context);//其实就是对下面方式的封装
LayoutInflater lif = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

layoutInflater.inflate()

1
2
3
public View inflate (int resource, ViewGroup root){
return inflate(resource, root, root != null);
}

layoutInflater.inflate(resource, root)->layoutInflater.inflate(resource, root, true/false)

1
2
3
4
5
6
7
8
9
10
11
public View inflate(int resource, ViewGroup root, boolean attachToRoot){
final Resources res = getContext().getResources();
...
//Android 默认实现为 Pull 解析的实现类 XmlPullParser
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

layoutInflater.inflate(resource, root)->layoutInflater.inflate(resource, root, attachToRoot)->inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot){
synchronized(mContructorArgs){
Trace.tarceBegin(Trace.TRACE_TAG_VIEW, "inflate");

final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mContructorArgs[0];
mContructorArgs[0] = mContext;
View result = root;

try {
int type;
// 找根节点
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();
if(TAG_MERGE.equals(name)){
if(root == null || !attachToRoot){
throw new InflateException("merge can be used only with a valid ViewGroup root and attach = true");
}
rInflate(parser, root, attrs);
}else {
// 方法内部又会去调用 createView()
// 然后使用反射的方式创建出 View 的实例并返回
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if(root != null){
params = root.generateLayoutParams(attrs);
if(!attachToRoot){
temp.setLayoutParams(params)
}
}
rInflate(parser, temp, attrs);
if(root != null && attachToRoot){
root.addView(temp, params);
}
if(root == null || !attachToRoot){
result = temp;
}
}
}catch(XmlPullParserException e){
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
}catch(IOException e){
InflateException ex = new InflateException(parser.getPositionDescription() + ": " + e.getMessage());
ex.initCause(e);
throw ex;
}
return result
}
}

LayoutInflater 使用的是 Android 提供的 pull 解析方式来解析 xml 布局文件的。以上只是创建了一个根布局实例而已,接下来会调用rInflate()方法来循环遍历这个根布局下的子元素。其中获取 View 的方法是createViewFromTag(),通过反射的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs){
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_INCLUDE.equals(name)){
if(parser.getDepth() == 0){
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
}else if(TAG_MERGE.equals(name)){
throw new InflateException("<merge /> must be the root element");
}else{
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGruop.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs);
viewGroup.addView(view, params);
}
}
parent.onFinishInflate();
}

由上可以看出,布局嵌套过深不仅效率低还可能引起调运栈溢出。同时在解析 tag 时也有一些特殊处理(include\merge)。布局优化可以通过 hierarchyviewer 来查看,通过 Lint 也可以自动检查出来一些。关于布局优化

setContentView()整个过程主要是如何把 Activity 的布局文件或者 Java 的 View 添加至窗口:

  1. 创建一个 DecorView 的对象 mDecor,该对象作为整个应用窗口的根视图;
  2. 依据 Feature 等 style theme 创建不同的窗口修饰布局文件,并通过 findViewById 获取 Activity 布局文件该存放的地方(窗口修饰布局文件中 id 为 content 的 FrameLayout);
  3. 将 Activity 的布局文件添加到 id 为 content 的 FrameLayout 内;
  4. setContentView()设置显示为 OK 以后会回调 Activity 的onContentChanged()。Activity 的各种 View 的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

View 渲染关系

View 绘制流程与源码分析:

每一个 View 绘制都经历三个最主要的过程:measure -> layout -> draw
整个 View 树的绘图流程是在 ViewRootImpl 类的performTraversals()开始的。该函数功能主要是根据之前设置的状态,判断是否重新计算视图的大小measure,是否重新安放视图位置layout,以及是否重绘draw

1
2
3
4
5
6
7
8
9
10
11
12
private void performTraversals(){
...
// lp.width 和 lp.height 在创建 ViewGroup 实例的时候就被赋值了,都等于 MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
...
mView.draw(canvas);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension){
int measureSpec;
switch(rootDimension){
case ViewGroup.LayoutParams.MATCH_PARENT:
// RootView 全屏的原因:windowSize, EXACTLY.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
...
}
return measureSpec;
}

其中 mView 就是 View 对象。以下就是整个流程的大致流程图:

大致流程图

View 绘制第一步measure

measure 流程

View 的measure()

1
2
3
4
5
public final void measure(int widthMeasureSpec, int heightMeasureSpec){
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

为整个 View 树设置实际的阿晓,然后设置实际的高和宽,每个 View 控件的实际宽高都是由父视图和自身决定的measure()不可被重写,实际的测量是在onMeasure()中进行。
关于 widthMeasureSpec/heightMeasureSpec,这两个参数是从父 View 传递过来,代表了父 View 的规格。

MeasureSpec: 32位,也就是 specMode + specSize,高2位表示 MODE,低30位表示 size (也就是父 View 的大小)
MeasureSpec.EXACTLY:表示父视图希望子视图的大小应该是由 specSize 的值来决定的,系统会默认按照这个规则来设置子视图的大小,开发人员也可按照自己的意愿设置成任意大小。
MeasureSpec.AT_MOST:表示子视图最多只能是 specSize 中指定的大小,开发人员应该尽可能小的去设置这个视图,并且保证不会超过 specSize。系统会默认按照这个规则来设置子视图大小,开发人员也可以按照自己的意愿设置成任意大小。
MeasureSpec.UNSPECIFIED:表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

measure()->onMeasure()

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

onMeasure()是真正去测量并设置 View 大小的地方,默认会调用getDefaultSize()方法来获取视图的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

这里传入的 measureSpec 是一直measure()中传递过来的。系统这时默认行为是如果传递过来的 specMode 是 AT_MOST 或是 EXACTLY 就返回 specSize。之后通过setMeasuredDimension()来设定测量出的大小,这样一次measure过程就结束了。而一个布局中一般都会包含多个子视图,每个视图都需要一次measure的过程。在 ViewGroup 中定义了measureChild()measureChildren()measureChildWithMargins()方法来对子视图进行测量。measureChildren()内部实质只是循环调用了measureChild(),measureChild()measureChildWithMargins()的区别只是是否把 margin 和 padding 也作为子视图的大小。

1
2
3
4
5
6
7
8
9
10
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) != GONE){
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

1
2
3
4
5
6
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec){
final LayoutParams lp = child.getLaoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
child.measure(chlidWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec()去计算子视图的 MeasureSpec,计算的依据是布局文件中定义的 MATCH_PARENT、WRAP_CONTENT 等值。然后再调用子视图的measure(),并把计算出的 MeasureSpec 传递进去,之后的流程与前面介绍的相同。

measure() 原理总结

从顶层父 View 向子 View 递归调用view.measure()measure()中又回调onMeasure()

  • MeasureSpec:EXACTLY、AT_MOST、UNSPECIFIED
  • View 的measure()是 final 的,不允许重写,View 子类只能重写onMeasure()来完成自己的测量逻辑
  • 顶层 DecorView 测量时的 MeasureSpec 是由 ViewRootImpl 中的getRootMeasureSpec()方法确定的(LayoutParams 宽高参数均为 MATCH_PARENT,specMode 是 EXACTLY,specSize 为物理屏幕的大小)。
  • 使用 View 的getMeasuredWidth()getMeasuredHeight()方法来获取 View 测量的宽高,必须保证这两个方法在onMeasure()流程之后被调用才能返回有效值。

View 绘制第二步layout

当 ViewRootImpl 的performTraversals中的measure()执行完成以后会接着执行mView.layout()

1
2
3
4
5
private void performTraversal(){
...
mView.layot(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
...
}

layout()接收的四个参数,分别代表着左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的

整个 View 树的layout()递归流程:

layout 递归流程

layout 也是递归结构,ViewGroup 的layout()实际还是调用的 View 父类的layout()方法

1
2
3
4
5
6
7
8
9
10
11
public void layout(int l, int t, int r, int b){
...
//实际都是调用`setFrame()`把参数分别赋值给 mLeft、mTop、mRight 和 mBottom 这几个变量
//判断 View 位置是否发生过变化,以确定有没有必要对当前的 View 进行重新`layout()`
boolean changed = isLayoutModeOptical(mParent) ? setOptionalFrame(l, t, r, b) : setFrame(l, t, r, b);
if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED){
onLayout(changed, l, t, r, b);
...
}
...
}
1
2
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

ViewGroup 中的onLayout()竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法,在内部按照自己的规则对子视图进行布局。以 LinearLayout 为例子:

1
2
3
4
5
6
7
8
9
10
public class LinearLayout extends ViewGroup{
@Override
protected void onLayout(boolean changed, int l, int t, int t, int b){
if(mOrientation == VERTICAL){
layoutVertical(l, t, r, b);
}else{
layoutHorizontal(l, t, r, b);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void layoutVertical(int left, int top, int right, int bottom){
final int paddingLeft = mPaddingLeft;

int childTop;
int childLeft;

final int width = right - left; //计算父窗口推荐的子 View 的宽度
final int childRight = width - mPaddingRight; //计算父窗口推荐的子 View 右侧位置

int childSpace = width - paddingLeft - mPaddingRight; //child 可使用空间的大小
final int count = getVirtualChildCount(); // 获取子 View 个数
//获取 Gravity 属性设置
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch(majorGravity){
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for(int i=0; i<count; i++){
final View child = getVidtualChildAt(i);
if(child == null){
childTop += measureNullChild(i);
}else if(child.getVisibility() != GONE){
// LinearLayout 中子视图显示的宽和高由`measure()`过程来决定的。因此`measure()`过程的意义就是为`layout()`过程提过视图显示范围的参考值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)child.getLayoutParams();

int gravity = lp.gravity;
if(gravity < 0){
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGraviry(gravity, layoutDirection);
switch(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK){
case Gravity.CENTER_HORIZONTAL:
childLeft += paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

if(hasDividerBeforeChildAt(i)){
childTop += mDividerHeight;
}

childTop += lp.topMargin;

//通过垂直排列计算调运 child 的 layout 设置 child 的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}
}

以上可以看到,一般情况下layout()过程会参考measure()过程中计算得到的 mMeasuredWidth 和 mMeasuredHeight 来安排子 View 在父 View 中显示的位置,但这不是必须的,measure()过程得到的结果可能完全没有实际用处。

layout()原理总结
  • View 的 layout()方法可被重写,但是 ViewGroup 的layout()为 final,ViewGroup 的onLayout()为 abstract 的,子类必须重写实现自己的布局逻辑
  • measure()操作后得到的是每个 View 经过测量过的 measuredWidth 和 measuredHeight,layout()操作之后得到的是对每个 View 进行位置分配后的 mLeft、mTop、mRight、mBottom,这些都是相对父 View 来说的
  • 凡是 layout_XXX 的布局属性基本都是针对的是包含子 View 的 ViewGroup 的。为一个没有父容器设置相关的 layout_XXX 属性是没有任何意义的
  • 使用 View 的getWith()getHeight()方法来获取 View 测量的宽高,必须保证这两个方法在onLayout()流程之后被调用才能返回有效值

View 绘制第三步draw

draw()过程也是在 ViewRootImpl 的performTraversals()内部调用的,其调用顺序在measure()layout()之后。

1
2
3
4
5
6
7
8
9
private void performTraversals(){
...
final Rect dirty = mDirty;
...
canvas = mSurface.lockCanvas(dirty); // ViewRootImpl中的代码会创建一个 Canvas 对象
...
mView.draw(canvas); // 对于 Activity 来说就是 PhoneWindow.DecorView
...
}

View 递归 draw 流程:

递归 draw 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void draw(Canvas canvas){
...
/**
* Draw taversal performs serveral drawing steps which must be executed in the approptiate order:
*
* 1.Draw the background
* 2.If necessary, save the canvas's 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 (scroller for instance)
*/
...
// Step 1
if(!dirtyOpaque){
drawBackground(canvas);
}
...
// Skip step 2 & 5 if possible (common case)

// Step 2
...
if(drawTop){
canvas.saveLayer(left, top, right, top + length, null, flags);
}
...
// Step 3
if(!dirtyOpaque){
onDraw(canvas);
}

// Step 4
dispatchDraw(canvas);
...
// Step 5
if(drawTop){
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
...
// Step 6
onDrawScrollBars(canvas);
}
第一步,对 View 的背景进行绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void drawBackground(Canvas canvas){
// 获取 xml 中通过 android:background 属性或者代码中`setBackgroundColor()`、`setBackgroundResource()`等方法进行赋值的背景 Drawable
final Drawable background = mBackground;
...
if(mBackgroundSizeChanged){
// 根据`layout()`过程确定的 View 位置来设置背景的绘制区域
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOurline();
}
...
background.draw(canvas); //调用 Drawable 的`draw()`来完成背景的绘制工作
...
}
第三步,对 View 的内容进行绘制(第二、第五步可以跳过)
1
protected void onDraw(Canvas canvas){}

空方法。这是因为每个 View 的内容部分是各不相同,需要具体子类实现具体逻辑

第四步,对 View 的所有子 View 进行绘制
1
protected void dispatchDraw(Canvas canvas){}

也是一个空方法。如果 View 包含子 View 需要重写它,看看 ViewGroup 的dispatchDraw()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void dispatchDraw(Canvas canvas){
...
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
...
for(int i = 0; i < childrenCount; i++){
...
if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null){
more |= drawChild(canvas, child, drawingTime);
}
}
...
// Draw any disappearing views that have animations
if(mDisappearingChildren != null){
...
for(int i = disappearingCount; i >= 0; i--){
...
more |= drawChild(canvas, child, drawingTime);
}
}
...
}

ViewGroup 确实重写了 View 的dispatchDraw(),该方法内部会遍历每个子 View,然后调用drawChild()

1
2
3
protected boolean drawChild(Canvas canvas, View child, long drawingTime){
return child.draw(canvas, this, drawingTime);
}

drawChild()调用了子 View 的draw()。ViewGroup 类已经为我们重写了dispatchDraw()的功能实现,一般不需要重写该方法,但可以重写父类函数实现具体的功能

第六步,对 View 的滚动条进行绘制

onDrawScrollBars(canvas);

draw()原理总结:
  • 如果该 View 是一个 ViewGroup,则需递归绘制其包含的所有子 View
  • View 默认不会绘制任何内容,真正的绘制都需要在子类中实现
  • View 的绘制是借助onDraw()方法传入 Canvas 类进行的
  • 默认情况下的子 View 的 ViewGroup.drawChld 的绘制顺序和子 View 被添加的顺序一致,但是你可以重写ViewGroup.getChildDrawingOrder()方法提供不同的顺序。

视图状态和视图重绘

视图状态

常见几个:enabled(是否可用,不可用无法响应onTouch事件)、focused、window_focused、selected、pressed…

视图重绘

调用setVisibility()setEnabled()setSelected()等方法时都会导致视图重绘,而如果想要手动强制让视图进行重绘,可用通过调用invalidate()来实现。其实上述方法内部也是调用此方法实现重绘。各种重载的invalidate()最终调用的都是invalidateInternal()

1
2
3
4
5
6
7
8
9
10
11
12
13
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean full Invalidate){
...
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if(p != null && ai != null && l < r && t < b){
final Rect damage = ai.mTmpInvalRect;
// 设置刷新区域
damage.set(l, t, r, b);
// 传递调用 Parent ViewGroup 的刷新方法
p.invalidateChild(this, damage);
}
...
}

这是一个向上级父 View 回溯的过程,每一层的父 View 都将自己的显示区域与传入的刷新的 Rect 做交集。看看 ViewGroup 的 invalidateChild 方法:

1
2
3
4
5
6
7
8
9
public final void invalidateChild(View child, final Rect dirty){
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
...
do{
...
parent = parent.invalidateChildInParent(location, dirty);//循环层层上级调用,直到 ViewRootImpl 会返回 null
}while(parent != null);
}

以上过程最后传递到 ViewRootImpl 的invalidateChildInParent()方法结束

1
2
3
4
5
6
7
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty){
...
scheduleTraversals();// 会通过 Handler 的 Runnable 发送一个异步消息,调用`doTraversal()`,最终调用`performTraversals()`执行重绘。
...
return null;
}

postInvalidate()

实际就是子线程通过 ViewRootImpl 类的dispatchInvalidateDelayed(),通过 Handler 发送 MSG_INVALIDATE 消息,ViewRootImpl 中的 Handler 收到消息就又在 UI 线程中调用了 View 的invalidate()方法。

其他

setContentView()addView()会通过invalidate(true)触发 ViewRootImpl 类的`performTraversals()

requestLayout()层层上传到 ViewRootImpl 后触发 ViewRootImpl 中的requestLayout()中的scheduleTraversals()requestLayout()会调用measure()过程和layout()过程,但不会调用draw(),也不会重绘任何子 View包括调用者自身。


参考引用

Android LayoutInflater原理分析,带你一步步深入了解View(一)

Android应用setContentView与LayoutInflater加载解析机制源码分析

Android应用层View绘制流程与源码分析