0%

View绘制的layout过程

作用

计算视图(View)的位置

即计算View的四个顶点位置:Left、Top、Right 和 Bottom

layout过程详解

类似measure过程,layout过程根据View的类型分为2种情况:

layout过程的入口 = 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
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
public void layout(int l, int t, int r, int b) {

// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
...

}

/**
* 分析1:setFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
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);

}

/**
* 分析2:setOpticalFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {

Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;

Insets childInsets = getOpticalInsets();

// 内部实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到调用原处

/**
* 分析3:onLayout()
* 注:对于单一View的laytou过程
* a. 由于单一View是没有子View的,故onLayout()是一个空实现
* b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()
* 后就已完成了
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
}

ViewGroup的layout过程

步骤:

1. 计算自身ViewGroup的位置:layout()
2. 遍历子View & 确定自身子View在ViewGroup的位置(调用子View 的 layout()):onLayout()

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
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
* 注:与单一View的layout()源码一致
*/
public void layout(int l, int t, int r, int b) {

// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->>分析3
...

}

/**
* 分析1:setFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
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);

}

/**
* 分析2:setOpticalFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {

Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;

Insets childInsets = getOpticalInsets();

// 内部实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到调用原处

/**
* 分析3:onLayout()
* 作用:计算该ViewGroup包含所有的子View在父容器的位置()
* 注:
* a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
* b. 在自定义ViewGroup时必须复写onLayout()!!!!!
* c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置

// 1. 遍历子View:循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);

// 2. 计算当前子View的四个位置值
// 2.1 位置的计算逻辑
...// 需自己实现,也是自定义View的关键

// 2.2 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom

// 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
// 即确定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
}
}
}

此处需注意:
ViewGroup 和 View 同样拥有layout()和onLayout(),但二者不同的:

一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
当开始遍历子View & 计算子View位置时,调用的是子View的layout()和onLayout()

细节问题:getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?

首先明确定义:

getWidth() / getHeight():获得View最终的宽 / 高
getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高

先看下各自的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获得View测量的宽 / 高
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredWidth
}

public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredHeight
}


// 获得View最终的宽 / 高
public final int getWidth() {
return mRight - mLeft;
// View最终的宽 = 子View的右边界 - 子view的左边界。
}

public final int getHeight() {
return mBottom - mTop;
// View最终的高 = 子View的下边界 - 子view的上边界。
}

一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?

答:人为设置:通过重写View的 layout()强行设置

1
2
3
4
5
6
7
8
9
10
11

@Override
public void layout( int l , int t, int r , int b){

// 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100);

// 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
// 即:View的最终宽/高 总比 测量宽/高 大100px

}

在非人为设置的情况下,View的最终宽/高(getWidth() / getHeight())
与 View的测量宽/高 (getMeasuredWidth() / getMeasuredHeight())永远是相等

赞赏是最好的支持