首先,可以明确知道一个Activity的根View是DecorView
,而DecorView extends FramLayout extends ViewGroup extends View
。根据源码追踪,发现最先调用的是View.measure()
。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (forceLayout || needsLayout) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...
}
...
}
...
}
首先注意到这个方法是被final
修饰的,这意味着开发者无法重写该方法。但是可以看到,实际的测量方法是在onMeasure()
中的。
这个时候查看ViewGroup
源码发现没有重写onMeasure()
,但是在FramLayout
中对这个方法进行了重写。想想也是,毕竟不同的控件对测量的方式肯定是不同的。但是在看FramLayout.onMeasure()
之前,我们需要先看看默认的测量是怎么样的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 如果父view给定了size,那么就用父view的。
* 否则就用最小值(minHeight/minWidth)或者背景的宽高。
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;// 最小值或者背景的最小值
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 父view没有任何限制
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
// 父view给了准确值或者知道父view给定的最大值
result = specSize;
break;
}
return result;
}
/**
* 有背景就找出背景和minWidth中最大的值,没有就是用minWidth
* getSuggestedMinimumHeight()一样的逻辑,这里就不贴了
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
/**
* 这个方法简单的理解为对mMeasuredWidth和mMeasuredHeight这两个变量赋值即可
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
逻辑很简单,都在代码中说明了。很明显,这种简单的逻辑对于一些稍微复杂点的控件来说是不够用的,尤其是容器类控件(ViewGroup)。如FramLayout
的测量逻辑肯定和LinearLayout
不一样。FramLayout
应该使用的是子view中最大的宽高,而LinearLayout
应该会根据水平或垂直进行子view宽累加或高累加。
前面说过,一个Activity的根View是DecorView extends FramLayout
,那么下面简单看看FramLayout
如何进行测量的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// 是否要测量出当前FrameLayout大小后再告诉Match_Parent的子view,
// 这里要求当前FrameLayout的父view 不能给到准确的大小。
// 因为如果能确认大小,那么MatchParent的子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);
// 要求子view不能为gone
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 生成给子view的建议大小和测量模式,并传递给子view,让子view测量自身
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 记录当前所测量出的最大值
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());
// 把MATCH_PARENT的子view记录下来
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// 累加外边距、内边距等等,确定最后的大小
...省略累加的代码
// 表示自己已经测量完毕了,一定要调用这个方法设置最终的测量结果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// 通过前面的第一次遍历,计算完最终的大小后,对需要MATCH_PARENT
// 的子view传入确定值
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
// 这里就是告诉MATCH_PARENT的子view,能使用的最大值是多少
}
}
}
整个流程是这样的:
MATCH_PARENT
,则先记录,等测量完其他的子view后再告诉这些子view大小。MATCH_PAREN
的子view最终的大小是多少。这里面,需要关注的是如何生成给子view的测量建议。首先是调用到measureChildWithMargins()
。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 生成给子view的建议
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);
}
方法很短,完成两件事:
childWidthMeasureSpec()
生成给子view的测量建议。下面看看,整个测量流程中,最关键的一个方法,如何生成给子view的测量建议。
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);
// 最终传递给子view的测量模式和值
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// 1. 本身的大小是精确值,如100dp
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 1.1 子view有确定值,那么不管父view多大,子view就是设置的大小
// 虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
// 1.2 子view想要充满父view,由于父view的大小确定了,那么子view大小也确定了
// 因此此时子view的测量模式为EXACTLY,精确值
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.
// 1.3 子view想要根据自身的内容来确定大小,但是由于父view大小已确定
// 子view无论内容多大,最大也就父view的大小,所以先设置子view为
// 父view大小,但是测量模式允许在大小的范围内进行变动,AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
// 2. 本身的大小未知,但是知道最大值,即size为最大值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 2.1 子view有确定值,那么不管父view多大,子view就是设置的大小
// 虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
// 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.
// 2.2 子view想充满父viwe,但是由于目前父view也不确定到底多大
// 子view自然也无法确定,但是父view知道自己最大是多少,
// 因此子view的最大值也能确定
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.
// 2.3 子view想根据自己的内容去决定大小,但是子view无论多大,都
// 不能超过父view的最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
// 3. 本身想多大就多大
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 3.1 子view有确定值,那么不管父view多大,子view就是设置的大小
// 虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
// 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
// 3.2 父view无法确定自己多少,那就只能继续按照父view的结果继续传递下去
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
// 3.3 父view无法确定自己多少,那就只能继续按照父view的结果继续传递下去
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
// 将测量模式和测量值拼接起来
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
现在,来总结一下,几个测量模式所表示的意思:
size
是父view建议的子view大小,是确定的数值,如:100dp。当作为子view收到这个测量模式建议时,表明父view的大小是已经确定了,此时子view的LayoutParams
是数值或MATCH_PARENT
。size
是父view允许子view的最大值。当作为子view收到这个测量模式建议时,可分为两种情况:1. 父view已经确定了大小,但是此时子view的LayoutParams
是WRAP_CONTENT
;2. 父view大小未确定,但是知道自己的最大值。此时子view可能是WRAP_CONETNT
或MATCH_PARENT
。注意:以上方法生成的测量模式以及测量值,都仅仅是父view根据子view的LayoutParams
以及父view自身接收到的测量模式和测量值生成的建议。作为子view,你可以不按照建议进行测量,当然后果就要自己承担了。
其实view的测量并没有特别复杂,关键是我们要明白,在onMeasure()
中收到的测量模式以及size表示什么意思。然后我们只要按照父view的建议,再结合自身的实际情况,计算出作为子view的大小即可。
布局相对于测量而言,相对比较简单。首先依然从View.layout()
入手,看看默认情况。
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); // 调用到布局方法
...省略部分代码
}
}
逻辑都在注释之中了,在View.onLayout()
,默认实现是空的,而在ViewGroup
中onLayout()
是被abstract
修饰的。意味着,如果是自定义ViewGroup
,必须要实现这个方法。
另外需要说明的是,layout()
传入的4个参数l、t、r、b
分别表示子view相对于父view左边距离、顶部的距离、右边的距离、底部的距离。大概如图:
下面,来看看最简单的FrameLayout
是如何布局的,借此来理解一下这4个参数的意义以及用处。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
调用了layoutChildren()
,需要注意的是,最后一个参数在这里传入了false
。
这个方法,我将进行分步的讲解,这样比较好理解。
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
直接看代码可能比较难理解,可以配合这张图来看。
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
// 2.1 水平居中
// 这里之所以要在外面加多一层parentLeft,是因为要计算后的
// 坐标是相对坐标
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
// 2.2 从右到左布局
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
// 从左到右布局
default:
childLeft = parentLeft + lp.leftMargin;
}
这里的话,就只分解最具代表的水平居中。这里默认子view没有设置leftMargin
和rightMargin
。
现在看图说话,我想计算childLeft
,需要怎么计算呢?
根据图可以列出算式:childLeft = (w1-childWidth)/2 + parentLeft
其中,w1 = parentRight-parentLeft
两个算式进行合并得到:childLeft = (parentRight-parentLeft-childWidth)/2 + parentLeft
switch (verticalGravity) {
case Gravity.TOP:
// 3.1 从上到下布局
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
// 3.2 垂直居中
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
// 3.3 从下到上布局
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
// 3.4 默认情况,从上到下布局
childTop = parentTop + lp.topMargin;
}
这里同样选择最具代表性的,垂直居中进行分解。同样,这里默认子View没有设置topMargin
和bottomMargin
。
依旧看图说话,计算出childTop。
根据图可列出算式:childTop = (h1-childHeight) / 2 + parentTop
其中,h1 = parentBottom - parentTop。
两个算式合并得到:childTop = (parentBottom - parentTop - childHeight)/2 + parentTop
至此,子view的四个边距都计算出来了,你可能会问:“childBottom和childRight不是还没计算吗?”。的确,FrameLayout
并没有计算,那是因为没必要啊,因为childRight=childLeft+childWidth;childBottom=childTop+childHeight。然后就可以调用child.layout()
将计算结果传递给子view了。
通过分析FrameLayout
的布局方法,可以看到,布局这个动作其实是需要进行大量的计算的。尤其是LinearLayout
、RelativeLayout
这种布局属性更多,布局更加复杂的控件。因此,在日常的开发中,要尽可能的避免嵌套过多的布局,这样可以降低View绘制的时候,在布局这里计算的时间,提高应用的流畅度。
Draw
是自定义view的时候最常见的方法,但是确实整个流程中最简单的方法,因为只需要画图就好了,不需要像Measure
和Layout
需要联系父view和子view去进行。下面,先看看在View.draw()
方法中,官方为我们做了哪些事情。
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;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) { // 第一步,绘制背景
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
// 看看是否有设置Fading Edge属性,可以理解为边缘羽化的效果
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 第四步,绘制子view
dispatchDraw(canvas);
// 自动填充功能,需要绘制一下高亮色
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
// 绘制悬浮层
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
// 第六步,绘制装饰,如前景、滚动条
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
// 第七步,绘制默认获取到焦点时的高亮色
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...省略部分绘制Edge的代码
}
在源码中,google的工程师就使用注释告诉了我们,draw
的流程是怎么样子的:
drawBackground()
。onDraw()
。dispatchDraw()
。drawAutofilledHighlight()
。mOverlay.getOverlayView().dispatchDraw()
。onDrawForeground()
。drawDefaultFocusHighlight()
。整个流程中,我们需要关心的只有绘制自身时的onDraw()
和绘制子view时的dispatchDraw()
。onDraw()
不用说,肯定是空的,毕竟每个view的样子都不一样,需要开发者自己去绘制。那么这里,我们就着重看看dispatchDraw()
是如何工作的。
跳转到View.dispatchDraw()
,你会发现是空方法。这个和布局Layout
时一样,作为view是没有子view的,所以也没必要去分发,因此这里我们直接去看ViewGroup.dispatchDraw()
。
@Override
protected void dispatchDraw(Canvas canvas) {
// 这里只贴一些比较重要的代码
...省略部分代码
// 执行子view进场动画,给viewgroup设置AnimationController
// 可以使得viewgroup下的所有子view的进场动画一致
// 感兴趣的可以去看看LayoutAnimationController这个类
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
...省略部分代码
}
// 裁剪掉非可见区域,调用setClipChildren()
int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
...省略部分代码
for (int i = 0; i < childrenCount; i++) {
// 不知道是什么,翻译为瞬态视图,但是其add方法已经被标记hide了,所以这里就不分析了
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
...省略部分无关代码
}
// 绘制子view
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// 这里要求子view可见或者有设置动画
// 这里调用的drawChild(),最终其实调用到了child.draw()
more |= drawChild(canvas, child, drawingTime);
}
}
// 这里同样是和瞬态view相关的,所以也不分析了
while (transientIndex >= 0) {
...省略部分代码
}
...省略部分代码
// Draw any disappearing views that have animations
// 绘制有动画但是不可见的子view
// 调用removeView的时候,会将有动画的view加入到mDisappearingChildren中
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
// 采用倒序,因为可能在动画结束后移除view
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...省略部分无关代码
}
整个逻辑已经在代码中进行了注释,通过阅读这里的源码,其实可以总结出两个优化的UI布局的技巧。
通过阅读源码,我们可以知道,越先添加的View将越先绘制,而先绘制的View将会被后绘制的View遮挡。其次,在ViewGroup.dispatchDraw()
这个方法中,我们可以得出两个优化UI布局的小技巧:
setChildClicp(true)
来裁剪掉非可见区域,减少绘制内容。通过这次的源码阅读,我们可以知道,在优化UI布局的时候,基本着手点都在布局Layout
和绘制Draw
这两个方向。具体可以概述为: