之前在看Android开发艺术探索的时候也有写过一篇AndroidView的measure过程的文章,现在回头看看把自己看的都一头雾水,妥妥的水文,抽空还要再去把书读两遍才行啊。
一、目标
明确MeasureSpec三种测量模式的具体含义,并根据实际需求测量View的大小
二、明确MeasureSpec三种测量模式的含义
- EXACTLY :父控件已经确定了子控件的大小
- AT_MOST:父控件对子控件没有约束,但存在上限,上限一般是父控件的大小
- UNSPECIFIED:父控件对子控件没有任何约束。它可以是任意大小。
为了验证各个模式的含义,我们写个一测试的TestView,只显示一段文字,并打印View的测量模式。
public class TestView extends View {
private Paint mPaint;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(50);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("测试", 0, 50, mPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获得宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获得测量的宽高
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
String strWidthMode = "";
String strHeightMode = "";
//宽度的测量模式
switch (widthMode) {
case MeasureSpec.AT_MOST:
strWidthMode = "AT_MOST";
break;
case MeasureSpec.EXACTLY:
strWidthMode = "EXACTLY";
break;
case MeasureSpec.UNSPECIFIED:
strWidthMode = "UNSPECIFIED";
break;
}
//高度的测量模式
switch (heightMode) {
case MeasureSpec.AT_MOST:
strHeightMode = "AT_MOST";
break;
case MeasureSpec.EXACTLY:
strHeightMode = "EXACTLY";
break;
case MeasureSpec.UNSPECIFIED:
strHeightMode = "UNSPECIFIED";
break;
}
//打印测量模式
Log.e("tag", "=== WidthMode: " + strWidthMode + " HeightMode: " + strHeightMode
+ " widthSize: " + widthSize + " heightSize: " + heightSize);
}
}
XML文件布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical"
>
<com.zy.test.cusview.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0a0aea"
/>
</LinearLayout>
1、注意此时父布局宽高是match_parent
,TestView是自适应。并且给TestView设置了一个蓝色
的背景
运行结果如下:
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
运行截图截图可以看出:
从运行结果可以看出,当TestView设置自适应的时候,它的测量模式是AT_MOST
。并且此时我们的TestView充满全屏显示。
2、修改父布局的宽高为 wrap_content。
运行结果如下:
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
当我们TestView和父布局都自适应时,TestView也是全屏显示,并且测量模式为: AT_MOST
3、给TestView设置确定的大小,父控件自适应
运行结果如下:
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 200 heightSize: 200
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 200 heightSize: 200
截图:
当我们给TestView的宽高一个确定的大小时,TestView的测量模式为EXACTLY
,此时宽高固定。
4、当我们TestView宽高为match_parent时
运行结果如下:
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 800 heightSize: 1199
E/tag: === WidthMode: AT_MOST HeightMode: AT_MOST widthSize: 800 heightSize: 1199
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 800 heightSize: 1199
从运行结果上可以看到,虽然View测量了多次,但最终它的测量模式为EXACTLY
,并且TestView全屏显示。
同理当父控件宽高为match_parent时,依然是EXACTLY 模式, 运行结果如下:
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 800 heightSize: 1199
E/tag: === WidthMode: EXACTLY HeightMode: EXACTLY widthSize: 800 heightSize: 1199
对比两次结果我们可以知道当父布局宽高设置为wrap_content自适应时,会进行多次测量。而 宽高设置为match_parent充满全屏,因为屏幕是固定的,所以宽高也是固定的。不会进行多次测量。
ScrollView嵌套TestView时
执行结果如下:
E/tag: === WidthMode: EXACTLY HeightMode: UNSPECIFIED widthSize: 200 heightSize: 0
E/tag: === WidthMode: EXACTLY HeightMode: UNSPECIFIED widthSize: 200 heightSize: 0
可以看到此时TestView高德的测量模式为UNSPECIFIED
,并且测量高度heightSize为0。
总结上面的测试我们可以得到下面结论
三、明确了MeasureSpec的三种测量模式后,我们要如何修改,才能达到向TextView一样,显示一段文字呢?
我们可以看一下TextView的onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获得宽高的测量模式和测量大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
.....//省略部分代码
if (widthMode == MeasureSpec.EXACTLY) {
// 如果宽度的测量模式是EXACTLY,宽度就是测量的宽度
width = widthSize;
} else {
......//省略部分代码
width += getCompoundPaddingLeft() + getCompoundPaddingRight();
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec.AT_MOST) {
//如果宽度的测量模式是AT_MOST实际值
width = Math.min(widthSize, width);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
// 如果高度的测量模式是EXACTLY,已经确定大小,则高度为测量的高度
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if (heightMode == MeasureSpec.AT_MOST) {
//如果宽度的测量模式为AT_MOST,取实际高度
height = Math.min(desired, heightSize);
}
}
......//省略部分代码
//设置宽高
setMeasuredDimension(width, height);
}
TextView的功能比较复杂,所以源码也比较复杂。这里只展示了相关的代码。可以看到,TextView也是根据View的测量模式来进行View的宽高进行测量的。
接下来,测量一下我们的TestView的宽高。
根据TextView,修改我们的TestView测量方法
public class TestView extends View {
private Paint mPaint;
private String mText = "天青色等烟雨,而我在等你";
private int mHeight = 50;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(50);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mText, 0, mHeight, mPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获得宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获得测量的宽高
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else if (widthMode == MeasureSpec.AT_MOST){
width = (int)Math.ceil(mPaint.measureText(mText));
}else {
width = (int)Math.ceil(mPaint.measureText(mText));
}
if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else if (heightMode == MeasureSpec.AT_MOST){
height = mHeight;
}else {
height = mHeight;
}
//设置宽高
setMeasuredDimension(width,height);
}
}
测试:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical"
>
<com.zy.test.cusview.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0a0aea"
/>
</LinearLayout>
修改之前运行结果:
到现在总算知道了该如何测量View的大小。感觉表达的方式还有些问题。可能要等过段时间再回过头重新修改了