Android学习笔记---深入理解View#04

上次我们对View的测量过程有了了解,接着这次肯定就是要沿着View的3大流程往下走。我们本次的主角就是View的Layout过程。

performLayout()开始出发

我们的已经知道了View的布局过程layout pass就是从performLayout()开始的,那么我们就先来大概的浏览一下这个函数的代码。

TIP: 这里我们先简略的浏览整个函数的代码(不用认真看),后面我们会将代码分开几个部分,一个部分一个部分的进行分析。

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
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight)
{

// 先将当前是否有布局请求的flag设置为false
mLayoutRequested = false;

mScrollMayChange = true;
// 将当前正在布局的flag设置为true
mInLayout = true;
// 获取decorView
final View host = mView;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(TAG, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
// 对decorView进行布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 将当前正在布局的flag设置为false
mInLayout = false;
// 获取当前请求布局的View的个数
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// requestLayout() was called during layout.
// If no layout-request flags are set on the requesting views, there is no problem.
// If some requests are still pending, then we need to clear those flags and do
// a full request/measure/layout pass to handle this situation.
// 获取需要进行布局的View
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
// Set this flag to indicate that any further requests are happening during
// the second pass, which may result in posting those requests to the next
// frame instead
// 设置正在处理布局请求的flag为true
mHandlingLayoutInLayoutRequest = true;

// Process fresh layout requests, then measure and layout
// 获取需要进行布局的View的个数
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mHandlingLayoutInLayoutRequest = false;

// Check the valid requests again, this time without checking/clearing the
// layout flags, since requests happening during the second pass get noop'd
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
// Post second-pass requests to the next frame
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during second layout pass: posting in next frame");
view.requestLayout();
}
}
});
}
}

}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;

}

performLayout()的代码仔细看起来也并不难,我们一点一点的来进行分析。我们都知道这个函数是在ViewRootImplperformTravers()进行调用的。那我们来看看它的传入参数。

1
performLayout(lp, desiredWindowWidth, desiredWindowHeight);

分别是当前的窗口布局参数lp,窗口的宽度和高度desiredWindowWidth,desiredWindowHeight
既然知道了参数的意义,那我们就可以对performLayout()进行分析了。首先我们就来分析下面部分的代码(为了看起来更加的清晰,我将一些无关的代码去掉了,去掉的部分我用省略号代替):

1
2
3
4
5
6
7
8
9
10
11
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight)
{

.........
mInLayout = true;

final View host = mView;
........
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
.....

这里我们可以看到首先是设置了mInLayout = true,表示当前正在布局的过程当中。随后final View host = mView获取了我们的decorView,然后就调用了我们host.layout()对我们的decorView进行布局。布局完成后就将我们的mInLayout设置为false表示当前不在布局过程中。这里我们需要关心的就只有host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())这句代码,但这并不急于一时,我们先将performLayout()整个函数的逻辑分析清楚后再对里面的内容进行深入。
那么我们就接着看performLayout()的下面的代码(和上面一样的,我只保留了关键的代码):

Tip:下面的代码可以先简略的扫一眼,后面我会详细的分析

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
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
mHandlingLayoutInLayoutRequest = true;
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
.........
view.requestLayout();
}
}
});
}
}
}
..........
mInLayout = false;
}

上面的代码有个地方让我非常的在意,就是mLayoutRequesters这个属性变量,因为后面有几次都用到了这个变量。这是一个ArrayList<View>类型的一个集合,从变量名来看,这应该是一个存放着需要进行布局请求的View的集合。但这只是我的猜测,然后我就在ViewRootImpl类的代码中搜索了这个变量出现的地方。发现了一个函数requestLayoutDuringLayout(),我们可以来看看这个函数的代码:

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
/**
* Called by {@link android.view.View#requestLayout()} if the view hierarchy is currently
* undergoing a layout pass. requestLayout() should not generally be called during layout,
* unless the container hierarchy knows what it is doing (i.e., it is fine as long as
* all children in that container hierarchy are measured and laid out at the end of the layout
* pass for that container). If requestLayout() is called anyway, we handle it correctly
* by registering all requesters during a frame as it proceeds. At the end of the frame,
* we check all of those views to see if any still have pending layout requests, which
* indicates that they were not correctly handled by their container hierarchy. If that is
* the case, we clear all such flags in the tree, to remove the buggy flag state that leads
* to blank containers, and force a second request/measure/layout pass in this frame. If
* more requestLayout() calls are received during that second layout pass, we post those
* requests to the next frame to avoid possible infinite loops.
*
* <p>The return value from this method indicates whether the request should proceed
* (if it is a request during the first layout pass) or should be skipped and posted to the
* next frame (if it is a request during the second layout pass).</p>
*
* @param view the view that requested the layout.
*
* @return true if request should proceed, false otherwise.
*/

