View 的工作原理

初识 ViewRoot 和 DecorView

ViewRoot

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带。
在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象并将其和 DecorView 建立关联。

View 的三大流程均是通过它来完成的

  • Measure:测量 View 的宽/高
    可通过 getMeasuredWidth 和 getMeasuredHeight 获取 View 测量后的宽/高。
  • Layout:确定 View 在父容器中的放置位置
    可通过 getTop 等方法获取四个顶点的位置。
    可通过 getWidth 和 getHeight 方法获取 View 的最终宽/高。
  • Draw:将 View 绘制在屏幕上
    此方法完成后 View 的内容才能呈现在屏幕上。

View 绘制流程由 ViewRoot 的 performTraversals 方法开始 –> performMeasure –> measure –> onMeasure –> 对所有子元素进行 measure 过程 –> 接着由子元素重复这个过程,直到 View 树的遍历。其它两个流程类似,只不过 performDraw 的传递过程在 draw 方法中通过 dispatchDraw 来实现的。

DecorView

顶级 View,其实是一个 FrameLayout,一般内部包含一个竖直方向的 LinearLayout(两部分,上面标题栏,下面内容栏。具体情况和版本与主题有关)。
在 Activity 中通过 setContentView 设置的布局就是被加到内容栏(id 为 content 的 FrameLayout)中。

1
2
3
4
5
// 获取 content
ViewGroup content = findViewById(android.R.id.content);

// 获取我们设置的 View
content.getChildAt(0);

理解 MeasureSpec

MeasureSpec

它代表一个 32 位 int 值,高 2 位代表 SpecMode(测量模式),低 30 位代表 SpecSize(在某种测量模式下的规格大小)。
SpecMode 有三类:

  • UNSPECIFIED
    父容器不会 View 有任何限制,一般用于系统内部,表示一种测量的状态,多次 Measure 的情形,一般不需要关注此模式。
  • EXACTLY
    父容器已经检测出 View 所需的精确大小,这时 View 的最终大小就是 SpecSize 所指定的值。
    它对应于 LayoutParams 中的 match_parent 和具体数值这两种模式。
  • AT_MOST
    父容器指定一个可用大小即 SpecSize,View 的大小不能大于这个值,具体什么值要看不同 View 的具体实现。
    它对应于 LayoutParams 中的 wrap_content。

MeasureSpec 和 LayoutParams 的对应关系

对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同决定。
DecorView 的 MeasureSpec 产生过程,遵守如下规则:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 固定大小(比如 100dp):精确模式,大小为 LayoutParams 中指定的大小。

对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定的。

  • 当 View 采用固定宽/高时:不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。
  • 当 View 的宽/高是 match_parent 时:如果父容器的模式是精准模式,那 View 也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那 View 也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当 View 的宽/高是 wrap_content 时:不管父容器的模式是什么,View 的模式总是最大化并且大小不能超过父容器的剩余空间。

View 的工作流程

measure 过程

View 的 measure 过程
由其 measure 方法(final 类型)完成,measure 会去调用 onMeasure 方法。
onMeausre –> 通过 setMeasuredDimension 设置 View 宽/高的测量值 –> 其参数为 getDefaultSize() –> 其内部会对三种模式进行赋值。

  • UNSPECIFIED
    View 宽/高为第一个参数也就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 的返回值。
    关于 getSuggestedMinimumWidth() ,如果 View 没有设置背景,则返回 android:minWidth 值,默认为 0,如果设置了背景,则返回 android:minWidth 和背景的最小宽度中的最大值。
  • 另外两种才需要关注
    View 的宽高由 specSize 决定。
    直接继承 View 的自定义控件需重写 onMeasure() 并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content(specMode 是 AT_MOST 模式) 就相当于使用 match_parent。

ViewGroup 的 measure 过程
除了自己的 measure 过程外,还会遍历调用所有子元素的 measure 方法(通过 measureChild())。
measureChild() –> 取出子元素 LayoutParams –> 通过 getChildMeasureSpec 创建子元素的 MeasureSpec –>将其传递给 View 的 measure() 进行测量

某些情况下,系统可能需要多次 measure 才能确定最终宽/高,这种情况下得到的值可能不准确。建议在 onLayout() 中获宽/高取值。

启动三回调中均无法正确得到某个 View 的宽/高信息,因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的。

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
70
71
72
73
74
75
76
77
78
79
/**
* 四种方式实现
* 获取 View 的宽/高信息
*/
public class MainActivity extends AppCompatActivity {

private TextView tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);

log("");
measure();
}

/**
* 一:此方法表示 View 已初始化完毕
* 当 Activity 的窗口得到和失去焦点时均会被调用一次
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
log("onWindowFocusChanged:");
}
}

@Override
protected void onStart() {
super.onStart();
// 二:通过 post 可将一个 runnable 投递到消息队列的尾部,
// 然后等待 Looper 调用此 runnable 时,View 已经初始化好了。
tv.post(new Runnable() {
@Override
public void run() {
log("post:");
}
});

// 三:
final ViewTreeObserver observer = tv.getViewTreeObserver();
// 此接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,会被调用。
// 伴随 View 树的状态改变等,会被调用多次。
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
tv.getViewTreeObserver().removeGlobalOnLayoutListener(this);
log("ViewTreeObserver:");
}
});
}

/**
* 四:通过手动对 View 进行 measure 来得到宽/高
*/
private void measure(){
// wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30)-1, View.MeasureSpec.AT_MOST);
tv.measure(widthMeasureSpec,heightMeasureSpec);
log("measure:");

// 具体的数值(dp/px)
// int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
// int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
// tv.measure(widthMeasureSpec,heightMeasureSpec);
// log("measure:");

// match_parent
// 直接放弃,无法得知 parentSize 大小
}

