private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, “measure”);
try {
//调用 View 的 measure 方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
//说明 2.
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
…
final View host = mView;//代表 DecorView
…
//内部在调用 View onLayout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
…
}
//说明 3.
private void performDraw() {
…
//内部通过调用 GPU/CPU 来绘制
draw(fullRedrawNeeded);
…
}
复制代码
到这里上面对应的函数会对应调用 View 的 onMeasure -> onLayout -> ondraw 方法,下面我们就具体来说明下绘制过程。
measure 过程
measure 过程要分情况来看,如果一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历它所有的子 View 的 measure 方法,各个子元素在递归去执行这个流程(有子 View 的情况),下面针对这两种情况分别讨论。
View 的 measure 过程
//V
iew.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
…
onMeasure(widthMeasureSpec, heightMeasureSpec);
…
}
复制代码
View 的 measure 过程由其 measure 方法来完成,通过 View#measure 源码可以知道 它是被 final 修饰的,那么就代表了子类不能重写,通过上面源码我们知道在 View#measure 内部又会去调用 onMeasure 方法,我们接着看它的源码实现,代码如下:
//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码
上面的代码就做了一件事儿,就是设置测量之后的宽高值,我们先来看看 getDefaultSize 方法,代码如下:
//View.java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//根据 measureSpec 拿到当前 View 的 specMode
int specMode = MeasureSpec.getMode(measureSpec);
//根据 measureSpec 拿到当前 View 的 specSize
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //一般用于系统内部的测量过程
result = size;//直接返回传递进来的 size
break;
case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子类决定但是不能超过父类
case MeasureSpec.EXACTLY://精准模式
result = specSize;//返回测量之后的大小
break;
}
return result;
}
复制代码
通过上面代码可以看出,getDefaultSize 内部逻辑不多,也比较简单,对于我们来说只需要关心 AT_MOST, EXACTLY 这两种情况就行,其最终就是返回测量之后的大小。这里要注意的是这里测量之后的大小并不是最终 View 的大小,最终大小是在 layout 阶段确定的,所以这里一定要注意。
我们来看一下 getSuggestedMinimumXXXX() 源码实现:
//View.java
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码
可以看到 getSuggestedMinimumXXX 内部的代码意思就是,如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以为 0,如果 View 设置了背景,则返回 android:minWidth 和背景的 最小宽度/最小高度 这两者的者中的最大值,它们返回的就是 UNSPECIFIED 情况下的宽高。
从 getDefaultSize 方法实现来看, View 的宽高由 specSize 决定,所以我们可以得到如下结论:既然 measure 被 final 修饰不能重写,可是我们在它内部也发现了新大陆 onMeasure 方法,我们可以直接继承 View 然后重写 onMeasure 方法并设置自身大小。
这里在重写 onMeasure 方法的时候设置自身宽高需要注意一下,如果在 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,也就是父类控件空剩余可以使用的空间大小,这种效果和在布局中使用 match_parent 完全一致,那么如何解决这个问题勒,可以参考下面代码:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
/**
- 说明在布局中使用了 wrap_content 模式
*/
if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight)
}else if (widthMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSize)
}else if (heightMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,mHeight)
}else {
setMeasuredDimension(widthSize,heightSize)
}
}
复制代码
在上面代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth、mHeight),并在 wrap_content 的时候设置此宽高即可。对于非 wrap_content 情形,我们就沿用系统的测量值即可。
ViewGroup 的 measure 过程
对于 ViewGroup 来说,初了完成自己的 measure 过程以外,还会去遍历调用所有的子 View 的 measure 方法,各个元素递归去执行这个过程,和 View 不同的是 ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它定义了一个 measureChild 方法,代码如下:
//ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
//拿到所有子 View
final View[] children = mChildren;
//遍历子 View
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//依次对子 View 进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
复制代码
上面代码也很简单,ViewGroup 在 measure 时,会对每一个子元素进行 measure ,如下代码所示:
//ViewGroup.java
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//拿到该子 View 在布局XML中或者代码中定义的属性
final LayoutParams lp = child.getLayoutParams();
//通过 getChildMeasureSpec 方法,根据父元素的宽高测量规则拿到子元素的测量规则
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//拿到子元素的测量规则之后传递到 View 中,开始 measure 流程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
measureChild 代码逻辑也很容易理解,首先取出设置的 LayoutParams 参数,然后通过 getChildMeasureSpec 方法,根据父元素的宽高测量规格拿到子元素的测量规格,最后将拿到的测量规格直接传递给 View#measure 来进行测量。
measure 小总结:
- 获取 View 最终宽高,需要在 onLayout 中获取,因为 measure 在某些极端的情况下需要测量多次。
- 在 Activity 中获取 View 的宽高需要使用
Activity/View#onWindowFocusChanged
、view.post(runnable)
、ViewTreeObserver 的 onGlobalLayoutListener 回调
、手动调用 view.measure(int width,int height)
,最终使用哪个以实际情况来定。