boolean requestLayoutDuringLayout(final View view) {
if (view.mParent == null || view.mAttachInfo == null) {
// Would not normally trigger another layout, so just let it pass through as usual
return true;
}
if (!mLayoutRequesters.contains(view)) {
mLayoutRequesters.add(view);
}
if (!mHandlingLayoutInLayoutRequest) {
// Let the request proceed normally; it will be processed in a second layout pass
// if necessary
return true;
} else {
// Don't let the request proceed during the second layout pass.
// It will post to the next frame instead.
return false;
}
}

这个函数的注释,官方已经给出了很明确的解析。但有的同学可能英语不太好,我还是先简单的来讲一下注释的内容吧。
首先这个函数是在view tree正在进行的布局传递过程layout pass的时候由requestLayout()调用的。但通常情况下requestLayout()不会在布局过程中调用,除非container hierarchy(就是整个View树的层级)知道它自己当前的状态(例如当container hierarchy中所有的子View都完成了测量和在布局过程layout pass结束时完成布局的状态下是可以调用的)。如果requestLayout()在一个frame(这里的frame应该是View的一次绘制流程,即一次完整的measure,layout,draw过程)进行的过程中被调用了,我们需要将所有要求进行布局的View进行注册(记录起来)。在frame结束的时候,我们检查注册过的View,看看是否还有那些没有被正确的处理View进行布局请求。如果有的话,我们将view tree的所有标记清除,得到一个空白的容器,然后在本次frame强制的执行第二次request/measure/layout过程。如果在第二次布局时还收到requestLayout()的调用,我们把这次的布局请求延迟到下一个frame,以此来避免进入死循环。
上面就是注释中所说的东西,简单的来讲,这个函数就是用来判断当前frame是否接受在布局过程layout pass中View的重新布局请求。而且这个函数的代码逻辑也很简单,首先传进来的view参数就是请求重新布局的View对象,第一个条件判断(view.mParent == null || view.mAttachInfo == null)表示view没有父布局或view没有所属的窗口,简单来说就是该View不会触发其他的布局。所以接受布局请求返回true。接着就将view不重复地添加到mLayoutRequesters集合中,然后根据mHandlingLayoutInLayoutRequest来判断当前是否正在进行布局请求的处理。若正在进行布局请求的处理就返回false,将当前的view的布局请求放到下一次frame进行,否则就接受请求,并在第二次布局过程中处理。

理解了这个函数后在回过来看我们的performLayout()的代码就简单不少了。但我发现performLayout()的代码里还有一个比较关键的函数getValidLayoutRequesters()。这个函数看它的名字就知道是为了得到前面所说的有布局请求的View的集合。但我们前面讲了将在布局过程layout pass中发生的requestLayout()分成了两种情况,那它又是怎样进行处理的呢?我们可以先到getValidLayoutRequesters()的代码中先看看。

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
/**
* This method is called during layout when there have been calls to requestLayout() during
* layout. It walks through the list of views that requested layout to determine which ones
* still need it, based on visibility in the hierarchy and whether they have already been
* handled (as is usually the case with ListView children).
*
* @param layoutRequesters The list of views that requested layout during layout
* @param secondLayoutRequests Whether the requests were issued during the second layout pass.
* If so, the FORCE_LAYOUT flag was not set on requesters.
* @return A list of the actual views that still need to be laid out.
*/