private void log(String str){
Log.e("TAG",str+" "+tv.getMeasuredWidth()+" "+tv.getMeasuredHeight());
}
}

layout 过程

它的作用是 ViewGroup 用来确定子元素的位置,在 onLayout() 中遍历所有子元素并调用其 layout(),在 layout() 会先通过 setFrame() 设置 View 的四个顶点位置,接着会调用 onLayout()。
layout() 确定本身位置,而 onLayout() 确定所有子元素位置。

draw 过程

View 的绘制过程:

  • 绘制背景:background.draw(canvas)
  • 绘制自己:onDraw(canvas)
  • 绘制:children:dispatchDraw(canvas)
    会遍历所有子元素的 draw()
  • 绘制装饰:onDrawScrollBars(canvas)

当一个 View 不需绘制任何内容,可通过 setWillNotDraw(boolean willNotDraw) 设置这个标记位为 true,系统会进行相应的优化。
默认,View 没启用这个标记位,而 ViewGroup 启动。


自定义 View

自定义 View 的分类

  • 继承 View 重写 onDraw()
    主要用于实现一些不规则效果。并且要自己支持 wrap_content 和 padding。
  • 继承 ViewGroup 派生特殊的 Layout
    主要用于实现自定义的布局。需要合适的处理 ViewGroup 和子元素的测量、布局这两个过程。
  • 继承特定 View (比如 TextView)
    一般用于扩展某种已有的 View 功能。这种不需自己支持 wrap_content 和 padding 等。
  • 继承特定 ViewGroup(比如 LinearLayout)
    这种方法不需自己处理 ViewGroup 的测量和布局两个过程。它与方法 2 的区别在于,后者更接近 View 的底层。

自定义 View 须知

  • 让 View 支持 wrap_content
  • 如有必要,让 View 支持 padding
  • 尽量不要在 View 中使用 Handler
  • 及时停止 View 中的线程和动画
  • 处理好滑动冲突

自定义 View 示例

1、继承 View 重写 onDraw()

attrs.xml
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<!--reference:资源id。dimension:尺寸-->
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
activity_main.xml
1
2
3
4
5
6
7
<com.example.myapplication.CircleView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:background="#000000"
android:layout_margin="20dp"
android:padding="10dp"
app:circle_color="@android:color/holo_green_light"/>
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
70
71
72
73
74
75
76
77
/**
* 绘制一个圆
*/

public class CircleView extends View {

private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

public CircleView(Context context) {
super(context);
init();
}

public CircleView(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}

/**
* 解析自定义属性的值,并做处理
*/
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加载自定义属性集合
TypedArray a= context.obtainStyledAttributes(attrs,R.styleable.CircleView);
// 解析属性
mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
// 释放资源
a.recycle();
init();

}

private void init() {
mPaint.setColor(mColor);
}

/**
* 在 onMeasure() 处理 wrap_content 问题
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

// 如果为 wrap_content,那给个默认值。
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,200);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,heightSpecSize);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,200);
}
}

/**
* 在 onDraw() 中处理 padding 问题
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();

int width = getWidth()-paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width,height)/2;

// 绘制一个圆
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
}
}

2、继承 ViewGroup 派生特殊的 Layout

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
public class HorizontalScrollViewEx extends ViewGroup{
private static final String TAG = "HorizontalScrollViewEx";

private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;

// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;

private Scroller mScroller;
private VelocityTracker mVelocityTracker;

public HorizontalScrollViewEx(Context context) {
super(context);
init();
}

private void init() {
if (mScroller == null){
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();

switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}

Log.e(TAG,"intercepted = "+intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
mVelocityTracker.addMovement(ev);
int x = (int) ev.getX();
int y = (int) ev.getY();

switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX,0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50){
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
}else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx,0);
mVelocityTracker.clear();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
// 得到子元素个数
final int childCount = getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);

// 这里测量宽/高时,没有考虑到它的 padding 和 子元素的 margin。
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 是否有子元素
if (childCount == 0){
// 没有子元素,就把自己宽高设为 0.
// 正确做法,应该是根据 LayoutParams 中的宽/高做相应处理。
setMeasuredDimension(0,0);
}else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth,measureHeight);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
// 高度为第一个子元素的高度
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize,measureHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
// 宽度为所有子元素的宽度之和
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth,heightSpecSize);
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;

// 遍历子元素
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
// 类似 LinearLayout ,从左向右放置子元素。
// 这里没考虑自身的 padding 和子元素 margin
childView.layout(childLeft,0,childLeft + childWidth,childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
private void smoothScrollBy(int dx,int dy){
mScroller.startScroll(getScrollX(),0,dx,0,500);
invalidate();
}

@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}

@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}

链接

参考资料:
Android 开发艺术探索