上次我们对View的测量过程有了了解,接着这次肯定就是要沿着View的3大流程往下走。我们本次的主角就是View的Layout
过程。
从performLayout()
开始出发
我们的已经知道了View的布局过程layout pass
就是从performLayout()
开始的,那么我们就先来大概的浏览一下这个函数的代码。
TIP: 这里我们先简略的浏览整个函数的代码(不用认真看),后面我们会将代码分开几个部分,一个部分一个部分的进行分析。
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 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 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()
的代码仔细看起来也并不难,我们一点一点的来进行分析。我们都知道这个函数是在ViewRootImpl
的performTravers()
进行调用的。那我们来看看它的传入参数。
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
分别是当前的窗口布局参数lp
,窗口的宽度和高度desiredWindowWidth,desiredWindowHeight
。
既然知道了参数的意义,那我们就可以对performLayout()
进行分析了。首先我们就来分析下面部分的代码(为了看起来更加的清晰,我将一些无关的代码去掉了,去掉的部分我用省略号代替):
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:下面的代码可以先简略的扫一眼,后面我会详细的分析
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
ArrayList 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 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的集合。但这只是我的猜测,然后我就在ViewRootImpl
类的代码中搜索了这个变量出现的地方。发现了一个函数requestLayoutDuringLayout()
,我们可以来看看这个函数的代码:
/**
* 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.
*
* 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).
*
* @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()
的代码中先看看。
/**
* 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 getValidLayoutRequesters(ArrayList layoutRequesters,
boolean secondLayoutRequests) {
int numViewsRequestingLayout = layoutRequesters.size();
// 用于暂存有布局请求的view
ArrayList 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加入到返回集合
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()
的代码了。
// 获取当前请求布局的View的数量
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// 获取当前`frame`需要进行处理布局请求的View的集合
ArrayList 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 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;
}
我在代码中已经做了详细的注释,我相信通过对mLayoutRequesters
和getValidLayoutRequesters()
两个关键点的讲解后,大家都能轻易的理解performLayout()
的基本逻辑。那么现在我们要进入到View真正进行布局的地方---layout()
.
揭开layout()
的面目
layout(int l, int t, int r, int b)
这是我们的函数原型,4个参数分别是由父布局传进来的,代表view在父布局中所放置的位置信息,分别为距离父布局的上下左右的位置。下面给出示意图。
知道了参数的意义后,话不多说,马上让我们的主角登场吧!
/**
* Assign a size and position to a view and all of its
* descendants
*
* 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().
*
* 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.
*
* @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 listenersCopy =
(ArrayList)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()
这个函数吧:
/**
* 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()
的代码:
@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()
的真面目吧。
/**
* 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.
*
* Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.
*/
@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()
的代码也很容易就能明白。
这里有一点需要注意的,就是下面部分的代码:
// 判断是否需要对父View进行布局请求
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用ViewRootImpl的`requestLayout()`
mParent.requestLayout();
}
这里根据条件,调用了ViewRootImpl
的requestLayout()
函数,这个函数就是我在前面的文章中提到过的,调用了scheduleTraversals()
的函数。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
而且我们也知道了这个函数最后会调用ViewRootImpl
的performTraversal()
函数,会执行一遍View的measure,layout,draw
流程。
这就说明了,当View的requestLayout()
被调用的时候,我们的整个view tree
可能会进行一次measure,layout,draw
的过程,这时我们的ViewRootImpl
中的performMeasure()
和performLayout()
会一定被调用,但performDraw()
就有可能被调用也有可能不被调用。
既然讲到了requestLayout()
,那就不得不提与它非常密切的invalidate()
了。
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
*
* 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);
调用了ViewRootImpl
的invalidateChild()
。下面就是ViewRootImpl
中相关的代码。
@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()
函数,但是由于没有添加measure
和layout
的标记到flag中,所以只会调用performDraw()
函数。
总的来说:
-
invalidate()
函数会触发performDraw()
过程。 -
requestLayout()
就会触发performMeasure()
和performLayout()
过程,也有可肯触发performDraw()
。
注意:
invalidate()
需要在UI线程里被调用,如果要在非UI线程里调用,就需要调用postInvalidate()
。
更多关于invalidate()
和requestLayout()
的信息可以参考这两篇博文:
- Android requestLayout 和 invalidate
- Android View 深度分析requestLayout、invalidate与postInvalidate
总结
最后还是用图片进行总结吧,因为这样最实际。