View的工作流程:Measure,Layout,Draw
标签(空格分隔): Android自定义控件
本文仅作为总结学习用途,感谢开放的互联网,感谢乐于分享的前辈。
在讲View的三大流程之前,先做一下准备工作,熟悉与其相关的类。
MeasureSpec
MeasureSpec类似于测量指导,包括size(低32位)和mode(高2位)两个属性,用来规范子元素的测量标准。
很明显,size表示大小,mode有UNSPECIFIED,EXACTLY,AT_MOST三种模式:
- UNSPECIFIED:父View不对子View有任何限制
- EXACTLY:父View给出了精确大小,对应match_parent和具体数值(xxdp)情形。
AT_MOST:父View给了一个可用大小,子View不能大于这个值,对应wrap_content。
- 对于DecorView,其MeasureSpec由屏幕尺寸和自身的LayoutParams来确定。
- 参于普通View,其MeasureSpec由父View的MeasureSpec和自身的LayoutParams来确定。
measureChildWithMargins
ViewGroup.java
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);
}
- 第4行获取子View的LayoutParams。
- 第6行-11行通过getChildMeasureSpec获取子View的MeasureSpec。
- 第13行调用子View的measure进行测量
getChildMeasureSpec
ViewGroup.java
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);
}
普通View的MeasureSpec创建规则(图来源于Android开发艺术探索):
1. 子View的LayoutParams是dp/px的情况下,表示子View已经独立自主,确立了自己的标准,所以MeasureSpec模式都是EXACTLY,MeasureSpec大小也是自己设定的dp/px。
2. 子View的LayoutParams是match_parent的情况下,表示子View向父View看齐,所以MeasureSpec模式跟父View保持一致,见表格第二行。
3. 子View的LayoutParams是wrap_content的情况下,表示子View想决定自己的大小,但还是受父View的监管,所以MeasureSpec模式是AT_MOST。见表格第三行。
4. 父View是UNSPECIFIED的情况下有点特殊,一般不需要亲关注,可以参考源码体会。
measure过程
View和ViewGroup的measure过程有所不同,View只要测量自己就可以了,而ViewGoup除了要测量自己外,还要遍历去调用子元素的measure方法完成测量。
view的measure分析
View$measure方法会去调用View的onMeasure方法。
measure->onMeasure->setMeasuredDimension->getDefaultSize
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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,对应View的测量值。
setMeasuredDimension和getSuggestedMinimumWidth不是重点,略过。
注意:直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时自身的大小。因为从前面的表格可以看出,当view的layoutParams为wrap_content时,它的specMode是AT_MOST模式。它的大小等于specSize,也就是父View当前剩余的大小。这种效果相当于在布局中使用match_parent完全一致了。
ViewGroup的measure过程
ViewGroup中没有onMeasure方法,而是提供了一个measureChildren方法去遍历所有子View的measure方法。
measureChildren->measureChild->getChildMeasureSpec->child.measure
由于不同的ViewGoup布局特性差异很大,所以ViewGoup把onMeasure方法留给其子类去实现。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。
LinearLayout的measure过程分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
/TODO:测量过程比较繁琐,后续再补充
正确获取View的的方法
1.Activity/View$onWindowFocusChanged
当Activity的窗口获得焦点和失去焦点的时候均会调用该方法,示例代码:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.view.post(runnable)
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
3.ViewTreeObserver
当View树的状态发生改变或者内部的View的可见性发现改变时,onGlobalLayout方法将被回调。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动对view进行measure,需要根据view的LayoutParams来区分:
4.1 match_parent
这种情况无法准确测量,因为不知道父view的剩余大小。
4.2 具体的数值
假设宽/高是100dp
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
4.3 wrap_content
MeasureSpec高2位表示SpecMode,低30位表示SpecSize,所以这里使用(1<<30)-1表示View理论上能支持的最大值。
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
layout过程
在完成了View的测量之后,接下来进行的是View的布局。
public void layout(int l, int t, int r, int b) {
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;
// ...
}
}
直接捡重点分析
l,t,r,b分别表示view相对于parent左,上,右,下端的距离。
1. setFrame确定View的四个顶点的位置。
2. onLayout确定子View的位置,由于onLayout的实现跟具体布局有关,所以View和ViewGoup都没有实现该方法。
以LinearLayout的onLayout为例,它会遍历子View,会一级一级调用子View的layout方法,完成整个View树的layout过程。
调用逻辑是onLayout->layoutVertical->setChildFrame->child.layout
@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);
}
}
getMeasuredWidth和getWidth的区别:
这两个方法都是用来获取View的宽度,一般情况下二者的值都是相同的。
不同的是getMeasuredWidth的值形成于measure过程,getWidth的值形成于layout过程。
draw过程
draw过程比较简单,步骤如下:
1. 绘制背景background.draw(canvas)
2. 绘制自己(onDraw)
3. 绘制children(dispatchDraw)
4. 绘制装饰(onDrawScrollBars)