引言
自定义View作为Android开发者必须掌握的重点和难点,它是android开发的核心技能之一。网络上有很多介绍它们的文章,但存在一些问题:内容不全、浅尝辄止、无源码分析等等。在接下来的几篇博客当中,我将从View的测量、布局、绘制、触摸事件分发机制以及弹性滚动这几方面入手,从源码层面理解它们各自的实现原理,帮助大家彻底明白自定义View的实现原理,踩一踩坑。(源码为API26,与之前版本可能有些改动,但原理不变。)
(一)View的measure流程
1.理解MeasureSpec
/**源码分析:理解MeasureSpec
* <<========分析(1)========>>
A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
*
* - UNSPECIFIED
* -
* The parent has not imposed any constraint on the child. It can be whatever size
* it wants.
*
*
* - EXACTLY
* -
* The parent has determined an exact size for the child. The child is going to be
* given those bounds regardless of how big it wants to be.
*
*
* - AT_MOST
* -
* The child can be as large as it wants up to the specified size.
*
*
*
* MeasureSpecs are implemented as ints to reduce object allocation. This class
* is provided to pack and unpack the <size, mode> tuple into the int.
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/** <<========分析(2)========>>
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
*
* - {@link android.view.View.MeasureSpec#UNSPECIFIED}
* - {@link android.view.View.MeasureSpec#EXACTLY}
* - {@link android.view.View.MeasureSpec#AT_MOST}
*
*
* Note: On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide internal use only for compatibility with system widgets and older apps
*/
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**<<========分析(3)========>>
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**<<========分析(3)========>>
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Returns a String representation of the specified measure
* specification.
*
* @param measureSpec the measure specification to convert to a String
* @return a String with the following format: "MeasureSpec: MODE SIZE"
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
分析(1):通过该类的注释,我们知道: MeasureSpec封装父布局传给子布局的布局测量要求,是View测量的依据,它包括两个部分:测量模式和测量大小,它们封装在一个int数中,高两位是测量模式,低30位为大小,二者共同确定子布局的期望大小,为啥弄这么复杂呢?了解C语言嵌入式开发的都知道,通过移位操作将两个信息封装到一个int中,减少对象内存分配。
测量模式有三种:
1> UNSPECIFIED:子布局大小没有任何限制,主要用作系统内部测量,实际开发很少用到;
2> EXACTLY:父布局测出子View所期望的大小就是子View的大小,对应子View的布局参数为match_parent或者具体数值;
3> AT_MOST:父布局给出的期望大小size,子View大小不能超过这个size,对应子View布局参数为wrap_content,该模式下父布局只是限定了子View的大小上限,View的大小计算由自身确定,这里会引申出自定义View的布局参数为wrap_content不起作用的问题,后面会解释这个问题。
分析(2): makeMeasureSpec方法将mode和size封装到一个int里。
分析(3):getMode和getSize方法是通过位操作分别取出mode和size。
2.MeasureSpec的生成
前面说了那么多,这个父布局到底是怎么生成MeasureSpec给子View的呢?它是根据父布局的MeasureSpec以及子View的布局参数(一下简称LP)得到的,具体方法在ViewGroup的getChildMeasureSpec中。
/**源码分析: getChildMeasureSpec
*作用: 根据父视图的MeasureSpec & 布局参数LP,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
/* 注:@param spec 父布局的测量信息,由它的父布局传过来的
* @param padding :父布局的padding
* @param childDimension :子View的LP参数
*/
//父布局的测量模式
int specMode = MeasureSpec.getMode(spec);
//父布局的大小
int specSize = MeasureSpec.getSize(spec);
//父布局给子View的剩余空间
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//核心代码:通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY://父布局size确定
if (childDimension >= 0) {//子布局size确定
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//测量结果:EXACTLY+childsize
} else if (childDimension == LayoutParams.MATCH_PARENT) {//LP为match_parent
// Child wants to be our size. So be it.
resultSize = size;//大小为父布局size
resultMode = MeasureSpec.EXACTLY;
//测量结果:EXACTLY+parentsize
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//LP为wrap_content
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
//测量模式:AT_MOST+parentsize
}
break;
// Parent has imposed a maximum size on us
// 当父布局的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {//子布局大小为具体值
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//测量结果:EXACTLY+childsize
} else if (childDimension == LayoutParams.MATCH_PARENT) {//子view LP=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;
//测量结果为AT_MOST+parentsize
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//注意:子view LP = wrap_content时候与LP = match_parent的测量结果相同
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
//测量结果为AT_MOST+parentsize
}
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);
}
注意:由图可以看出,自定义View在LP=wrap_content和match_parent,在父布局AT_MOST测量模式下,效果是一样的,因此需要对自定义View在LP=wrap_conten时做特殊处理,指定默认值,这样就解决前面提到的wrap_content失效问题。
3.View的measure()方法
/**
* 源码分析:measure()
* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
* 作用:基本测量逻辑的判断
*
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
*
*
*
* measure方法最终还是会调用onMeasure,正真的测量实现是在onMeasure实现,覆写onMeasure方法必须执行setMeasuredDimension()设置View的测量宽高。
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
*
*
*
* @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) {
//布局边界是否可视,开发着模式用,忽略此段代码
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
//避免重复测量,尝试读取缓存,key值有宽高共同决定
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//首次测量创建缓存
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
//上面的标志位都是为了确定这个view是否需要重新测量
if (forceLayout || needsLayout) {//需要重新测量
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
//读取缓存
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//如果缓存没有命中,则调用onMeasure重新测量
// 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
//这里检测测量标记是否置位,如果没有置位,则表示setMeasuredDimension没有调用,抛异常,所以在自定义View的OnMeasure方法里必须调用setMeasuredDimension方法
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
}
measure的流程:
1>判断是否需要重新测量,如果需要,则走2>,否则走3>;
2>读取缓存:如果缓存命中,则读取缓存值,解析出宽高spec信息作为本次测量结果,然后通过setMeasuredDimensionRaw()设置测量mMeasuredWidth和mMeasuredHeight;如果未命中,则执行onMeasure方法,在onMeasure方法里面需要执行setMeasuredDimension()方法设置测量宽高;
3>保存本次测量结果并存入缓存
4>measure执行的最终测量大小存放在mMeasuredWidth和mMeasuredHeight中。
4.View的onMeasure()方法
/**
* 分析:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :获得View宽/高的测量值
// 传入的参数通过getDefaultSize()获得
}
/**
* 分析:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍
/**
* 分析:getDefaultSize()
* 作用:根据View宽/高的测量规格计算View的宽/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 参数说明:
// size:提供的默认大小
// measureSpec:宽/高的测量规格(含模式 & 测量大小)
// 设置默认大小
int result = size;
// 获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的宽/高值
return result;
}
getDefaultSize中的size 参数为getSuggestedMinimumHeight/Width()方法得到:
protected int getSuggestedMinimumHeight() {
//如果设置背景,则是背景高和mMinHeight的较大值
//没设置背景则是mMinHeight
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
(二)ViewGroup的measure流程
1>(API26)ViewGroup没有覆写measure和onMeasure方法,所以默认情况下measure流程和View的一致
2> ViewGroup不仅要测量自己,还要测量子View,ViewGroup测量子View的方法为measureChildren.
3>和自定义View一样,自定义ViewGroup也需要覆写onMeasure方法,根据子View的测量结果,按照自己的逻辑合并子View的宽高,确定自身的宽高。
/**测量子View
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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) {
//忽略GONE掉的View,INVISIBLE的View仍然测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**测量单个Child
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//根据parentMeasureSpec和子ViewLP生成子View的MeasureSpec,具体代码已经在MeasureSpec生成中分析过
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//子View各自测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
至此,ViewGroup的measure流程分析完毕,分析完原理,我们最终的目的还是为了学以致用,下面介绍View和ViewGroup的onMeasure的基本套路,这里只介绍流程,具体的实践后面的博客中会根据案例具体实现。
(三)覆写onMeasure方法的基本流程
1.View的onMeasure()方法基本流程:
1>拿到父View传过来的spec,解析出size;
2>对LP=wrap_content的情况,设置默大小;
3>根据自己的逻辑,如等比例宽高、宽高最值限定等等逻辑,结合size,得到最终的resultsize;
4>执行setMeasuredDimension(resultsize)设置测量结果;
//onMeasure伪代码
private int mDefaultWidth = 200;
private int mDefaultHeight = 400;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Step1:拿到父View期望的大小
int resultWidth = 0;
int resultHeight = 0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//先赋值
resultWidth = widthSize;
resultHeight = heightSize;
//Step2:wrap_content处理
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//在这里实现计算需要wrap_content时需要的宽
resultWidth = mDefaultWidth;
}
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
//在这里实现计算需要wrap_content时需要的高
resultHeight = mDefaultHeight;
}
//step3:自己定义View的逻辑,如宽高比,大小限制等等
resultHeight = resultWidth;
//step4:设置测量结果
setMeasuredDimension(resultWidth, resultHeight);
}
上面的代码简单实现了宽高比为1的自定义View,除了第三步,其他三步为固定套路,可以直接用。
2.覆写View的onMeasure()方法基本流程:
1>遍历所有子View,存放它们的大小;
2>根据自己的逻辑,合并子View的大小,得到最终ViewGroup的大小;
3>:设置大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 定义存放测量后的View宽/高的变量
int widthMeasure ;
int heightMeasure ;
// Step1. 遍历所有子View(child.measure) or 测量measureChildren()
measureChildren(widthMeasureSpec, heightMeasureSpec);
// Step2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
... // 核心部分自身实现
// Step3. 存储测量后View宽/高的值:调用setMeasuredDimension()
// 类似单一View的过程,此处不作过多描述
setMeasuredDimension(widthMeasure, heightMeasure);
}
总结:希望读者读完measure源码,对View/ViewGroup的测量原理有更清晰的认识,有关于measure方法的应用场景,一般在自定义ViewGroup中结合layout使用,关于View的Layout原理,在下一篇博客中会详细研究。