自定义View主要分measure,layout,draw三大步骤,对于直接继承View,则只需要完成measure和draw,对于直接继承ViewGroup的自定义view,则需要完成measure,layout,draw这三个步骤。view的绘制流程是重ViewRoot的performTraversals方法开始的,performTraversals方法会依次调用performMeasure方法,performLayout,performDraw方法来完成一个view的绘制。performMeasure方法中,会调用measure方法,measure方法完成对自身的测量,接着又回调用onMeasure方法,onMeasure方法内部,又回调用各个子view的measure方法完成对子view的测量,子view在重复这个过程,完成对整个view树的测量工作。performLayout和performDraw的过程和performMeasure的过程是类似的,只不过,绘制子view时,调用的是dispatchDraw方法。这就是view绘制的整体流程。整体流程知道了,我们还需要了解各个步骤的细节,才能写出自定义控件。下面开始了解一下,measure这个步骤的具体细节。
Measure过程:
/**
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//具体代码省略
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
首先,我们查看View的源码的measure方法,该方法是final修饰的,表示该方法不能被重写,可以看到该方法需要传递两个参数,widthMeasureSpec和 heightMeasureSpec,通过这个方法的参数注释,可以知道这两个参数,是被父容器给确定的。为了方便理解后续的流程,我们先要了解什么widthMeasureSpec是个什么东西?简单的说,它就是测量规格,它是32位的int值,高两位代表了测量模式(specMode),低30位代表了测量的大小(specSize)。可以通过以下方法获取到测量模式和测量的大小
int specMode = MeasureSpec.getMode(measureSpec);//测量模式
int specSize = MeasureSpec.getSize(measureSpec);//测量大小
android系统一共提供了三种测量模式,分别是:
MeasureSpec.UNSPECIFIED
这种测量模式表示,控件想要多大就要多大,父控件对子控件不做限制,一般用于系统内部
MeasureSpec.AT_MOST
这种测量模式表示,父容器已经指定了一个specSize,子控件的最大宽度/高度不能超过这个specSize,但是,view的具体大小,要看view的具体实现
MeasureSpec.EXACTLY
这种测量模式表示,父容器已经检测出了View的大小,这个大小就是specSize
接着看onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这个方法内部看起来很简答,也只是调用了setMeasuredDimension(int measuredWidth,int measuredHeight)方法,这个方法的作用就是将测量的宽和高设置给view。这个方法的传入的参数,是通过getDefaultSize(int size, int measureSpec)方法来获取到测量的宽高的,下面看看getDefaultSize(int size, int measureSpec)是如何获取到测量的宽和高的。
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,则返回的测量大小就是specSize,如果测量模式是UNSPECIFIED,则测量的大小就是传入的size。那这个传入的size究竟是多大呢?这个size的大小是getSuggestedMinimumWidth()方法获取的,下面我们来看看getSuggestedMinimumWidth()方法的源码
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
通过getSuggestedMinimumWidth()方法内部的代码可以知道,如果view的background为null,则这个方法返回的大小就是mMinWidth,这mMinWidth其实就是View的属性android:minWidth设置的大小,如果View未设置这个minWidth属性,则mMinWidth的默认大小就是0,如果view的background不为null,则会执行max(mMinWidth, mBackground.getMinimumWidth())也就是取mMinWidth和mBackground.getMinimumWidth()两者中的较大值。我们在看看mBackground.getMinimumWidth()方法,这个方法是Drawable类中的方法,getMinimumWidth()方法的源码如下:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
通过查看getMinimumWidth()方法的源码发现,如果这个background是个BitmapDrawable,则返回这个BitmapDrawable的大小,如果是ShapeDrawable,则返回是0。经过对setMeasuredDimension方法的分析,我们知道,view的测量宽度/高度,就是传入的measurSpec这个测量模式中的specSize。为什么这样说呢,因为一般我们自定义的view,测量模式不会是UNSPECIFIED的,所以,在getDefaultSize方法中,也就不会执行UNSPECIFIED这个case分支,所以getDefaultSize返回的就是specSize。这个specSize就是view的测量宽高,注意是
测量宽高,不是最终宽高,因为最终宽高是在layout方法中才确定的。这就是view的测量过程。分析完这个过程,有些人可能还有疑问,父容器是如何计算view的测量模式的呢?这种情况还只是view存在父容器的时候,如果对于DecorView,这样的顶层view,它是没有父容器的,它的MeasureSpec是如何计算的呢?我们先来看DecorView的MeasureSpec是如何计算的,对于,DecorView来说,它的具体实现类ViewRootImpl中的measureHierarchy方法中有如下一段代码:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.widht);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
接下来,看看getRootMeasureSpec方法的实现:
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;
}
从getRootMeasureSpec方法中,可以得出结论,DecorView的测量模式,只有两种,一种是EXACTLY,一种是AT_MOST,当DecorView的layoutParams是具体的数字或者MATCH_PARENT时,它的测量模式就是EXACTLY,否则就是AT_MOST,并且,当DecorView的宽度是具体的数字时,它的specSize就是这个具体的数字,否则,它的specSize就是window的宽或者高。其实通过getRootMeasureSpec方法,可以确认,DecorView就是走的第一个case。它的测量模式就是EXACTLY,specSize就是window的宽度或者高度。看完了DecorView的测量模式是如何计算的后,在来看看普通的view的测量模式是如何计算的。对于普通的view,也就是我们的Activity显示的布局的根View是一个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);
}
通过measureChildWithMargins方法,我们可以发现,子view进行测量前,会先计算子view的measureSpec,具体的计算过程是在getChildMeasureSpec()这个方法中完成的,下面我们来看看ViewGroup类中的getChildMeasureSpec()这个方法的具体实现:
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
通过对getChildMeasureSpec()代码的分析,发现,view的measureSpec是由父容器的messureSpec和view的layoutParms共同决定的。并且可以得出以下结论:
1.当view的宽或高采用具体数字,比如100px或者100dp时,无论父容器的测量模式是什么,view的specMode都是EXACTLY的,并且view的大小就是这个具体的数字。
2.当view的宽或高采用MATCH_PARENT时,view的specMode和父容器的测量模式保持一致。但是specSize的值,确不同,当父容器的SpecMode是UNSPECIFIED时,view的specSize是0,当父容器的SpecMode是EXACTLY或者AT_MOST时,view的specSize的大小就是父容器的剩余空间。
3.当view的宽或高采用WRAP_CONTENT时,如果父容器的SpecMode是EXACTLY或者AT_MOST时,view的测量模式就是AT_MOST,当父容器的specMode是UNSPECIFIED时,view的测量模式也是UNSPECIFIED,view的测量大小也受父容器的测量模式的影响,具体是当父容器的SpecMode是EXACTLY或者AT_MOST时,view的specSize的大小最大不能超过父容器的剩余空间。父容器的specMode是UNSPECIFIED时,view的测量大小就是0 。
根据getChildMeasureSpec方法的逻辑,可以总结出view的测量模式和父容器的测量模式以及view自身的layoutParams之间的关系图如下:
通过上图,就可以很好的解释一个现象,当我们自定义一个CustomView,直接继承View时,如果CustomView的layoutParams是WRAP_CONTENT时,如果不给CustomView设定一个默认的宽和高,则在使用CustomView时,无论CustomView的宽或者高设置成MATCH_PARENT或者WRAP_CONTENT时,CustomView的大小始终是一样的,都是父控件的剩余空间,也就是上图中的parentSize,也就是说给CustomView的宽高设置成MATCH_PARENT和WRAP_CONTENT是CustomView的大小是一样的,这就和我们平时使用系统控件时给系统控件设置成MATCH_PARENT和WRAP_CONTENT看到的效果是不一样的。这样就违背了我们的主观逻辑。为了解决这个问题,我们需要 在CustomView的layoutParams是WRAP_CONTENT时,给CustomImageview设置默认的宽高。
上面这个过程完成了View的measure过程分析,具体ViewGroup的测量是如何进行的呢?其实ViewGroup是个抽象类,它没有重写父类View的onMeasure方法,而是在自己内部定一个了measureChildren方法,通过该方法完成对子view的测量。下面来看看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);
}
}
}
这个方法内部其实,就是遍历自己的子view,然后判断子view的Visibility是否是GONE,如果不是的,就
调用measureChild对各个子view进行测量。下面来看看measureChild方法的具体实现:
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方法的实现过程就很明确了,就是根据子view的layoutParmas和父容器的MeasureSpec,通过
getChildMeasureSpec来计算出子view的MeasureSpec,在将计算的MeasureSpec传入到view的measure方法中,完成对view的测量工作了,后续的具体测量过程,前面已经分析过了,这里就不再重复这个过程了。
前面提到,ViewGroup是个抽象类,它并没有实现onMeasure方法,为什么ViewGroup方法没实现onMeausre方法呢?这是因为,每个具体的ViewGroup的子类,他们的特性都是不同的,这就决定了他们的测量方式就是不同的,比如,LinearLayout,它在Vertical方法和Horizontal方法的测量方式就是不同的,需要不同的实现,RelativeLayout的特性和LinerLayout也是不同的,导致RelativeLayout的测量方式和LinearLayout的测量方式也是不同的,这样LinearLayout
,RelativeLayout的父类ViewGroup中的onMeasure就不能有具体实现,只能是它的各个子类自己去按照自己的布局特性去自己实现。
有时候我们想要在Activity中获取view的宽和高,有些人可能直接就在Activity的生命周期方法中去调用view的getWidth()或者getMeasuredWidth()方法去获取,这种获取view的宽高的方式其实并不能获取到view准确的宽高,这是因为,Activity的生命周期和view的测量过程并不是同步的,这就导致,在Activity的生命周期中获取view的宽高时,view还并未测量或者还未测量完毕,这时获取到的view的宽高是不准确的。想要获取的view的准确宽高,可以通过如下几种方式:
1.通过view获取到ViewTreeObserver,然后给ViewTreeObserver添加onGlobalLayoutlisnrter监听,当View树内部的view的可见性发生改变时,就会回调onGlobalLayout方法,这时,就可以在这个方法中获取view的宽高了。具体获取view宽高的kotlin代码如下:
tv.viewTreeObserver.addOnGlobalLayoutListener (object:ViewTreeObserver.OnGlobalLayoutListener{
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
override fun onGlobalLayout() {
tv.viewTreeObserver.removeOnGlobalLayoutListener(this)
LogUtil.i("${tv.width } ${tv.measuredWidth}")
}
} )
2.通过view的post方法,将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化完毕,这时获取到的view的宽高就是准确的。具体代码实现:
val task = Runnable {
LogUtil.i("${tv.width} ${tv.measuredWidth}")
}
tv.post(task)
3.在Activity的onWindowFocusChanged方法中,获取view的宽高,具体代码实现:
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if(hasFocus){
LogUtil.i("${tv.width} ${tv.measuredWidth}")
}
}