这些读书笔记是根据《Android开发艺术探索》和《Android群英传》这两本书,然后将书上的一些知识点做一下记录。方便学习和理解,如果有存在侵犯版权的地方,还麻烦告知。个人强烈建议购买这两本书。真心不错。
本节是和《Android开发艺术探索》中的第4章 “View的工作原理” 有关系,建议先买书查看这一章。
[]初始ViewRoot和DecorView
ViewRoot:ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl和DecorView建立关联。这个过程可参考如下源码:
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
View的绘制流程是从ViewRoot的performTraversals()开始,它经过measure(),layout(),draw()三个过程才能最终将一个View绘制出来,其中measure()用来测量View的宽和高,layout()用来确定View在父容器中的放置位置,draw()负责将View绘制在屏幕上。
performTraversals()的流程:会依次调用performMeasure(),performLayout(),performDraw()三个方法,这三个方法分别完成顶级View的measure(),layout(),draw()三个流程,其中在performMeasure()会调用measure(),在measure()又会调用onMeasure(),在onMeasure()中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素。这样就完成一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View的遍历,同理,performLayout()和performDraw()的传递流程和performMeasure()是类似的。唯一不同的是,performDraw()的传递过程是在draw()通过dispatchDraw()来实现的。
measure过程决定View的宽/高。measure完成后,可以通过getMeasuredWidth()和getMeasuredHeight()来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View的最终的宽/高,但是在特殊情况下除外。layout过程决定View的四个顶点的坐标和实际的View的宽/高。完成以后,可以通过getTop(),getBottom(),getLeft(),getRight()来获取View的四个顶点的位置,并可以通过getWidth()和getHeight()来获取View的最终宽/高,draw过程决定View的显示,只有draw()完成以后View的内容才能呈现在屏幕上。
DecorView:DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中通过setContentView()设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,因此可以理解为Activity指定布局的方法不叫setView()而叫setContentView(),因为我们的布局的确加到id为content的FrameLayout中。DecorView其实是一个FrameLayout,View层的事件都先通过DecorView,然后才传递给我们的View。
[]理解MeasureSpec
{}MeasureSpec
MeasureSpec在很大程度上决定一个View的尺寸规格,因为这个过程还受到父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个MeasureSpec来测量出View的宽/高,这里的宽/高是测量宽/高,不一定等于View的最终宽/高。
MeasureSpec代表一个32位int值。高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,SpecSize是指在某种测量模式下的规格大小。
SpecMode有三类:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的math_parent和具体的数值这两种模式。
AT_MOST
父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么要看View的具体实现,它对应于LayoutParams中的wrap_content。
{}MeasureSpec和LayoutParams的对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步确定view的宽高。对于顶级View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定。对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。MeasureSpec一旦确定后,onMeasure()中就可以确定View的测量宽/高。
对于DecorView来说,在ViewRootImpl中的measureHierarchy()中有如下的一段代码,它展示了DecorView的MeasureSpec的创建过程。
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
通过上述代码, 对于顶级View(DecorView)来说MeasureSpec产生遵守如下规则,根据它的LayoutParams的宽/高参数来划分。
LayoutParams.MATH_PARENT:精确模式,大小就是窗口大小。
LayoutParams.WRAP_CONTENT:最大模式,大小不定。但是不能超过窗口的大小。
固定大小:精确模式,大小为LayoutParams中指定的大小。
对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先来看一下ViewGroup的measureChildWithMargins()。
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);
}
上述方法会对子元素进行measure,在调用子元素的measure()之前会先通过getChildMeasureSpec()来得到子元素的MeasureSpec,从代码来看,子元素的MeasureSpec创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关。
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 = 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
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec()的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小。
对于普通View,针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec。View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小是LayoutParams中的大小,当View的宽/高是math_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间,当View的宽/高是wrap_content,不管父容器是精准还是最大,View的模式总是最大化并且大小不能超过父容器的剩余空间。UNSPECIFIED 模式主要用于系统内部多次MeasureSpec的情形,一般来说,我们不需要关注此模式。
[]View的工作流程
View的工作流程主要是指measure,layout,draw三大流程,即测量,布局,绘制。其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,draw将View绘制到屏幕上。
{}measure过程
measure过程要分情况来看,如果只是一个原始的View,那么通过measure()就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure(),各个子元素在递归去执行这个流程。
1,View的measure过程
View的measure过程由其measure()来完成,measure()是一个final类型的方法。这意味着子类不能重写此方法,在View的measure()会去调用onMeasure(),因此只需要看onMeasure()的实现即可。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension()会设置View宽/高的测量值。因此只需要看getDefaultSize()即可。
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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getDefaultSize()中我们只需要看AT_MOST和EXACTLY这两种情况,简单的理解,其实getDefaultSize()返回的大小就是measureSpec中的specSize。而这个specSize就是View测量后的大小,
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()的第一个参数size,即宽/高分别是getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
getSuggestedMinimumWidth():
如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应android:minWidth这个属性指定的值,因此View的宽度即为android:minWidth属性指定的值,如果这个属性没有指定,那么mMinWidth则默认为0,如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimunWidth())
mBackground.getMinimunWidth()返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。
从getDefaultSize()的实现来看,View的宽/高由specSize决定,所以我们得出如下结论:直接继承View的自定义控件需要重写onMeasure()并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。因为如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,这这种模式下,它的宽/高等于specSize,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小。也就是父容器当前剩余的空间大小,很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和布局中使用match_parent完全一致。如何解决:只需要高View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高。对于非wrap_content情况,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小则根据业务指定。
2,ViewGroup的measure过程
ViewGroup除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure(),各个子元素再递归去执行这个过程,和View不同的是
ViewGroup是一个抽象类,因此它没有重写View的onMeasure(),但是它提供了一个measureChildren(),
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
从上述代码来看,ViewGroup在measure时,会对每个子元素进行measure。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild()的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec()来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()进行测量。
为啥ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()需要各个子类去实现,为啥不统一,这是因为不同的ViewGroup子类有不同的布局特性。以下分析LinearLayout的onMeasure()。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
选择竖直布局的LinearLayout的测量过程,即measureVertical()。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
measureVertical():系统会遍历子元素并对每个子元素执行measureChildBeforeLayout(),这个方法会调用子元素的measure(),这样子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度,每测量一个子元素,mTotalLength就会增加,增加的部分主要包括子元素的高度以及子元素在竖直方向上的margin等,当子元素测量完毕后,LinearLayout会测量自己的大小。
当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小,针对竖直的LinearLayout来说,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程则和View有所不同,具体来说是指,如果它的布局中高度采用match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize。如果它的布局中高度采用wrap_
content,那么它的高度是所有子元素占用的高度总和,但是不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向的padding。
measure完成后,通过getMeasuredWidth()和getMeasureHeight()就可以正确的获取到View的测量宽/高,需要注意的是。在某些极端情况下,系统可能需要多次measure才能最终确定最终的测量宽/高,在这种情况下,onMeasure()拿到的测量宽/高很可能是不准确的吗,一个比较好的习惯是在onLayout()中去获取View的测量宽/高或者最终宽/高。
在Activity已启动的时候就做一件任务,这个任务需要获取某个View的宽/高。在onCreate(),onStart(),onResume()中均无法正确得到某个View的宽/高,这是因为View的measure过程和Activity什么周期不是同步执行的,因此无法保证Activity执行了onCreate(),onStart(),onResume()时某个View已经测量完毕,如果View还没有测量完毕,那么获取的宽/高就是0。下面给出四种方法来解决这个问题:
1,Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这时去获取宽/高是没有问题的。需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。具体来说,当Activity继续执行和暂停执行时,它均会被调用。如果频繁的进行onResume()和onPause(),那么它就会被频繁的调用。
2,view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
3,ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout方法会被回调。因此这时获取View的宽/高一个很好的时机。需要注意的是,伴随着View树状态的改变,onGlobalLayout会被回调多次。
4,view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
1,match_parent:
直接放弃,无法measure出具体的宽高,原因很简单,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间。而这个时候我们无法知道parentSize的大小,所以理论上无法测量出View的大小。
2,具体的数值(dp/px)
3,wrap_content
{}layout过程
layout过程是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout()中会遍历所有的子元素并调用其layout()。在layout()中onLayout()又会被调用,layout()确定View本身的位置,onLayout()则会确定所有子元素的位置。
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;
}
layout()的流程:首先会通过setFrame()来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了,接着会调用onLayout(),这个方法是父容器确定子元素的位置。onLayout()的具体实现和具体的布局有关。所以View和ViewGroup都没有真正实现onLayout()。
以LinearLayout的onLayout()为例:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
以layoutVertical()来讲解:首先会遍历所有子元素并调用setChildFrame()来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout特性。setChildFrame()它仅仅是调用子元素的layout()而已,这样父元素在layout()中完成自己的定位以后,就通过onLayout()去调用子元素的layout(),子元素又会通过自己的layout()来确定自己的位置,这样一层一层地传递下去完成整个View树的layout过程。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame()中的width和height实际上就是子元素的测量宽/高。
View的测量宽/高和最终宽/高有什么区别,也就是View的getMeasuredWidth()和getWidth()的区别。View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成与View的measured过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们认为View的测量宽/高等于最终宽/高。但是的确存在某些特殊情况会导致两者不一致。比如重写View的layout(),代码如下:
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+100);
}
这样会导致View的最终宽/高总是比测量宽/高大100,虽然这样会导致View显示不正常并且没有实际意义。但是这说明了测量宽/高和最终宽/高可以不相等。
另外一种情况时在某些情况下,View需要多次measure才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但是最终来说,测量宽/高和最终宽/高是相等的。
{}draw过程
draw过程的作用是将View绘制到屏幕上,View的绘制过程遵守下面几步:
1,绘制背景:drawBackground(canvas)。
2,绘制自己:onDraw()。
3,绘制children:dispatchDraw()。
4,绘制装饰:onDrawForeground()。
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);
// 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);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// 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);
}
View绘制过程的传递是通过dispatchDraw()来实现的,它会遍历调用所有子元素的draw(),如此draw事件就一层层地传递下去。
注:
()drawBackground()是private的,不能被重写,可以通过android:background或者View.setBackgroundXxx()来设置。
()onDrawForeground()绘制装饰包括:滑动边缘渐变,滑动条,前景,都是在这个方法中重写。
()滑动边缘渐变和滑动条可以通过android:scrollbarXXX或者View.setXXXScrollbarXXX()来设置,前景可以通过android:foreground或者View.setForeground()来设置。
setWillNotDraw(boolean willNotDraw)
从注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化(不调用draw(),直接执行dispatchDraw())。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化,当明确知道一个ViewGroup需要通过onDraw来绘制内容的时候,需要显式的关闭标记位。
[]自定义View
{}自定义View的分类
1,继承View重写onDraw()
这种方式主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw()。采用这种方式需要自己支持wrap_content和padding。
2,继承ViewGroup派生特殊的Layout
这种方式主要用于实现自定义的布局,即除了系统定义的布局,我们重新定义一种新布局。当某种效果看起来很像几种View组合在一起时,可以采用这种方式来实现。需要合适地处理ViewGroup的测量和布局这两个过程,并同时处理子元素的测量和布局过程。
3,继承特定的View(比如TextView)
这种方式一般是用于扩展某种已有的View的功能,比如TextView,这种方式不需要自己支持wrap_content和padding。
4,继承特定的ViewGroup(比如LinearLayout)
当某种效果看起来很像几种View组合在一起时,可以采用这种方式来实现。不需要自己处理ViewGroup的测量和布局这两个过程,方式2能实现的方式4都能实现,主要的差别是方式2更接近View的底层。
{}自定义View须知
1,让View支持wrap_content
因为直接继承View或者ViewGroup,如果不在onMeasure()中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content就无法达到预期的效果。
2,如果有必要,让你的View支持padding
因为直接继承View,如果不在draw()中处理padding,那么padding属性是无法起作用的,直接继承ViewGroup需要在onMeasure()和onLayout()中考虑padding和子元素的margin对其的影响。不然将导致padding和子元素的margin失效。
3,尽量不要在View中使用Handler,没必要
因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你明确地要使用Handler来发送消息。
4,View中如果有线程或者动画,需要在View的onDetachedFromWindow中停止
如果有线程或者动画需要停止,如果不及时处理,有可能会造成内存泄漏,如果要处理,那么onDetachedFromWindow()是一个很好的时机,当包含此View的Activity退出或者当前View被remove()时,View的onDetachedFromWindow()会被调用。当包含此View的Activity启动时,View的onAttachedToWindow()会被调用。
5,View带有滑动嵌套情况时,需要处理好滑动冲突
如果有滑动冲突,那么要合适的处理滑动冲突,否则将会严重影响View效果。