View的测量、布局及绘制过程
一、Activity、Window及DecorView关系
- Activity:页面
- Window:交互窗口,是一个抽象类
- PhoneWindow:Window的唯一实现类
- DecorView:一个页面的根View,继承自FrameLayout
- 既然我们知道整个View的Root是DecorView,那么View的绘制是从哪里开始的呢,我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口,每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的。
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
// mView就是DecorView
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
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;
}
return measureSpec;
}
二、setContentView后的ViewTree
上述的两个图已经很清楚地说明了setContentView后的结构。
View decorView = getWindow().getDecorView();
id是content的就是我们设置的布局的根布局,而DecorView是整个页面的根布局。
FrameLayout contentRootLayout = (FrameLayout)findViewById(android.R.id.content);
content下面id是linear的即是我们XML的布局结构。
三、MeasureSpec参数的理解
对于View的测量,肯定会和MeasureSpec接触,MeasureSpec是两个单词组成,翻译过来就是“测量规格”或者“测量参数”,更精确的说法应该这个MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,这个测量要求就是MeasureSpec。
大家都知道一个MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,是为了减少对象的分配开支。
MeasureSpec一共有三种模式:
- EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
- AT_MOST:子容器可以是声明大小内的任意大小
- UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大。这种一般用于系统内部,表示一种测量状态。
如果从代码上来看view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个MeasureSpec是父类传递过来的,但并不是完全是父View的要求,而是父View的MeasureSpec和子View自己的LayoutParams共同决定的,而子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的。
四、Measure过程
1、单纯一个View的Measure过程
View的测量过程其实是由measure方法来完成,这个方法是一个final类型,就说明是不可以被重写的,因此所有ViewGroup及Layout里是没有该方法的。
measure方法中会调用onMeasure方法,我们来看看View里的onMeasure方法源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 根据父控件传递下来的spec,来计算出当前这个View的尺寸
* 当模式是EXACTLY和AT_MOST时,View设置的大小即为父控件里传递下来的specSize
* 当模式是UNSPECIFIED时,使用当前View的默认大小(这个默认大小和是否设置背景及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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以得出一个结论:直接继承View的自定义控件,假如我们没有重写onMeasure去处理大小,那么当我们在布局里使用这个自定义控件时,使用wrap_content和match_parent是没有区别的,结果都是match_parent。需要结合上面的4X4表格以及View的getDefaultSize源码来进行分析,当我们的自定义View设置wrap_content时,这个View的测量规格应该是AT_MOST + parentSize,而上述源码我们也知道AT_MOST模式下,这个parentSize即为这个自定义控件设置的最终大小。
解决方案就是,自己重写onMeasure方法,在AT_MOST模式下自己去设置对应宽高即可
2、ViewGroup的Measure过程
ViewGroup是一个抽象类,并没有重写View的onMeasure方法,这是因为其子类(LinearLayout、FrameLayout等)都有不同布局特性及表现形式,因此ViewGroup没法做到统一实现,而交由其各个布局自己去实现onMeasure。
我们就以LinearLayout作为例子看源码:
/**
* LinearLayout的onMeasure方法
*/
@Override
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) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
// 去测量和设置子控件的宽高
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
...
// 最后才设置LinearLayout的宽高
// 通过mTotalLength这个变量来存储LinearLayout在竖直方向累加的高度
// 最终假如自己是wrap_content,就用这个mTotalLength来设置成自己的高度
// 最后部分看看resolveSizeAndState源码
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);
...
}
}
/**
* LinearLayout的measureChildBeforeLayout方法
*/
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
/**
* ViewGroup的measureChildWithMargins方法(不是LinearLayout的)
*/
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);
// 最终这里就是调用View的measure方法,View的measure方法上面有介绍
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/**
* LinearLayout的resolveSizeAndState方法
* 假如该LinearLayout是AT_MOST模式,那么就使用当前内容的累加高度和其父控件给
* 他的最大高度进行比较,取其最小的值。
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
3、测量后获取控件的宽高
在Activity中要去获取控件的宽高,很多时候我们调用getMeasureHeight/Width获取到是0,这是因为我们在onCreate、onStart、onResume中获取时,ViewTree并一定已测量完毕,因为View和Activity的生命周期兵没有任何关系,不能保证。有四种获取方法供参考:
3.1 Activity/View#onWindowFocusChanged,这个方法的含义是View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。但是要注意的是onWindowFocusChanged会被调用多次,当Activity的窗口获取的焦点和失去焦点的时候均会调用。
3.2 view.post(runnable),把一个消息放到消息队列的尾部,然后等待主线程的Looper去执行。
3.3 ViewTreeObserver,view树的状态发生改变时,会进行回调,注意其里的方法(比如onGlobalLayoutListener会被多次回调)
mView.getViewTreeObserver().addOnGlobalLayoutListener(new XXXListener) {
mView.getViewTreeObserver().removeGlobalLayoutListener();
int width = mView.getMeasuredWidth();
}
3.4 通过调用View的measure方法进行测量,比如
/*
* 具体数值
*/
int widthSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);
mActiveAreaLayout.measure(widthSpec, heightSpec);
/*
* wrap_content
* 1<<30 - 1 = 2的30次方-1 = Spec能存放的最大值
*/
int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
mActiveAreaLayout.measure(widthSpec, heightSpec);
五、Layout过程
1、单纯一个View的Layout过程
/**
* 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方法,而View自己是没有实现onLayout方法的
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
2、ViewGroup的Layout过程
和measure一样,ViewGroup也是没有实现onLayout方法,而是交由其子类(LinearLayout/FrameLayout等)来实现,我们仍然以LinearLayout作为例子:
/**
* LinearLayout的onLayout方法
*/
@Override
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);
}
}
/**
* LinearLayout的layoutVertical方法
*/
void layoutVertical(int left, int top, int right, int bottom) {
...
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去设置子控件的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
/**
* LinearLayout的setChildFrame方法
* 最终调用的就是View的layout方法,上面已经介绍过了
*/
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
六、Draw过程
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;
/*
* 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) {
// 第二步,绘制自己
if (!dirtyOpaque) onDraw(canvas);
// 第三步,绘制子控件
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;
}
// Step 4, draw the children
dispatchDraw(canvas);
}
@Override
protected void dispatchDraw(Canvas canvas) {
...
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
} else {
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
......
}