自定义view之measure、layout、draw三大流程
一个view要显示出来,需要经过测量、布局和绘制这三个过程,本章就这三个流程详细探讨一下。View的三大流程具体分析起来比较复杂,本文不会从根源详细地分析,但是可以保证能达到实用的地步。
1. measure过程
1.1 理解MeasureSpec
View的测量方法为public final void measure(int widthMeasureSpec, int heightMeasureSpec)
和protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,它们的参数都是两个MeasuerSpec,因此在搞懂测量过程之前,要先搞明白MeasureSpec。
MeasureSpec可以理解为测量规范,它是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecSize就代表测量值。查看MeasureSpec的部分源码,不难发现MeasureSpec的工作方式。
MeaureSpec部分源码:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
不难看出MeasureSpec的结构是怎样的。并且我们可以从一个MeasureSpec中分解出SpecMode和SpecSize,也可以用SpecMode和SpecSize来组装一个MeasureSpec。在上面的代码中看到了有SpecMode,我们先看下SpecMode的源码和注释。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
SpecMode有三种:
1. EXACTLY:确定大小,如果SpecMode是Exactly,那么SpecSize是多少,测量结果就是多少。比如子View在layout中设置的是指定大小,那么在测量时从父布局中传到measure方法的SpecMode就是Exactly。或者说子布局设置的是match_parent,而父布局此时已经能够确定自己的大小,那么模式也是Exactly。
2. AT_MOST:子布局可以自行确定自己的大小,但是不能超过SpecSize的大小。典型的就是父布局已经确定了自己的大小,而子布局设置的参数是wrap_content,
3. UNSPECIFIED:父布局对子view不做限制,要多大给多大。用于特殊的测量场合。
1.2 理解measure过程
在弄明白MeasureSpec之后,就可以看看measure方法了,其实measure方法是View中的一个final方法,我们是无法重写的,measure方法做了一些基本工作,但是在measure方法的注释里说道真正的measure工作应该放在onMeasure方法里,所以基本都是重写onMeasure方法。接下来看一下onMeasure的源码和注释。大家在看源码的时候千万不要漏掉注释只看源码,注释是非常重要的,往往一些流程的说明就在注释里,看关键注释能比看十个方法的源码有用。
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
注释里的第一部分说了,测量大小的工作应该主要放在这个方法里,并且所有继承的子类都应该重写这个方法。还有两个比较重要的点:一是重写的方法在onMeasure最后一定要将测量结果通过setMeasuredDimension(int, int)
来存储起来,这样这个view的measuredWidth和measuredHeight就是有效值了;二是需要保证测量得出的高和宽不能小于getSuggestedMinimumHeight()
和getSuggestedMinimumWidth()
的返回值。
View类中的onMeasure方法很简单,它就是直接获得建议的宽和高作为测量结果,为了能明确onMeasure方法而又不至于陷入源码无法自拔,我们选取一个比较简单的FrameLayout来看看它的onMeasure源码,相应的注释我已经写在里面了。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
/**
* 来确定是否需要重复测量那些宽和高参数为match_parent的子view,如果FrameLayout的宽高都不是确定的(Exactly),
* 那么只有在确定了FrameLayout的宽高之后,才能去测量那些宽或高参数为match_parent的子view。
* */
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
/**
* 测量所有可见的子view
* */
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
/**
* 这个是测量子view的主要方法
* */
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
/**
* 子view测量完之后,获取子view的测量的宽和高,然后用FrameLayout已有的长和宽相比较,取其大者,这样能保证
* 完整显示所有的子view。
* */
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
/**
* 将padding也加入到测量结果中
* */
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
/**
* 检查一下是否小于推荐的最小值,如果小于了,就使用推荐的最小值
* */
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
/**
* 再比较前景的大小,取其大
* */
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
/**
* 设置该FrameLayout的测量大小
* */
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
/**
* 查看需要测量宽或高为match_parent的子view,如果需要测量,就重新构造子view的MeasureSpec。
* */
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
lp.leftMargin - lp.rightMargin,
MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
lp.topMargin - lp.bottomMargin,
MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
上面就是FrameLayout的测量过程,结合注释能看得很明白。首先FrameLayout会测量所有的子view,如果FrameLayout的大小是确定的,那么一轮测量就可以确定所有子view的大小。如果FrameLayout的大小不确定,比如设置为wrap_content,那么此时那些宽或高参数为match_parent的子view是无法被确切测量大小的,因为此时parent的大小都还不知道呢,并且这些子view会存到mMatchParentChildren
里。一轮测量下来,此时FrameLayout的宽和高分别都是测量过的子view的最大的宽和最大的高(最大的宽和最大的高不一定会出现在同一个子view上)。为防止出现极端情况,比如所有的子view宽高参数都是match_parent,那么此时测量出来的宽和高都是0(为什么是0后面会解释)。因此还需要对比一下最小的建议值以及前景的宽和高。
最后一轮是测量那些宽或高参数为match_parent的子view,此时FrameLayout的大小已经确定了,然后使用FrameLayout的SpecMode和第一轮刚测量出来的宽高重新构造子view的MeasureSpec,然后再重新测量。
至于父layout怎么测量子view的,其实从onMeasure
的第二轮测量中就可以看到,首先父layout会依据自己的MeasureSpec和要测量的子view的LayoutParams.width、LayoutParams.height来生成子view的MeasureSpec,然后将这个MeasureSpec传给子view的measure方法,子view再根据自己的情况来测量自己大小。在父layout生成子view的MeasureSpec的过程中,主要是getChildMeasureSpec
方法,浏览一下这个方法的源代码。
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
方法并不难,我们可以很清晰地看到父layout是如何生产子view的MeasureSpec的,各项的注释也写得很清楚,总结起来如下表:
这个很简单,可以看到父layout是如何根据自己的MeasureSpec和子view的LayoutParams来创建子view的MeasureSpec的。
在onMeasure的第一轮测量中有个比较关键的方法measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
,从名字可以看出这个方法是测量子view的,并且还会将子view的margin值考虑在内。我们可以接着看下这个方法的源码。
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看出这个方法仍然是先生成了子view的MeasureSpec,然后调用子view的measure方法。需要注意的是传入的各个参数。查看getChildMeasureSpec(int spec, int padding, int childDimension)
方法,我们知道它的三个参数分别为:
- int spec:该view的MeasureSpec,这里就是FrameLayout的Spec。
- int padding:这并不是单纯字面意思的父layout的padding值,简而言之,这就是父layout已经被占用掉的空间。如果要生成的是HeightMeasureSpec,那么这个padding就包括其他子view在垂直方向已经占用掉的位置、父layout的paddingTop和paddingBottom、子view本身的topMargin和bottomMargin值。
- int childDimension:要获取MeasureSpec的子view的宽度或者高度值,这个值是确定大小、wrap_content和match_parent这三种情况之一。
在onMeasure
方法中第一轮测量子view的写法是
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
显然传给这个方法的widthUsed和heightUsed值是0,到这个方法内部的getChildMeasureSpec(int spec, int padding, int childDimension)
中的padding就不包括其他子view已经占用掉的位置。这也恰好符合FrameLayout的特点:所有的子view都重叠排列,互相之间不影响。并且Framlayout如果是wrap_content的,那么它的宽是所有子view中最大的宽、高是所有子view中最大的高。
1.3 measure过程总结
以上就是FrameLayout完整的测量过程,当然FrameLayout本身布局特点就是非常简单的,如果是RelativeLayout,那么测量过程会更加复杂。但即使如此,我们还是能从中总结出Layout测量的一般规律的:
- 通过自身的MeasureSpec和子view的LayuoutParams,生成子view的MeasureSpec。这一步调用的是
getChildMeasureSpec(int spec, int padding, int childDimension)
方法。 - 调用子view的
measure(int widthMeasureSpec, int heightMeasureSpec)
方法,来测量子view的宽高。 - 在子view测量结束之后,根据情况来计算自身的宽高。假如自己的MeasureSpec是Exactly的,那么可以直接将SpecSize中的大小作为自己的宽或高;如果是wrap_content或者其他的,那么就需要在每一个子view测量完之后,调用子view的
getMeasuredHeight()
和getMeasuredWidth()
来获得子view测量的结果,然后根据情况计算自己的宽高。 - 使用
setMeasuredDimension(int measuredWidth, int measuredHeight)
方法保存测量的结果。
这样就完成了layout及其子view的测量过程。而view的测量就更简单了,因为没有子view,只要确定了自身内容的大小,再结合MeasureSpec便可以测量完毕。比如有个view需要画个圆,那么只要考虑设置的padding值以及圆的大小即可。
注意:三种宽、高值的区别:
1. LayoutParams.width、LayoutParams.height:这个是布局文件中的宽度和高度值,单位是px,并且WRAP_CONTENT对应-2,MATHCH_PARENT对应-1,这个是任何时候都可以调用的。特别注意,无论是measure还是layout过程,都不会对这个LayoutParams产生影响,除非在代码中手动调用setLayoutParams()方法来设置,否则LayoutParams中存储的都是布局文件中的宽和高,既不是测量出来的宽和高,也不是最终确定的宽和高。可以做个实验,初始化任意一个控件时,使用ViewTreeObserver
添加ViewTreeObserver.OnGlobalLayoutListener
,并在回调函数中打印log来分别显示使用LayoutParams获取控件宽和高,以及直接使用getWidth()和getHeight()来获取宽和高,就可以看到效果。从这方面来说,其实LayoutParams更像是一种基准,是给父layout为该view生成MeasureSpec时参考用的,而不一定确切就是其中的值。所以如果要获得一个控件的真实宽和高,一定不要使用LayoutParams。可以通过setLayoutPramas(LayoutParams param)
方法来改变view的宽和高,而且往往这是唯一可以手动指定宽和高的方法,比如虽然TextView有setWidth(int width)
方法,但是ImageView却没有,只能通过setLayoutPramas(LayoutParams param)
。
2. getMeasuredWidth()、getMeasuredHeight():获取测量出的宽和高,这是在measure方法结束后才可以得到有效值。
3. getWidth()、getHeight():获取最终实际的宽和高,实际的宽和高在layout阶段才会确定,但是大部分情况,测量出的宽和高就是最终的宽和高。
2. layout过程
layout相比measure,就比较简单,而且不像measure是有固定套路,基本实现方式比较*。和measure同理,在自定义view的时候应该重写onLayout()
,虽然layout()
方法是可以重写的。
2.1 理解layout过程
同样为了简单,我们继续选取FrameLayout的layout过程来学习。也许大家会说,FrameLayout的布局太简单了,不就是所有view都靠着左上角布局吗?其实并不是这么简单的,FrameLayout也有可以控制子view位置的参数,并且在布局过程中我们会看到某些需要在自定义view中需要注意的事情。接下来就看一下layout函数。需要注意的是layout函数是在View类中定义的,并且FrameLayout遵循了规范,并没有重写layout函数,因此需要到View类中找到layout函数。
/**
* 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;
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;
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
可以看到,View的layout函数其实并没有执行多少实际的布局操作,而是负责一些状态的更新以及本view的坐标设置,使用setFrame()
函数将父容器传来的坐标应用到了自己的视图。然后调用了onLayout(changed, l, t, r, b)
,接下来就可以去看FrameLayout的onLayout()
函数了。需要注意的是,l、t、r、b都是相对于父view的位置,而不是在屏幕中的绝对位置,这点在注释里也说明了。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
很简单,直接调用了layoutChildren
,并且比原来的参数多了个布尔值。其中changed
这个参数需要说明一下,这是布局根据自己原来的位置和之后layout函数中传来的新值做比较来发现自己的大小和位置是否改变,如果改变则changed为真,否则为假。这也可以用来决定什么时候对子view进行layout操作,避免频繁进行不必要的layout浪费资源。而验证是否changed这项工作是有View类的layout函数中进行的,因此不必我们担心。
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
/**
* 获取父layout的上下左右位置,这里的父layout是指FrameLayout本身
* */
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
/**
* 依次测量每个子view,并且考虑子view的gravity
* */
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
/**
* 根据gravity和布局方向来确定横向和纵向的绝对gravity,以此来决定子view的左边和上边的位置。如果熟悉开发者选项,
* 会发现其中有一个选项是“强制从右到左的布局”,并且FrameLayout也有android:layoutDirection属性,可以设置为继承(inherit)、
* 本地(local)、从左到右(ltr)、从右到左(rtl)四个选项,但是没有设置上下方向的,毕竟没有哪个文化的阅读习惯是上下颠倒的。因此
* 左右的gravity需要结合布局方向,但是上下布局只需要解析view自己的gravity设置即可,而不需要direction。另外absoluteGravity
* 是按照从左到右的,无论开发者选项里怎么设置。
* */
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
/**
* 计算子view的left位置
* */
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
/**
* 如果绝对gravity为横向居中,下面的计算显而易见是为了让子view横向居中的,并且考虑了margin值
* */
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
/**
* 如果不是强制gravity为左,那么还要计算gravity为右的情况
* */
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
/**
* 计算子view的top位置
* */
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的layout函数,传入计算好的子view的left、top、right、bottom值
* */
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
关键注释已经在代码中写好了,我们可以从中发现,onLayout的关键就是在于确定子view的上下左右四个边界的位置。而FrameLayout的布局过程中,所有子view的布局都是独立的,而不会受到其他子view的影响,这也验证了FrameLayout的特性:所有子view都会叠加排列。如果是竖向布局的LinearLayout,那么每次下一个子view的top都会建立在上一个子view的bottom位置的基础上来计算,以保证它们是顺序排列的,这一点可以自己查看LinearLayout的onLayout()
函数验证。
也许有人会问,这只是测量了位置,但是还没有应用到视图啊。其实设置不在onLayout()
函数,而是早在View的layout()
函数中就进行了。就是setFrame()
函数,changed就是setFrame()
函数的返回值。从这里也可以看出,子view的位置(应该)完全是由父layout确定的,并且在父layout调用子view的layout()
函数中直接设置了位置。不建议强行在onLayout()
函数中再次调用setFrame()
,避免出现布局错乱。onLayout()
函数应该只用来布局子view,或者进行其他需要在layout阶段进行的工作,比如打log。如果自定义view是一个view而不是layout,那么完全不用重写onLayout()
也是可以的。
2.2 FrameLayout属性验证
在布局过程中看到,在不改变FrameLayout的布局方向的情况下(毕竟改变布局方向的情况很少),只有子view的gravity和margin值能影响子view的位置。接下来会验证一些特性。
(1) 无任何特殊设置
新建一个布局,没有任何特殊选项来看看效果
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.zu.customview.FrameLayoutTest">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"/>
</FrameLayout>
效果
(2) 子view添加margin
修改布局TextView,添加margin
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_marginLeft="20dp"
android:layout_marginTop="30dp"/>
效果
这里我们有个现象可以看一下,如果设置marginLeft值一直到TextView的右边界超出FrameLayout的右边界,会出现什么情况。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_marginTop="30dp"
android:layout_marginLeft="330dp"/>
我们发现TextView居然折行了。显然子view是也可以得到父layout的布局信息的,并且在布局过程中会自动进行某些改变。虽然这一点来说是比较智能的,但不可避免的会出现某些不希望出现的情况。如果不希望出现子view自做主张的情况,在measure时可以构建一个UNSPECIFIED的MeasureSpec来测量子view。
(3) 子view添加layout_gravity
修改布局TextView,添加gravity
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_gravity="center"/>
效果
(4) FrameLayout修改layoutDirection
去掉TextView的margin和gravity,然后在FrameLayout中添加下面一句,就可以修改FrameLayout的布局方向为从右到左
android:layoutDirection="rtl"
效果
从以上过程,我想大伙儿应该已经完全能理解FrameLayout的布局过程,也顺便从代码中了解了一些FrameLayout其他的特性并且做了验证。
2.3 layout过程总结
layout的一般过程就是如此,总结起来就是如下几步:
- 父layout在自己的
onLayout()
函数中负责对子view进行布局,安排子view的位置,并且将测量好的位置(上下左右位置)传给子view的layout()
函数。 - 子view在自己的
layout()
函数中使用setFrame()
函数将位置应用到视图上,并且将新位置和旧位置比较来得出自己的位置和大小是否发生了变化(changed),之后再调用onLayout()
回调函数。 - 如果此时子view中还有其他view,那么就在自己的
onLayout()
函数中对自己的子view进行第1补的布局操作,如此循环,只到最后的子view中没有其他view,这样就完成了所有view的布局。
当然,以上说的还是ViewGroup的layout过程,如果是View的layout过程就会更加简单,毕竟没有子view,只要将传进来的位置应用到视图上就OK。
3. draw过程
draw过程相比与其他的两个就简单多了。它的作用就是把view内容绘制到屏幕上。
先看一下View的draw()
源码
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' 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 (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
...
}
可以看出绘制分为6步,其中第二步和第五步通常都是跳过的。我们只看剩下的四步:
- 绘制背景
- 绘制自己的内容(onDraw())
- 绘制子view(dispatchDraw())
- 绘制装饰
以上步骤不难理解,自己在Bitmap画过自定义图形的同学都知道,canvas画图,如果位置重叠的话,后绘制的内容会把先前的内容覆盖掉。这个canvas是由ViewRoot传过来的,这样保证界面上所有的子view都绘制在一张画布上。其实所有view的测量、布局、绘制过程都是由ViewRoot发起的。
注释里仍然说明了draw()
方法不应该被重写,应该在onDraw()
方法里处理本身的绘制,而在dispatchDraw()
里绘制子view。如果一定要重写draw()
方法,那么也一定要在开始调用super.draw(Canvas canvas)
。
由于不同的view绘制方法不同,而且有的是layout有的是view,对于是否需要绘制子view的需求也不同,所以View类中的onDraw()
和dispatchDraw()
都是空实现。而由于ViewGroup是容器,自身需要绘制的东西比较少,主要在于子view的绘制,因此ViewGroup主要实现了dispatchDraw()
。相反的,TextView、ImageView等这些非容器的控件则主要实现onDraw()
方法来呈现更为复杂的内容。本来流程并不是很复杂,相比起流程,draw过程的细节却多得让人可怕。不过为了完全弄明白容器和控件的绘制过程,我们仍然节选一点源码简单了解一下。
节选一段ViewGroup类中的dispatchDraw()
方法
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
if (buildCache) {
child.buildDrawingCache(true);
}
}
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save();
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
if (preorderedList != null) preorderedList.clear();
...
}
看得出来很多工作是为绘制子view做准备的,包括准备cache、根据clip来设置canvas等,真正绘制的语句是more |= drawChild(canvas, child, drawingTime)
这句,我们看下这个函数的源码。
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
从注释中可以看到,这个方法不仅仅是为了绘制内容这么简单,我们对View应用的各种动画或者其他视觉效果也将在这里体现。它直接调用了子view的draw()
,但显然这个draw函数和我们之前提到的不一样,现在这个三个参数。这个函数仍然是在View类中,由于源码太长,我就不贴了。注释中说明了,这个是专门让ViewGroup.drawChild()
来调用的,子类不应该重写这个方法,也不应该在除了ViewGroup.drawChild()
之外的其他地方调用它。看一下它的代码,会发现里面是一些对canvas进行位移、缩放和变形的代码,也验证了它确实是为View的动画效果准备的。并且在这里也调用了单参数版本的draw(Canvas canvas)
来绘制内容。
具体各View怎么绘制的,就不详细看了,canvas本来就有绘制各种图像的方法,比如绘制椭圆、方形、文字、甚至还有绘制Drawable方法。在查看了TextView和ImageView的onDraw()
方法后,发现在正式绘制前,都会把内容绘制到一个Drawable上,然后再将这个Drawable绘制到canvas上。
至于Canvas、Paint、Path、Drawable等等这些和图像相关的,可以具体去查一下,现学现用也可以。
总结
至此View的显示流程就讲解完毕了,也看到了一个View走过了measure、layout和draw这三大阶段需要多么复杂的工作,在此不得不感叹一句:现在的cpu真tm快啊~那么多view那么多流程,还能保证每秒60fps的帧率。有点跑题,总之详细内容在各部分也已经讲的很明白了。这里只做一个简单的总结。
measure
- measure过程的信息传递是基于MeasureSpec的。
- MeasureSpec由SpecMode和SpecSize组成。SpecMode有三种:EXACTLY、AT_MOST、UNSPECIFIED。
- 子view的MeasureSpec是由父容器结合自己的MeasureSpec和子view的LayoutParams来构建的,所以在子view自己的measure函数中不必再考虑自己的LayoutParams,仅参考父容器传入的MeasureSpec即可。
- 控件仅测量自己即可,容器则需要测量子view。通过调用一个View或ViewGroup的
measure(int, int)
函数来测量。 - 测量完毕后务必调用
setMeasuredDimension(int, int)
来保存测量结果。 - 测量出的宽和高在绝大多数情况下是等于最终的宽和高的,但是不排除会不同,毕竟最终的宽和高是在layout阶段确定的。
layout
- 通过调用一个View对象的
layout(int, int, int, int)
函数对该view进行布局工作。 - 子view的确切布局是父容器负责确定的,父容器一旦调用子view的
layout(int, int, int, int)
,子view就会调用setFrame()
函数将位置应用到视图,并且根据旧位置和新位置来判断是否布局发生改变,并将判断结果和位置参数一起传到回调函数onLayout()
。 - 我们自己自定义的布局工作应该都在
onLayout()
中完成,包括对子view进行布局。
draw
这个应该是流程最简单但是细节最复杂的一步了。具体的绘制需要用到Cavans、Drawable等,可以详细去查这方面的资料。总之一句话,内容都是绘制在传入的Canvas上,具体画什么怎么画,完全没有限制。
至此,View的显示的流程已经探索完了,接下来就是View的事件分发机制了,将会在我的下一篇博客《自定义view之view事件分发机制》中讲解。
声明:本系列文章部分知识点来自于《Android开发艺术探索》,在此对作者表示感谢。部分内容可能会有错误和遗漏,欢迎大家留言讨论。