自定义控件之onMeasure方法的研究整理

前言

为了满足设计需求,在自定义View时往往不可避免得要对onMeasure方法进行覆写,其中比较常规的写法就是先使用MeasureSpec类提供的getSize和getMode方法获取到尺寸和模式,然后进行相应的处理,最后通过setMeasuredDimension方法完成测量。最近,在编写自定义View时遭遇了一些问题和现象,困扰的同时又激发了我,我发现自学习自定义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属性,运行程序查看结果。

LinearLayoutmyView
layout_widthmatch_parent40dp
layout_heightmatch_parent40dp

测试结果1

输出结果:尺寸对应myView的layoutParams(单位px),模式为EXACTLY.


LinearLayoutmyView
layout_widthmatch_parentmatch_parent
layout_heightmatch_parentmatch_parent

测试结果2

输出结果:尺寸为LinearLayout的剩余空间(因为是NoActionBar主题,且只有一个子控件,所以剩余高度等于屏幕高度-状态栏高度,剩余宽度等于屏幕宽度),模式为EXACTLY.


LinearLayoutmyView
layout_widthwrap_contentwrap_content
layout_heightwrap_contentwrap_content

测试结果3

输出结果:尺寸为LinearLayout的剩余空间,模式为AT_MOST.


通过控制变量法,其实已经可以得出一个简单的结论:onMeasure方法中的两个参数是由被测View自身的LayoutParams和它的父容器共同决定的。如果继续修改属性,最终可以得出下表:

EXACTLYAT_MOSTUNSPECIFIED
dp/pxEXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parentEXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_contentAT_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,观察不同放置顺序下的情况。
测试结果4
测试结果5
图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,即下表状态时,输出结果与之前存在差异。

LinearLayoutmyView
layout_widthwrap_contentmatch_parent
layout_heightwrap_contentmatch_parent

疑难杂症

相比于之前的结果,这次结果中onMeasure方法调用了四次,并且只有第1、3次结果符合规律,第2、4次结果不符合。对此,我觉得很奇怪,也没有什么头绪,如果有好兄弟能帮忙解答一下就太感谢了。

总结

  1. onMeasure方法中的两个参数由被测量的View自身LayoutParams和它父容器的测量模式共同决定。
  2. 如果在onMeasure方法中不处理wrap_content的情况,那么使用时与match_parent效果一致。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值