Android 自定义view的测量
2018-01-29 11:15
204 查看
Android 自定义view的测量
首先在自定义view的时候,你需要告诉系统该画一个多大的View。这个过程在onMeasure()方法里进行的。Android系统给我们提供了一个设计短小精悍却功能强大的类——MeasureSpec类。通过它可以帮助我们测量view。
MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。
如下是MeasureSpec源码:
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; /** * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: * <ul> * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li> * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li> * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> * </ul> * * <p><strong>Note:</strong> 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.</p> * * @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); } /** * 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); } /** * 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(); } }
在MeasureSpec类中有三种模式:
EXACTLY
即精准模式,当我们将控件的layout_width属性或者layout_height属性指定为具体数值时,比如layout_width=”200dp”,或者指定为match_parent属性时(占据父类View的大小),系统使用的是EXACTLY。
AT_MOST
最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件的内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大值即可。
UNSPECIFIED
这个属性它不指定其大小测量模式,View想多大就多大,通常情况下绘制自定义view时才使用。
下面我们在看下View中的onMeasure()方法是怎么写的:
/** * <p> * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. * </p> * * <p> * <strong>CONTRACT:</strong> When overriding this method, you * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the * measured width and height of this view. Failure to do so will trigger an * <code>IllegalStateException</code>, thrown by * {@link #measure(int, int)}. Calling the superclass' * {@link #onMeasure(int, int)} is a valid use. * </p> * * <p> * The base class implementation of measure defaults to the background size, * unless a larger size is allowed by the MeasureSpec. Subclasses should * override {@link #onMeasure(int, int)} to provide better measurements of * their content. * </p> * * <p> * If this method is overridden, it is the subclass's responsibility to make * sure the measured height and width are at least the view's minimum height * and width ({@link #getSuggestedMinimumHeight()} and * {@link #getSuggestedMinimumWidth()}). * </p> * * @param widthMeasureSpec horizontal space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. * @param heightMeasureSpec vertical space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. * * @see #getMeasuredWidth() * @see #getMeasuredHeight() * @see #setMeasuredDimension(int, int) * @see #getSuggestedMinimumHeight() * @see #getSuggestedMinimumWidth() * @see android.view.View.MeasureSpec#getMode(int) * @see android.view.View.MeasureSpec#getSize(int) */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
有上面的代码看出来在View中onMeasure中最后执行的是setMeasureDimension()方法,参数是getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
我们在来看下getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
/** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ public static int getDefaultSize(int ba4b 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; }
由上面的源代码可以看出来只处理 MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY这俩种测量方式。
所以我们在自定义view时,如果想让自定义View支持warp_content属性,那么就必须要重写onMeasure()方法来指定warp_content时的大小。
接下来我们可以自己自定义View 如下
首先重写onMeasure()方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); }
其中setMeasuredDimension() 是view在onMeasure()方法中最后要调用的。这里我们用measureWidth(widthMeasureSpec)和measureHeight(heightMeasureSpec)重新定义测量规则。
下面我们对measureWidth(widthMeasureSpec)举例说明如何自定义测量值的。
第一步从MeasureSpec对象中提取具体的测量模式和大小,代码如下:
int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);
第二步通过判断测量的模式,给出不同的测量值如下:
private int measureWidth(int widthMeasureSpec){ int result=0; int specMode=MeasureSpec.getMode(widthMeasureSpec); int specSize=MeasureSpec.getSize(widthMeasureSpec); if(specMode==MeasureSpec.EXACTLY){ result=widthMeasureSpec; }else{ result=500; if(specMode==MeasureSpec.AT_MOST){ result=Math.min(result,specSize); } } return result; }
我在这里说明一下,当specMode为EXACTLy时,直接指定specSize即可,当specMode为其他的模式时,需要给一个默认的大小。如果指定wrap_content属性,即AT_MOST模式,需要取出我们指定的大小与specSize 其中最小的那个值,作为测量的最后测量值.
ViewGroup
在最后简单说下ViewGroup的测量,ViewGroup回去管理其子View,其中一个管理项目就是负责子View的显示大小。当ViewGroup的大小为wrap_content,ViewGroup就需要对子view进行遍历,以便获的所有子view的大小,从而决定自己的大小。而在其他模式下则会通过具体的指定值来设置自身的大小。
当子view测量完毕后,就需要将子view放到合适的位置上,这个过程就是View的Layout过程。ViewGroup在执行Layout过程时,同样是遍历来调用子view的layout的方法,并指定其具体显示的位置,从而来决定其布局位置。
自定义ViewGroup时,通常会重写onLayout方法来控制子View显示位置的逻辑。同样,如果需要支持wrap_content属性,那么它还必须重写onMeasure()方法,这点与View是一样的。
相关文章推荐
- Android自定义View 之 View的测量
- Android 关于自定义TextView的测量
- android绘制自定义view时,控件大小的测量与确定问题
- Android之自定义View,你需要了解和掌握的onMeasure测量规则
- Android自定义View(一) View的测量
- Android自定义View之绘制、测量
- 【Android自定义View】测量和绘制浅析
- (转)Android自定义View(三、深入解析控件测量onMeasure)
- Android 自定义View 之测量过程(onMeasure)
- android自定义view之测量父view和迭代测量子view
- (转)Android自定义View 之 View的测量
- Android 自定义View基础-View的测量
- android绘制自定义view时,控件大小的测量与确定问题,代码演示
- android 自定义View在弹出的软键盘之上,以及测量软键盘高度和监听软键盘状态
- Android自定义View(三、深入解析控件测量onMeasure)
- Android填坑之旅(第十四篇)关于软键盘弹出未及时隐藏导致自定义View的onMeasure方法测量错误引发的血案
- [置顶] Android自定义View(三、深入解析控件测量onMeasure)
- Android自定义View(三、深入解析控件测量onMeasure)
- android 自定义View之View的测量(onMeasure()方法)