private ArrayList<View> getValidLayoutRequesters(ArrayList<View> layoutRequesters,
boolean secondLayoutRequests)
{


int numViewsRequestingLayout = layoutRequesters.size();
// 用于暂存有布局请求的view
ArrayList<View> validLayoutRequesters = null;
// 遍历每个需要请求布局的view
for (int i = 0; i < numViewsRequestingLayout; ++i) {
View view = layoutRequesters.get(i);
// 判断是否需要检查和清除view的flag
if (view != null && view.mAttachInfo != null && view.mParent != null &&
(secondLayoutRequests || (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) ==
View.PFLAG_FORCE_LAYOUT)) {
boolean gone = false;
View parent = view;
// Only trigger new requests for views in a non-GONE hierarchy
// 只触发`view tree`层级结构中可见view的布局请求
while (parent != null) {
if ((parent.mViewFlags & View.VISIBILITY_MASK) == View.GONE) {
gone = true;
break;
}
if (parent.mParent instanceof View) {
parent = (View) parent.mParent;
} else {
parent = null;
}
}
if (!gone) {
if (validLayoutRequesters == null) {
validLayoutRequesters = new ArrayList<View>();
}
// 将需要处理的view加入到返回集合
validLayoutRequesters.add(view);
}
}
}
// 判断是否为第2次获取view集合
if (!secondLayoutRequests) {
// If we're checking the layout flags, then we need to clean them up also
// 遍历集合里所有的view,并将其中的flag重置
for (int i = 0; i < numViewsRequestingLayout; ++i) {
View view = layoutRequesters.get(i);
while (view != null &&
(view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) {
view.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
if (view.mParent instanceof View) {
view = (View) view.mParent;
} else {
view = null;
}
}
}
}
// 清除参数集合中的所有view,给下一次`frame`提供一个空的容器
layoutRequesters.clear();
return validLayoutRequesters;
}

这个函数的功能就是遍历参数layoutRequesters里的每个View,根据每个View在view tree层级结构中是否可见以及是否已被处理,来判断哪些view仍然需要布局。其中第2个参数secondLayoutRequests表示获取的view集合是否是在一次frame里的第2次获取。因为我们需要将布局请求分为两种,在第2次获取view的集合时得到的是在下一次frame中需要进行处理的view,而代码中是通过第2个参数secondLayoutRequests来区分。
简单的来说,这个函数就是用来获取当前frame中需要进行布局处理的view集合或者获取下一次frame时需要进行布局处理的view集合。

知道了mLayoutRequesters这个成员属性和getValidLayoutRequesters()函数的意义后我们可以来继续分析我们的performLayout()的代码了。

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
// 获取当前请求布局的View的数量
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// 获取当前`frame`需要进行处理布局请求的View的集合
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
// 表示当前正在处理布局请求
mHandlingLayoutInLayoutRequest = true;
int numValidRequests = validLayoutRequesters.size();
// 对集合中的每个view并对它进行新的布局请求,进行测量和布局
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
// 对整个View树进行重新测量
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
// 表示当前正在布局
mInLayout = true;
// 进行第2次布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 表示布局请求处理完毕
mHandlingLayoutInLayoutRequest = false;
// 获取下次`frame`需要进行布局处理的view集合
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
// 将相应布局请求发生到下一次`frame`中进行处理
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
.........
view.requestLayout();
}
}
});
}
}
}
..........
// 表示布局结束
mInLayout = false;
}

我在代码中已经做了详细的注释,我相信通过对mLayoutRequestersgetValidLayoutRequesters()两个关键点的讲解后,大家都能轻易的理解performLayout()的基本逻辑。那么现在我们要进入到View真正进行布局的地方—layout().

揭开layout()的面目

layout(int l, int t, int r, int b)这是我们的函数原型,4个参数分别是由父布局传进来的,代表view在父布局中所放置的位置信息,分别为距离父布局的上下左右的位置。下面给出示意图。

知道了参数的意义后,话不多说,马上让我们的主角登场吧!

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
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/

@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
// 判断在布局前是否需要测量
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 判断当前的View的显示模式的并进行边界的设置,并以此来判断View的布局大小和位置是否有所改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 进行布局
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
// 执行布局的监听接口,像onClickListener等监听接口一样
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
// 设置相应的flag
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

我先来解析一下文档注释的内容吧。首先这个函数是给View和它的后代节点(在view tree上)分配一个大小和位置,将它们布局在窗口上。这是布局机制的第2个阶段(第1阶段是测量阶段)。在这个阶段view tree上的
每个父阶段都调用孩子节点的layout()函数来放置它们,而且使用的是孩子节点在测量过程measure pass中保存的测量值。View的派生类不应该重写这个函数来实现自定义布局,而应该重写onLayout()函数,并在onLayout()中对它的孩子进行布局。
好了,解析完文档的内容后我们来分析代码吧。代码的逻辑也并不复杂,首先通过View的flag判断在布局前是否需要进行测量。然后根据window的模式来对View进行布局。
下面我们来看看onLayout()这个函数吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

可以看到这个函数在View类下的实现是空的函数体。这就说明了我们需要在子类来重写onLayout()函数来完成我们的自定义布局。通常情况下,ViewGroup是需要实现该函数的,因为它需要处理子View的布局。既然这样,我们就来看一下Android提供的FrameLayout类所实现的onLayout()的代码:

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
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 对子View进行布局
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity)
{

// 得到子View的数量
final int count = getChildCount();

// 计算得到FrameLayout的上下左右
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

// 遍历每一个子View,按照FrameLayout的属性对子View进行布局
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);

// 只对属性不为GONE的view进行布局
if (child.getVisibility() != GONE) {

// 得到子View的布局参数
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

// 得到子View测量后的宽高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();

int childLeft;
int childTop;

// 获取View的gravity属性
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}

// 得到FrameLayout的布局方向,RTL或LTR
final int layoutDirection = getLayoutDirection();

// 根据子View的gravity属性和FrameLayout的布局方向
// 设置布局开始和结束的位置,是从左边开始,到右边结束;还是从右边开始,到左边结束。
// 得到一个代表水平布局方向的值
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

