Android关于View的MeasureSpec详解
2018-01-04 00:00
417 查看
前言
在开发过程中,可能会遇到需要获取View的宽高的问题。但是View在
onMeasure之前,通过
getHeight(),
getWidth()获取到的宽高为0。
其中一种解决方法如下:
int widthMeasureSpec = 0; int heightMeasureSpec = 0; view.measure(widthMeasureSpec, heightMeasureSpec); int height = view.getMeasuredHeight(); int width = view.getMeasuredWidth();
使用这种方式获得的宽高和最终onMeasure->onLayout->onDraw后通过
getHeight(),
getWidth()获取到的宽高不一致。
下边介绍一下为什么会出现这个问题。
问题
为什么view.measure(0, 0);中的参数是0?
View.measure方法的文档如下:
/** * <p> * 为了找到这个view应该多大,可以调用这个方法。父视图提供宽高参数的约束信息。 * </p> * * <p> * view实际的测量工作在{@link #onMeasure(int, int)}中,{@link #onMeasure(int, int)}由当前方法调用。 * 因此只有{@link #onMeasure(int, int)}可以且必须被子类重写。 * </p> * * * @param widthMeasureSpec 由父视图施加的水平方向的要求。 * @param heightMeasureSpec 由父视图施加的垂直方向的要求。 * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { .... }
所以,在调用
measure方法时,传入的参数不应该是
0, 而应该是一个约束规则MeasureSpec.
这时使用
measure方法获取宽高和View在绘制后通过
getHeight(),
getWidth()获取宽高产生不一致的根本原因。
Int型的MeasureSpec能取那些值呢?
我们首先需要产看一下MeasureSpec的文档
MeasureSpec的源码如下(API25)
/** * Use the old (broken) way of building MeasureSpecs. * 使用旧的(带破坏性的)方式构建MeasureSpecs. */ private static boolean sUseBrokenMakeMeasureSpec = false; /** * 一个MeasureSpec封装了从父视图到子子视图传递布局的要求。 * 每个MeasureSpec代表宽度或高度的要求。 * MeasureSpec由一个大小和一个模式组成。有三种可能的模式: * <dl> * <dt>UNSPECIFIED</dt> * <dd> * 父视图没有对子视图施加任何限制。子视图可以是它想要的任何大小。 * </dd> * * <dt>EXACTLY</dt> * <dd> * 父视图已经确定了子视图的确切大小。无论子视图想要多大,子视图将要变成被给予的边界大小。 * </dd> * * <dt>AT_MOST</dt> * <dd> * 子视图最大可以达到指定的大小。 * </dd> * </dl> * * 为了减少对象的分配(注:对象创建),MeasureSpecs 用int作为实现。 * 这个类提供一个打包和解包size和mode元组变为int的功能。 */ public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; /** * 根据Measure模式和大小生成一个MeasureSpec * 3ff8 * <p><strong>Note:</strong> 在API 17或更低版本中,makeMeasureSpec's 的实现是这样的,参数的顺序是不重要的, * 任何一个溢出的参数都可能影响MeasureSpec的结果。 * {@link android.widget.RelativeLayout} 受此bug影响. * App的目标API级别大于17将得到固定的,更加严格的行为。</p> */ 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); } } /** * 像 {@link #makeMeasureSpec(int, int)}, 但是任何MeasureSpec的mode是UNSPECIFIED * 它的getSize将自动为0. 旧的app期待这样 * * @hide 仅仅为系统控件和旧版app的兼容性内部使用(隐藏api) */ public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } /** * 从MeasureSpec中获取Measure模式 */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /** * 从MeasureSpec中获取大小 */ 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); } 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应该使用
MeasureSpec.makeMeasureSpec(int size, int mode)方法生成,而不是任意的传入int值。一个MeasureSpec有mode和size组成。
在使用measure
方法时,如何确定MeasureSpec的size和mode来生成一个MeasureSpec呢?
对于手动调用measure方法,获取宽高的情况,一般是想知道view展示它的内容是想要多大的宽高。所以mode应该是
UNSPECIFIED,size应该是0。view想到多大的size由它自己计算。
但是如果父视图的宽是
match_parent,假设运行时是屏幕宽度,这时候view
measure时,mode是
UNSPECIFIED可能就不太合适,mode可能要变为
AT_MOST,size应该是screenWidth。告诉view它的最大宽度是屏幕宽度。
ps: 像TextView这时候宽度有最大值了,这时它可能会换行,计算出一个准确的高度。如果widthMeasureSpec的mode是
UNSPECIFIED,TextView无论文本多长,永远是单行,计算出的高度肯定是单行的高度。
MeasureSpec的实现原理
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static void main(String[] args) { int spec = makeMeasureSpec(20, EXACTLY); int mode = getMode(spec); int size = getSize(spec); System.out.println("MODE_MASK: " + format(MODE_MASK)); System.out.println("UNSPECIFIED: " + format(UNSPECIFIED)); System.out.println("EXACTLY: " + format(EXACTLY)); System.out.println("AT_MOST: " + format(AT_MOST)); System.out.println("spec: " + format(spec)); System.out.println("mode: " + format(mode)); System.out.println("size: " + format(size)); } public static String format(int value) { String s = String.format("%1$32s", Integer.toBinaryString(value)).replace(" ", "0"); StringBuilder builder = new StringBuilder(s); int index = 32 - 4; while (index > 0) { builder.insert(index, ' '); index = index - 4; } return builder.toString(); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } public static int makeMeasureSpec(int size, int mode) { return (size & ~MODE_MASK) | (mode & MODE_MASK); } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }
执行Main函数的输出如下:
MODE_MASK: 1100 0000 0000 0000 0000 0000 0000 0000 UNSPECIFIED: 0000 0000 0000 0000 0000 0000 0000 0000 EXACTLY: 0100 0000 0000 0000 0000 0000 0000 0000 AT_MOST: 1000 0000 0000 0000 0000 0000 0000 0000 spec: 0100 0000 0000 0000 0000 0000 0001 0100 mode: 0100 0000 0000 0000 0000 0000 0000 0000 size: 0000 0000 0000 0000 0000 0000 0001 0100
MODE_MASK的开头两位是11,其余都是0.
UNSPECIFIED是00
EXACTLY是01
AT_MOST是10
mode保存在int的开头两个二进制位中。
size保存在除开头的两个二进制位中
对于一个MeasureSpec,
通过与
MODE_MASK做与(&)运算获取mode
通过与
MODE_MASK先做非(~)运算,再做与(&)运算获取size.
相关文章推荐
- Android自定义View-关于Graphics的解释(一)
- 关于可能会触发android.view.WindowManager$BadTokenException异常的情况
- android关于RecyclerView的使用
- Android开发:关于WebView
- 关于android自定义View时报error: No resource identifier found for attribute ‘XXX’ in package ‘(
- 关于Android中ImageView中tint属性的一点点整理
- Android中关于Volley的使用(四)利用NetworkImageView来加载图片
- 关于android的view触摸事件
- 关于Android Recyclerview隐藏item的所在区域显示大空白问题的解决方案
- 关于android findViewById 返回为空问题
- 关于android WebViewClient的方法解释
- 关于Android中View滑动知识的一点个人理解
- 关于android 中WebView使用Css
- Android关于WebView中无法定位的问题解决
- 关于自己android学习中的RecycleView的问题
- Android 关于WebView里的页面第三方登录
- 关于MVP设计模式 和 BaseRecyclerViewAdapterHelperV2.4.4 Android
- 关于android开发ListView的Adapter的convertView中有EditText的问题
- Android 关于WebView的相关属性
- [Android] 关于WebView的内存泄露问题