前言
为了满足设计需求,在自定义View时往往不可避免得要对onMeasure方法进行覆写,其中比较常规的写法就是使用MeasureSpec类的getSize和getMode方法获取到尺寸和模式,再进行相应的处理,最后通过setMeasuredDimension方法完成测量。最近,在编写自定义View时遇到了一些问题和现象,困扰的同时又激发了我,我发现一直以来循规蹈矩,却从未思考过一个问题:onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中的两个参数是如何得到/计算出来的?只知其然而不知其所以然,我决定研究一下这个问题。
栗子
关于这个问题,其实只需要稍微看一下源码就能立刻获得答案。然而,我觉得透过现象看本质才是最好的学习方法,并且相比于枯燥的源码,更便于记忆和加深印象(其实在此之前,我已经看过多次源码,甚至每次都觉得就这,就像考前自信满满的学渣一样,整本书我都复习过了,结果一看试卷,大脑瞬间空白)。
Example I
扯远了,先整一个自定义View吧,让它在onMeasure方法中输出获取到的尺寸和模式。
public class MyView extends View {
//省略部分代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
Log.i(TAG,"widthSize --> " + widthSize);
Log.i(TAG,"heightSize --> " + heightSize);
Log.i(TAG,"widthMode --> " + widthMode);
Log.i(TAG,"heightMode --> " + heightMode);
Log.i(TAG,"--- onMeasure finished ---");
}
}
将它添加到容器中,此处使用Linearlayout,然后通过调整Linearlayout和myView的layout_width和layout_height属性,运行程序查看结果。
LinearLayout | myView | |
---|---|---|
layout_width | match_parent | 40dp |
layout_height | match_parent | 40dp |
输出结果:尺寸对应myView的layoutParams(单位px),模式为EXACTLY. ——————————————————————————————————————
LinearLayout | myView | |
---|---|---|
layout_width | match_parent | match_parent |
layout_height | match_parent | match_parent |
输出结果:尺寸为LinearLayout的剩余空间(因为是NoActionBar主题,且只有一个子控件,所以剩余高度等于屏幕高度-状态栏高度,剩余宽度等于屏幕宽度),模式为EXACTLY. ——————————————————————————————————————
LinearLayout | myView | |
---|---|---|
layout_width | wrap_content | wrap_content |
layout_height | wrap_content | wrap_content |
输出结果:尺寸为LinearLayout的剩余空间,模式为AT_MOST. ————————————————————————————————————————— 通过控制变量法,其实已经可以得出一个简单的结论:onMeasure方法中的两个参数是由被测View自身的LayoutParams和它的父容器共同决定的。如果继续修改属性,最终可以得出下表:
EXACTLY | AT_MOST | UNSPECIFIED | |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
此表摘自《Android开发艺术探索》,横轴为父容器的测量模式,纵轴为子View的layoutParams,内容即为子View的MeasureSpec,parentSize是父容器的剩余大小。经测试,除了AT_MOST/match_parent的对应关系可能存在问题,其它都准确无误,关于这个问题我也十分疑惑,之后会提到。
Example II
在这个结论的基础上,让我们再来看一个例子:
让MyView绘制一个黄色的圆。
@Override
protected void onDraw(Canvas canvas) {
//注:由于onDraw会多次调用,因此不要在此初始化画笔,此处仅为方便展示
Paint paint = new Paint();
paint.setColor(Color.parseColor("#FFF000"));
//绘制一个圆
canvas.drawCircle(200, 200, 200, paint);
}
然后把它和一个TextView一起添加到Linearlayout中,并且将layout_width和layout_height属性都设置为wrap_content,观察不同放置顺序下的情况。
图1中,特意选中了MyView,可以看到它占据了父容器的剩余空间,符合上表。
图2中,特意选中了TextView,它并没有消失,而是被MyView挤出了屏幕,MyView仍占据父容器的剩余空间(先于TextView测量),符合上表。
从总结表或此例中都不难发现,无论父容器是什么模式(UNSPECIFIED除外),只要子View的layout_width或layout_height属性任意为wrap_content,那么对应的模式一定是AT_MOST,尺寸一定是父容器的剩余大小。那为什么图中的TextView不符合这个规律?这当然是因为TextView重写了onMeasure方法,处理了属性为wrap_content时的情况。
所以可以得到第二个结论:在特定需求下,自定义控件需要处理wrap_content时的宽高,否则使用wrap_content和使用match_parent的效果一致,都将占满父容器的剩余空间。
疑难杂症
之前谈到的那个问题,当父容器的模式为AT_MOST,子View的属性为match_parent,即下表状态时,输出结果与之前存在差异。 | | LinearLayout | myView | | ---- | :----: | :----: | | layout_width | wrap_content | match_parent | | layout_height | wrap_content | match_parent |
相比于之前的结果,这次结果中onMeasure方法调用了四次,并且只有第1、3次结果符合规律,第2、4次结果不符合。对此,我觉得很奇怪,也没有什么头绪,如果有好兄弟能帮助解答一下就太感谢了。
总结
- onMeasure方法中的两个参数由被测量的View自身LayoutParams和它父容器的测量模式共同决定。
- 如果在onMeasure方法中不处理wrap_content的情况,那么使用时与match_parent效果一致。