// 下面的两个switch语句分别根据布局的垂直方向属性和水平方向属性
// 计算子View的上左位置,即Top和Left(将边距考虑在内)
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}

switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
// 对子View进行布局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

虽然代码有点长,但逻辑都非常的清晰,而且我在代码中也做了相应的注释,相信大家能很容易的就看得明白。

requestLayout()invalidate()

到这里我们的布局过程layout pass也分析完毕了。但这里还有一个疑问,就是我们在分析performLayout()的时候多次提到了requestLayout()这个函数,但却不清楚它的内容,既然这样,我们就来看看requestLayout()的真面目吧。

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
/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*
* <p>Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.</p>
*/

@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// 只有在当前的View请求布局,而不是父级层级的View请求布局时
// 才触发布局时请求的逻辑代码
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

// 设置相应的布局flag,或操作表示在flag中添加该标志
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

// 判断是否需要对父View进行布局请求
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用ViewRootImpl的`requestLayout()`
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

照样的,我先讲讲文档注释中的内容。当我们的View的内容发生了改变导致View的布局变得无效(即我们想要的效果没有正确的展现在布局上),这个时候我们可以调用requestLayout()来告诉系统让系统在view tree上执行一次layout pass来刷新布局。如果正在执行布局layout pass的时候调用了requestLayout(),那么布局的请求可能会在当前布局结束的时候重新进行一次layout pass,或者是等到下一次frame的时候才进行布局。如果子类要重写该方法,需要先调用super.requestLayout()以保证能正确的处理各种布局请求。
文档所说的与我们在前面分析的requestLayoutDuringLayout()函数时是一样的。如果前面的分析看懂了,那么结合前面的分析,requestLayout()的代码也很容易就能明白。
这里有一点需要注意的,就是下面部分的代码:

1
2
3
4
5
// 判断是否需要对父View进行布局请求
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用ViewRootImpl的`requestLayout()`
mParent.requestLayout();
}

这里根据条件,调用了ViewRootImplrequestLayout()函数,这个函数就是我在前面的文章中提到过的,调用了scheduleTraversals()的函数。

1
2
3
4
5
6
7
8
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

而且我们也知道了这个函数最后会调用ViewRootImplperformTraversal()函数,会执行一遍View的measure,layout,draw流程。
这就说明了,当View的requestLayout()被调用的时候,我们的整个view tree可能会进行一次measure,layout,draw的过程,这时我们的ViewRootImpl中的performMeasure()performLayout()会一定被调用,但performDraw()就有可能被调用也有可能不被调用。

既然讲到了requestLayout(),那就不得不提与它非常密切的invalidate()了。

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
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/

public void invalidate() {
invalidate(true);
}
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
*/

void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

// 将View中指定的区域变为无效
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate)
{

if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}

if (skipInvalidate()) {
return;
}

if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
// 去掉flag中的draw标记,以此来表示view未被绘制
mPrivateFlags &= ~PFLAG_DRAWN;
}

mPrivateFlags |= PFLAG_DIRTY;

if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}

// Propagate the damage rectangle to the parent view.
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);
// 调用`ViewRootImpl`的`invalidateChild()`
p.invalidateChild(this, damage);
}

// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}

// Damage the entire IsolatedZVolume receiving this view's shadow.
if (isHardwareAccelerated() && getZ() != 0) {
damageShadowReceiver();
}
}
}

注释说了,这个函数会可见的View变成无效的状态,即需要进行重新绘制,这就说明了onDraw()函数将会被调用。
上面的代码我们关注的部分是,首先invalidate()函数最终调用的是invalidateInternal()函数。所以我们直接看这个函数。在invalidateInternal()里,通过mPrivateFlags &= ~PFLAG_DRAWN;这句代码将flag相应的位设置为未被绘制。然后可以看到通过p.invalidateChild(this, damage);调用了ViewRootImplinvalidateChild()。下面就是ViewRootImpl中相关的代码。

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
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);

if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}

if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}

invalidateRectOnScreen(dirty);

return null;
}

private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}

// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region
final float appScale = mAttachInfo.mApplicationScale;
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
// 关注这里
scheduleTraversals();
}
}

我们只需关注上面最后的那句代码scheduleTraversals();即可。这里也调用了scheduleTraversals(),也就是说最终也会调用performTraversals()函数,但是由于没有添加measurelayout的标记到flag中,所以只会调用performDraw()函数。
总的来说:

  • invalidate()函数会触发performDraw()过程。
  • requestLayout()就会触发performMeasure()performLayout()过程,也有可肯触发performDraw()

注意:invalidate()需要在UI线程里被调用,如果要在非UI线程里调用,就需要调用postInvalidate()

更多关于invalidate()requestLayout()
的信息可以参考这两篇博文:

总结

最后还是用图片进行总结吧,因为这样最实际。