Android 自定义控件源码分析----谈Android自定义控件中 onMeasure()方法处理 wrap_content 情况的必要性
2016-01-19 22:12
447 查看
转载请注明本文出自 clevergump 的博客:http://blog.csdn.net/clevergump/article/details/50545257, 谢谢!
假如我们有如下需求: 要紧贴着手机屏幕的左上角放置一个圆, 要求该圆的半径为50px, 在圆的右边要放置一个Button (尺寸随意). 由于今天我们讨论的是自定义控件的话题, 所以我们就以自定义控件的方式来绘制这个圆. 那么, 既然是要以自定义控件的方式来实现, 那么自定义控件的一些初学者肯定会说, 这太简单了, 直接自定义一个继承自 View 类的圆, 在其 onDraw() 方法中设置圆心的 x, y 坐标都等于半径50px, 并以 50px 为半径画圆即可实现该自定义的圆. 而布局文件可以使用水平方向的 LinearLayout, 先将该圆添加进去, 由于我们已经在 onDraw() 方法中设置了圆心坐标和半径都为 50px 这个绝对数值, 这样我们就可以直接计算出圆的宽和高都为 100px 了, 那么在布局文件中我们就可以直接设置该圆的 layout_width 和 layout_height 都为 100px, 但是仔细一想, 万一今后需求变了, 改为要让该圆的半径可以由用户自由设定, 那我们岂不是又要改这里的 layout_width 和 layout_height ? 所以这里索性就设置为 wrap_content 吧, 包裹内容, 既满足需求又能满足扩展要求. 然后再添加一个 Button 到该布局文件中, OK 搞定了. 于是就可能有了如下的代码:
上面是自定义圆的代码.
上面是布局文件.
Activity非常简单, 仅仅设置了该 Activity 对应的布局文件.
我们直接预览布局文件, 或者运行上述代码, 我们会发现, 屏幕中竟然只显示了圆, 而没有显示Button, 如下图所示:
太奇怪了, 不可能吧! 是不是哪里手误写错了? 于是又仔细检查了代码, 发现都没问题, 那这是怎么回事呢? 有些同学此刻可能已经陷入了疑惑和不解中, 但为了实现需求, 就直接把 wrap_content 改为了 100px, 而有些有钻研精神的同学, 可能暂时不会立即将 wrap_content 改为 100px, 而是考虑先试着来个 debug, 看能不能解决这个问题, 如果实在不能解决, 再改为 100px 也不迟啊. 那么我们就来试着调试一下这个 bug 吧.
为了调试该bug, 我们可以为自定义的圆添加一个黄色的背景, 为 Button 添加一个淡蓝色背景, 这样将各个子控件的背景颜色和布局文件中的父容器 LinearLayout 的白色背景加以区分, 便于我们诊断故障. 我们在布局文件中为我们的自定义圆添加如下代码:
为 Button 添加如下代码:
再来看下布局预览或运行APP后的效果:
我们发现, 整个屏幕的背景都变为了黄色并且没有看到淡蓝色背景的Button. 这说明, 整个屏幕都被我们自定义的圆所占据了, 奇怪, 我们为自定义的圆设定的宽高明明都是 wrap_content, 为何实际上却占满了整个屏幕呢? 另外, 我们看不到 Button, 难道是因为我们自定义的圆将 Button 挤出了屏幕? 还是因为圆已经占满了屏幕, 使得 Button 压根儿就没有绘制呢?
上述两个问题, 我们先解决第二个. 即: 我们看不到 Button, 到底是因为 Button 被挤出了屏幕, 还是压根儿没被绘制.
我们分析一下, 即使是 Button 被挤出了屏幕, 那么也是需要对他进行绘制的. 只要他完成了绘制, 那么我们也依然能够获取到他的宽高以及四条边的坐标. 而如果Android系统压根儿就没有绘制这个 Button , 那么我们一定获取不到他的宽高, 也获取不到他的四条边的坐标. 这就是上述两个可能原因之间的区别. 我们可以从这个区别点入手, 就能分析 Button 消失的具体原因了.
为了计算 Button 的宽高以及四条边的坐标位置, 我们需要修改先前 MainActivity 的代码, 改为如下内容:
这里顺便提一下, 上述代码中, 要想获取控件的宽高或四条边的位置, 我们不能在 Activity 的生命周期方法中去执行 View.getWidth(), View.getHeight(), View.getLeft(), View.getRight() 等方法, 因为 Activity 的加载和 View 的绘制是异步进行的. 所以我们必须要在确保 Button 确实完成了绘制后, 或者至少要完成了 layout 后, 我们再去获取其宽高和四条边的位置. 因此我们可以按照上述代码中 setListener() 方法那样, 在 onGlobalLayout() 回调方法中来执行上述操作. 代码中相关注释已经很详细了, 就不再详细介绍这个小细节了.
上述代码运行后, 我们看看打印的log:
看来, Button 成功回调了 onGlobalLayout() 方法, 说明系统确实是绘制了这个 Button. 并且其左右边框的位置都是 1080px, 而我们的代码刚好就是运行在分辨率为 1920*1080px 的手机上, 手机屏幕的宽度也是1080px, 这和 Button 的左右边框的位置值刚好相同. 所以, 看来 Button 还是绘制了, 只是宽度被挤成了0, 其左右边界都刚好与屏幕的右边界重合. 所以这种情况即可算作在屏幕内, 也可算作在屏幕外. 而此刻, 如果我们为 Button 添加一个正数的 leftMargin 值, 看看此时测量的 Button 的左右边界的坐标值是否会大于屏幕右边界的坐标呢? 我们在布局文件中为 Button 添加如下代码:
再次运行, 查看打印的log:
我们看到, Button 的左右边框都变为了 1170px, 这已经超出了手机屏幕右边框的 1080px了, 表明 Button 此时是在手机屏幕外. 而由于我们手机的分辨率 1920*1080px 属于 xxhdpi, 所以对于该手机来说, 1dp == 3px, 那么我们为 Button 添加的 30dp 的 marginLeft, 其实等于90px, 而 1170px - 90px = 1080px, 刚好等于手机屏幕右边框的 x 坐标, 而我们自定义的圆没有设置 marginLeft 和 marginRight, 所以这说明 Button 此时确实是在屏幕外. 其实我们也可以使用同样的方式, 来分别测量我们自定义圆以及布局文件中父容器 LinearLayout 二者的宽高和四条边的位置, 由于代码和测量 Button 的代码类似, 所以这里就不贴出了, 我们直接看结果:
从上图的结果中可以看出, 我们自定义圆的宽高和父容器 LinearLayout 的相等, 并且二者的左边框和上边框分别重合, 所以, 我们可以得出结论, 我们自定义的圆确实完全填充了他的父容器. (读者朋友此处不要纠结于高度 1701px 小于手机分辨率 1920*1080px 中的 1920px, 因为我也把 LinearLayout 他的父容器, 即: id为 android.R.id.content 的 FrameLayout 的尺寸和位置也打印出来了, 发现和其他二者是完全相同的, 所以我们就不要去纠结于高度不等于1920px了, 只要知道自定义的圆填满了整个 LinearLayout 这个结论就行了).
既然得出了这个结论, 那么我们就要分析一下, 为什么我们自定义的这个圆明明设置的宽高都是wrap_content, 但最后呈现出的效果却是填满整个父容器, 也就是 match_parent 的效果? 如果将该自定义圆的宽高改为 match_parent, 或者具体的尺寸数值, 又将会是什么样的效果呢?
于是, 我们首先验证将自定义圆的宽高都修改为 match_parent, 发现仍然是填充满整个父容器的效果, 这种反倒是正确的现象, 如果不填充满父容器反而不符合 match_parent 的含义了. (这里我就不贴出效果图了).
而将自定义圆的宽高改为具体的数值, 我们发现, 情况有所不同了. 例如: 我们将宽和高都改为 200dp, 直接预览布局文件或者运行代码, 效果图如下:
我们能看到紫色背景的 Button 了, 并且自定义圆的大小也不再是填充满整个屏幕了.
而如果我们只将宽度改为 200dp, 高度仍保持为 wrap_content, 效果图如下:
我们发现, 还是能看到 Button, 并且自定义圆的宽度不是填充满父容器的宽度, 但他的高度却仍然是填充满父容器的高度.
我们再将高度改为 200dp, 将宽度再改回到 wrap_content, 效果图如下:
我们发现, 这种情况下, 自定义圆的宽将填充满父容器的宽导致 Button 看不见了, 而自定义圆的高度则不会充满父容器的高.
通过上述三个测试, 我们能够发现一个可能的规律: 对于自定义圆来说, 其宽度和高度, 我们设置二者中的哪个尺寸为 wrap_content, 那么那个尺寸实际上呈现出的效果将不是包裹内容, 而是充满他所在父容器的那个尺寸, 也就是在那个尺寸上将呈现出 match_parent 的效果. 而设置为具体的dp/px数值 (只要尺寸数值不超过父容器的相应尺寸), 则不会出现这种异常现象.
但是, 这只是该 bug 的现象, 那么该 bug 的真正原因到底是什么呢? 这个问题我们将在后边给出答案. 这里先暂时做个标记.———- 标记0
要想知道这个问题的答案, 我们就必须要先了解一下 View 系统的绘制原理或流程. Android 中整个 View 系统可以用下图来表示:
另外, 为了便于接下来的一小段理论分析, 我再把我们前面自定义圆的那个 demo 工程使用 Hierarchy Viewer工具生成的 View 树也贴出来, 如下图所示:
我们的一个 Activity 内一般都是包含着一个Window (翻译为”窗户”, 对于手机来说, 就是 PhoneWindow这个子类), PhoneWindow 内有个名叫 DecorView 的 FrameLayout (见上图中的①). decor 的意思是”装饰物”, 所以这个 DecorView 就是用来装饰手机屏幕这扇窗户的, DecorView 本身是一个 FrameLayout, 所以他也是一种存放控件的容器, 该容器内部又是由一个竖直方向的 LinearLayout (见上图中的②) 容器来进行统一管理. 该 LinearLayout 内部分为上下两部分, 上边用来放置 ActionBar (见上图中的③), 但这个 ActionBar 被系统设置为 ViewStub 的惰性加载形式, 也就是默认情况下是不会被加载的, 可以认为默认情况下其体积为0, 不占据任何空间. 下边是一个id为 android.R.id.content 的 FrameLayout (见上图中的④), 而④这个 FrameLayout 容器, 才是系统开放给我们应用开发者的, 至于他内部摆放什么控件以及如何摆放这些控件, 则完全由我们开发者自己来决定. 我们通常把④这个 FrameLayout 容器内部摆放的所有控件的整体称作这个容器的 content view, 也就是他内部的 View. 这个 content view 可以有多种提供方式. 我们既可以先将我们要提供的控件写在 xml 布局文件中, 然后借助于 Activity 来为④设置 content view; 也可以直接在 Activity 中通过 java 代码创建控件对象, 并通过一系列布局方法设置这些控件属性的方式来提供; 还可以同时结合上述两种方式共同提供. 在我们自定义圆的 demo 项目中, 我们是使用第一种方式, 也就是 xml 布局文件的方式来提供容器④的content view, 布局文件中最外层的 LinearLayout 其实就是上图中的 ⑤, 我们自定义的圆就是上图中的⑥, Button 就是上图中的⑦.
其实上图就是一棵View树, 只是这棵树倒在地上了而已. 我们按照数据结构中对树的定义, 来重新绘制这棵树, 如下图所示:
介绍完 Android 控件的树形系统之后, 再来简要介绍下 Android 控件这棵树的测量机制.
Android 控件树 (也可以叫做 View 树) 的测量流程必定是从 DecorView 开始的. 任何一个控件的测量都是走的 measure(int widthMeasureSpec, int heightMeasureSpec) 这个方法. 该方法有两个参数 widthMeasureSpec 和 heightMeasureSpec, 这两个参数分别表示该控件的父容器对该控件在宽/高方面的限制条件, 都是32位的 int 型数. 他们的高2位都表示 specMode (即: 父容器对该控件在测量模式方面的限制, 有3个可取的值: UNSPECIFIED, EXACTLY, AT_MOST), 低30位表示specSize (即: 父容器对该控件在测量尺寸方面的限制).
此时的我们不禁要问, 系统为 DecorView 的 measure()方法中那两个参数到底传的是什么数值呢?
其实, View 系统的测量过程是在 ViewRoot 类中的 performTraversals() 方法中进行的 (我这里分析的是 Android 2.2.2 版本的源码, 其他版本的源码可能略有不同), 看下其源码:
第14行, 就是根视图的测量过程. 而从第3行可知, host 其实就是 mView. 那么 mView 到底是什么呢? 我们在 ViewRoot 类的源码中找找对该变量的赋值语句吧. 我们找到了很多个包含对 mView 的赋值语句的方法. 但是由于我们这里分析的是 View 的测量过程, 所以类似于 dispatchDetachedFromWindow() 这样的方法从名称来看就与我们正在分析的情景不符, 所以直接被我们忽略掉, 最后, 我们发现 setView()方法相对来说是最符合我们的情景的, 于是我们看下这个方法的源码:
这个方法内部, 从整体上来看就是个同步代码块, 所以在同一个时刻, 该方法只可能被一个线程执行. 而在同步代码块的内部, 相当于是一个单例的操作, 即: 只能为 mView 赋一次值, 一旦进行过第一次的赋值后, 下一次再去执行该方法时由于 mView 已经不为 null 了导致 if 代码块内的逻辑无法执行. 所以这个方法的含义其实就是保证了 mView 这个变量不论在任何线程环境下都是单例的, ViewRoot 只有 mView 这一个孩子. 另外, 该方法的注释也能证明我们的结论是正确的.
看来 ViewRoot 的 setView() 方法是相当重要啊, 万一不小心给他设置了错误的孩子, 那么我们此后想补救都没办法, 所以我们就来看看哪些地方调用了该方法. 经过搜索, 我们发现, 只有 WindowManagerImpl 类的 addView() 方法调用了该方法, 那么我们就来看看 addView() 方法的源码吧:
第7行, setView() 方法中传递的参数 view 是来自于 addView() 方法的参数. 所以, 我们再次搜索哪些地方调用了 WindowManagerImpl 类的这个 addView() 方法. 由于我们要分析的是系统刚加载后, 整个View树要添加进去的第一个View, 所以, 经过排除掉一些搜索结果后, 我们发现, 只有两处调用比较符合我们的要求:
1. ActivityThread 类中的 handleResumeActivity() 方法.
2. Activity 类中的 makeVisible() 方法.
而1这个方法其实也会调用2的方法. 而我们知道一个 Activity 启动后只有完成了 onResume() 的过程之后才会将屏幕中的控件呈现出来, 所以从方法名称来看, 1中提到的 ActivityThread 类的 handleResumeActivity() 方法更符合我们当前分析的情景. 所以, 我们来看看这个方法的源码:
从第16行可知, addView()方法要设置的 mView 其实就是参数decor, 从第7行中的方法名称可知, 这个 decor 就是我们前面介绍过的 DecorView. 而先前我们提到过, mView 是 ViewRoot 唯一的孩子, 所以整个 ViewRoot 的唯一孩子就是 DecorView. 另外, 从第16行还能知道, 我们为该 DecorView 设置的布局参数为变量 l, 从第10行可知, 变量 l 就是 r.window.getAttributes(), 也就是调用 Window 类中 getAttributes() 方法的返回值. 我们看看该 getAttributes() 方法的源码:
从该源码可知, 我们为 DecorView 设置的布局参数其实就是 Window 类中的 mWindowAttributes. 而 Window 类对于 mWindowAttributes 的赋值如下:
该变量的值就是 WindowManager.LayoutParams 这个类的无参构造方法所创建的实例. 该构造方法源码如下:
留意上述源码第2行, 他会调用其父类 ViewGroup.LayoutParams 含有两个int型参数的构造方法, 并为这两个参数都传入 LayoutParams.MATCH_PARENT 的数值. 而查看其父类 ViewGroup.LayoutParams 的该构造方法的源码:
可知, 所传入的两个 LayoutParams.MATCH_PARENT 分别赋给了 width 和 height, 也就是说, 系统为 DecorView 设定的 width 和 height 的数值均为 LayoutParams.MATCH_PARENT.
我们再回到 ViewRoot 类的 performTraversals() 方法, 再次查看他的源码如下:
再次看这个源码, 我们知道了系统第一个进行测量的控件 host, 其实就是 DecorView. 第15行就是对 DecorView 的测量, 该测量又涉及到 childWidthMeasureSpec 和 childHeightMeasureSpec 这两个参数, 二者分别在第11, 12行进行了赋值. 我们分析一下11行 childWidthMeasureSpec 的计算吧. 我们先计算出 desiredWindowWidth 和 lp.width 的具体数值, 然后再去分析 getRootMeasureSpec()方法.
desiredWindowWidth 其实就是整个手机屏幕的宽度, 这个就不做具体分析了. 我们重点分析下 lp.width. 从第5行可知, lp 其实就是 mWindowAttributes, 而 mWindowAttributes 在 ViewRoot 类中的赋值如下:
也就是说, lp = mWindowAttributes = new WindowManager.LayoutParams().
而我们前面曾经分析过, WindowManager.LayoutParams 类的无参构造方法, 其实就是设置width 和 height 都为 match_parent. 所以, 在 performTraversals() 方法第11行中的 lp.width 就是 match_parent, 第12行的 lp.height 也是 match_parent. 所以这里其实也再次印证了我们前面总结的这个结论.
知道了参数具体赋的数值以后, 接下来我们就来分析下第11, 12行都调用的 getRootMeasureSpec() 方法的内部细节了, 源码如下:
我们为该方法的参数赋值是:
windowSize = 手机屏幕的宽度或高度,
rootDimension = MATCH_PARENT, 那么该方法返回值的计算就来自于第20行.
所以, 我们其实可以将前边 performTraversals() 方法源码中第11, 12行等效替换为如下内容:
而 MeasureSpec.makeMeasureSpec() 方法其实就是将两个参数相叠加或者按位或的过程. 也就是说,
计算出了 childWidthMeasureSpec 和 childHeightMeasureSpec 的数值后, 我们再回到 performTraversals() 方法的源码, 此时就可以分析第15行, 也就是 DecorView 的 measure() 方法了, 该方法的作用就是测量 DecorView 的尺寸, 该方法的两个参数也已经计算出来了(看这里). 接下来我们分析下 DecorView 中 measure() 方法的源码. 由于 DecorView 是 FrameLayout 的子类, 在 FrameLayout 类以及其父类 ViewGroup 类中都没有找到 measure() 方法的定义, 所以继续向父类查找, 最后在 android.view.View 类中找到了该方法的定义, 我们来看看其源码:
从源码可知, 该方法是 final 的, 表示我们不能重写该方法, 控件的测量过程必须要按照这个方法内规定的步骤来执行. 其实, 控件的具体测量过程是在第34行的 onMeasure() 方法中进行的. 对于我们现在分析的 DecorView 来说, 其测量过程就是在 onMeasure() 方法中进行的, 并且根据前边的结论可知, 我们为 onMeasure() 方法传递的参数分别为:
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY). 下面我们再来看看 onMeasure() 方法的具体过程. 下面是 FrameLayout 类中 onMeasure() 方法的源码:
经过前面的分析, 我们为该方法传递的参数分别是:
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY).
上述源码中第9~16行, 其实是在循环遍历该 DecorView 中所包含的所有子 View, 只要系统允许对所有子 View 都进行测量, 或者虽然系统不允许测量全部子 View, 但只要某个子 View 的 visibility 不为 GONE, 就会对那个子 View 执行 measureChildWithMargins() 方法. 在该方法中, 将我们为 onMeasure() 方法的两个参数 widthMeasureSpec 和 heightMeasureSpec所传递的数值 (数值见这里的结论) 又传递给了这个 measureChildWithMargins() 方法. 根据方法名称可以猜测, measureChildWithMargins() 方法的作用是将子 View 四周的 margin 值都考虑在内, 然后对他进行测量. 我们看下这个方法的源码:
上述代码一共就四句话, 其中第27和33行中的 getChildMeasureSpec() 方法的作用是: 根据当前 ViewGroup 的父亲(也就是当前子 View 的爷爷) 对当前 ViewGroup 宽度或高度的限制条件, 以及当前子 View 的布局参数, 计算出该 ViewGroup 对当前子 View 在宽度或高度方面的限制条件. 而在执行该方法时, 将我们为 onMeasure() 方法传递的两个参数的数值
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY)
又原封不动地分别传给了第27行测量 childWidthMeasureSpec 的 getChildMeasureSpec() 方法, 以及第33行测量 childHeightMeasureSpec 的 getChildMeasureSpec() 方法. 我们就只分析 childWidthMeasureSpec 的计算吧, childHeightMeasureSpec 的计算是类似的. 我们查看 getChildMeasureSpec() 方法的源码如下:
前面我们提到过, getChildMeasureSpec() 方法的作用是: 根据当前 ViewGroup 的父亲(也就是该 ViewGroup 中当前正在进行测量的那个子 View 的爷爷) 对当前 ViewGroup 宽度或高度的限制条件, 以及该 子 View 的布局参数, 计算出该 ViewGroup 对该子 View 在宽度或高度方面的限制条件. 而上面的源码就是对这一结论的具体描述, 由于 specMode 有3种取值, childDimension 也有3种取值, 二者之间互不影响, 所以对他们进行排列组合可知, 一共有9种情况需要讨论, 而任玉刚主席在他的《Android开发艺术探索》这本书中已经将该方法中的这9种情况使用了一个表格进行了一番总结, 使得该方法的逻辑变得更直观. 我这里就直接借用这个表格吧, 如下图所示:
对照上图可知, 在计算 DecorView 宽度的 MeasureSpec 时, 传递的 parentSpecMode 是: MeasureSpec.EXACTLY, parentSpecSize 是手机屏幕的宽度, 而我们在前面也对 DecorView 的布局参数得出过这样的结论: 系统为 DecorView 设定的 width 和 height 的数值均为 LayoutParams.MATCH_PARENT (如果忘了, 请看这里). 所以, 对照上边的表格, 我们就可以知道, DecorView 的父亲测量 DecorView 的宽度和高度上所做出的限制都是: parentSpecMode = EXACTLY . 而 DecorView 的宽和高都是 match_parent (即: 其宽和高的 childLayoutParams 都等于 match_parent ). 所以根据上面的表格, 我们可以知道, DecorView 宽和高的 measureSpec 的值都是 EXACTLY + parentSize. 而 parentSize 对应宽和高来说, 分别是手机屏幕的宽度和高度. 所以, 我们可以得出如下结论:
DecorView 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
DecorView 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
上述等式中的 widthMeasureSpec 和 heightMeasureSpec 的数值, 正是我们前面还未分析完的 FrameLayout 类的 measureChildWithMargins() 方法中的变量 childWidthMeasureSpec 和 childHeightMeasureSpec 的具体值. 我们回到该方法的源码, 继续来分析 DecorView 的测量过程. 我们接下来要分析该方法中的最后一句代码了:
而对于 DecorView 来说, 这句代码其实等效于:
而我们先前介绍过, DecorView 中只有一个子 View, 是一个竖直方向的 LinearLayout (也就是这个图中的②), 在该 LinearLayout 中包含了两个子View, 一个是默认不会加载的ActionBar (体积为0, 不占据空间. 见这个图中的③), 另一个是 id 为 android.R.id.content 的 FrameLayout (见这个图中的④), 我们完全可以认为该 LinearLayout 中只包含后者这一个子 View. 于是接下来我们就要来分析这个 LinearLayout 的 measure() 方法了. 其实, LinearLayout 的 measure() 方法的分析思路和过程, 与我们分析 DecorView 这个 FrameLayout 的 measure() 方法是相同的, 也和他们共同的父类 ViewGroup 的分析思路相同, 都是遍历他们内部的每一个 visibility 不为 GONE 的子 View, 并逐一进行测量. 所以, 我这里就不再具体分析. 直接给出如下结论:
id 为 android.R.id.content 的 FrameLayout 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
id 为 android.R.id.content 的 FrameLayout 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
而这个id为 android.R.id.content 的 FrameLayout (见这个图中的④) 内只有一个子View, 也就是我们在xml布局文件中设置的 LinearLayout (见这个图中的⑤) , 并且我们设置其 layout_width 和 layout_height 都是 match_parent, 该 LinearLayout 的各个参数与先前分析的那个 LinearLayout (也就是这个图中的②) 是完全一样的, 所以我们也可直接得出如下结论:
xml布局文件中的 LinearLayout 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
xml布局文件中的 LinearLayout 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
而在得到上述两个 measureSpec 变量的值后, 接下来, 我们要做的就是, 执行xml布局文件中 LinearLayout (也就是这个图中的②)) 的 measure() 方法, 并将上述结论中的 widthMeasureSpec 和 heightMeasureSpec 作为参数传入该方法. 而 measure()方法的执行流程, 我们前面已经描述过, 也就是对非 GONE 的子 View 逐一执行该 LinearLayout 的 measureChildWithMargins() 方法. 而 measureChildWithMargins() 方法的执行步骤仍然是:
1. 获取到当前正在测量的子 View 的布局参数 (通常是布局文件中该子 View 的 layout_width 和 layout_height 的值)
2. 根据该 LinearLayout 宽/高的 measureSpec 值和步骤1中的布局参数分别计算出该子 View 宽/高的 measureSpec 值.
(备注: 可以根据前边贴的那张表格截图来分别计算)
3. 调用该子 View 的 measure() 方法, 并将步骤2中计算出的该子 View 宽/高的 measureSpec 值作为参数一起传递给该方法.
而在遍历测量各个子 View 的过程中, 在xml布局文件中先添加的子 View总是先被访问, 所以, 在我们自定义圆的例子中, 会先进行自定义圆的测量, 后进行 Button 的测量.
我们先来分析xml布局文件中的 LinearLayout 对我们自定义圆进行 measureChildWithMargins() 测量的过程吧.
我们按照上述3个步骤来进行.
1. 获取当前正在测量的子 View 的布局参数 (通常是布局文件中该子 View 的 layout_width 和 layout_height 的值).
可以通过布局文件中我们为自定义圆设定的 layout_width 和 layout_width 这两个属性的值来得到该布局参数, 一共有3种可能的取值: dp/px数值, wrap_content, match_parent.
2. 根据该 LinearLayout 在宽和高两方面各自的 measureSpec 值和步骤1中的布局参数分别计算出该自定义圆在宽和高两方面各自的 measureSpec 值.
先看宽度: 我们在前面得出过结论: xml布局文件中的 LinearLayout 在宽度方面的 measureSpec 值等于 (EXACTLY + 手机屏幕的宽度), 再结合步骤1中得到的自定义圆的布局参数, 根据 getChildMeasureSpec() 方法或者直接由我们前边贴出的那个表格, 就能得到该自定义圆在宽度方面的 measureSpec 值了. 该自定义圆在高度方面的 measureSpec 值也同理可得. 为便于具体分析我们这个自定义圆的例子, 我这里把对 getChildMeasureSpec() 方法内部流程的那张总结表格再次贴出来:
在我们这个例子中, 自定义圆在宽度方面的 parentSpecMode (也就是 xml布局文件中 LinearLayout 的 widthSpecMode ) = EXACTLY, parentSpecSize (也就是 xml布局文件中 LinearLayout 的 widthSpecSize ) = 手机屏幕的宽度, 而宽度方面的布局参数 childLayoutParams (也就是布局文件中为自定义圆设置的 layout_width ) 的数值由我们自己设定. 根据该表格中标红的那一列可知,
自定义圆在宽度方面的 measureSpec (通常也称作 widthMeasureSpec) 可以分为如下3种情况:
如果我们将自定义圆的 layout_width 设置为具体的 dp/px 数值, 那么自定义圆的 widthMeasureSpec 就是
EXACTLY + childSize (也就是我们设置的 dp/px 数值).
如果我们将自定义圆的 layout_width 设置为 match_parent, 那么自定义圆的 widthMeasureSpec 就是
EXACTLY + parentSize (即: 手机屏幕的宽度).
如果我们将自定义圆的 layout_width 设置为 wrap_content, 那么自定义圆的 widthMeasureSpec 就是
AT_MOST + parentSize (即: 手机屏幕的宽度).
从上述3种情况, 我们可以得出自定义圆在宽度方面的如下结论:
如果我们为自定义圆的 layout_width 设置的是 wrap_content, 那么该自定义圆的 widthMeasureSpec 中的 specMode (也就是 widthSpecMode) = AT_MOST.
如果我们自定义圆的 widthSpecMode = AT_MOST, 那么该圆的 layout_width 数值一定是 wrap_content.
总结来说, 我们自定义圆的 layout_width = wrap_content 与 widthSpecMode = AT_MOST 是一一对应关系.
同理可知, 自定义圆在高度方面的 measureSpec (通常也称作 heightMeasureSpec) 也可以分为如下3种情况:
如果我们将自定义圆的 layout_height 设置为具体的 dp/px 数值, 那么自定义圆的 heightMeasureSpec 就是
EXACTLY + childSize (也就是我们设置的 dp/px 数值).
如果我们将自定义圆的 layout_height 设置为 match_parent, 那么自定义圆的 heightMeasureSpec 就是
EXACTLY + parentSize (即: 手机屏幕的高度).
如果我们将自定义圆的 layout_height 设置为 wrap_content, 那么自定义圆的 heightMeasureSpec 就是
AT_MOST + parentSize (即: 手机屏幕的高度).
同样地, 从上述3种情况, 我们也可以得出自定义圆在高度方面的如下结论:
如果我们为自定义圆的 layout_height 设置的是 wrap_content, 那么该自定义圆的 heightMeasureSpec 中的 specMode (也就是 heightSpecMode) = AT_MOST.
如果我们自定义圆的 heightSpecMode = AT_MOST, 那么该圆的 layout_height 数值一定是 wrap_content.
总结来说, 我们自定义圆的 layout_height = wrap_content 与 heightSpecMode = AT_MOST 是一一对应关系.
我们将前面得出的自定义圆在 宽度方面的结论 和 高度方面的结论 归纳一下, 得出如下结论:
自定义圆的宽/高的结论:
对于自定义圆的宽度和高度中的任意一个尺寸来说, 只要在xml布局文件中设置的是 wrap_content, 那么该尺寸的 specMode 就一定是 AT_MOST. 相反, 只要某个尺寸的 specMode 是 AT_MOST, 那么该尺寸的数值就一定是 wrap_content.
3. 调用自定义圆的 measure() 方法, 并将步骤2中计算出的 widthMeasureSpec 和 heightMeasureSpec 作为参数一起传递给该方法.
自定义圆的类名是 CustomCircleView, 该类中没有 measure() 方法, 所以会去调用其父类(即: View类)中的 measure() 方法. 前面我们介绍过, View类中的 measure()方法的核心步骤是 onMeasure()方法, 该方法可以由用户来重写以提供自定义的测量过程. 而由于我们的 CustomCircleView 类中没有重写 onMeasure() 方法, 所以还是会调用其父类 (即: View 类) 中的该方法, 并且将步骤2中计算出的 widthMeasureSpec 和 heightMeasureSpec 作为参数传递给该方法. 我们来看看该方法的源码:
该方法会直接调用 setMeasuredDimension() 方法. 而 setMeasuredDimension() 方法的作用是设置该控件的”测量宽高” (当然, 你可以将”测量宽高”简单理解为就是该控件最终呈现出来的宽高. 这种观点在一般情况下都是正确的, 只有在极少数特殊情况下才是错误的. 此处不详细介绍). 那么, 也就是说, setMeasuredDimension() 方法中传入的两个 getDefaultSize() 方法的返回值将分别是该控件的测量宽度 (通常用 measuredWidth 表示) 和 测量高度 (通常用 measuredHeight 表示). 而上边源码中, 我们直接将 onMeasure() 方法的 widthMeasureSpec 和 heightMeasureSpec 这两个参数 (他们都是在步骤2中计算的 ) 原封不动地分别传递给该方法内的两个 getDefaultSize() 方法. 我们就来看看这个 getDefaultSize() 方法的源码:
从该方法的名称就可以看出, 该方法的作用是, 获取系统分配给该子控件的默认尺寸 (默认宽度或默认高度) . 源码中的注释已经非常详细了, 所以就不再对源码本身做出解释了. 我们就以获取默认宽度为例来进行分析. 由于在宽度方面, 我们为该方法第二个参数 measureSpec 传入的数值其实就是在步骤2中计算出的 widthMeasureSpec 的数值, 在步骤2中我们知道, widthMeasureSpec 一共有两种可能的取值, 一种是 EXACTLY + dp/px 数值, 另一种是 EXACTLY + 手机屏幕的宽度. 至于取哪个值, 取决于我们在 xml 布局文件中为自定义圆的 layout_width 属性所赋的值. 但有一点是确定的, 即: widthMeasureSpec 中的 specMode 一定是 EXACTLY, 而只是 specSize 需要根据情况而定. 而再看 getDefaultSize() 的源码可知, EXACTLY 对应的结果是 specSize. 所以 getDefaultSize() 方法的返回值一定是 specSize, 即: 我们自定义圆的默认宽度一定是 specSize, 而 specSize 的数值是根据我们在xml布局文件中为 layout_width 属性设定的数值而定的, 如果我们为 layout_width 设置为具体的 dp/px 数值, 则 specSize 的值 (也即: 我们自定义圆的默认宽度) 就等于该 dp/px 值, 而如果为 layout_width 设置的是 wrap_content 或 match_parent, 则 specSize 的值 (也即: 我们自定义圆的默认宽度) 就是手机屏幕的宽度. 即:
我们自定义圆的默认宽度是:
如果为自定义圆的 layout_width 设置的是 dp/px 数值, 则自定义圆的默认宽度 = 该 dp/px 值.
如果为自定义圆的 layout_width 设置的是 wrap_content 或 match_parent, 则自定义圆的默认宽度 =
手机屏幕的宽度
用表格表示就是:
同理, 我们也可以得出我们自定义圆的默认高度:
我们自定义圆的默认高度是:
如果为自定义圆的 layout_height 设置的是 dp/px 数值, 则自定义圆的默认高度 = 该 dp/px 值.
如果为自定义圆的 layout_height 设置的是 wrap_content 或 match_parent, 则自定义圆的默认高度 = 手机屏幕的高度
用表格表示就是:
分析完系统为控件宽和高所提供的默认尺寸的获取方法以后, 我们再回头去看 View 类中的 onMeasure() 方法, 我们看到, 该方法直接调用了 setMeasuredDimension() 方法, 并且把系统为控件提供的默认宽和高直接作为该方法的参数, 而前面也提到, setMeasuredDimension() 方法中的两个参数分别表示控件的 “测量宽度”和 “测量高度”, 并且我们还提到过, 一个控件的 “测量宽度”/”测量高度”一般来说就是它最终在屏幕上呈现出来的实际宽度/高度. 所以, onMeasure() 方法的含义其实是, 将系统为我们自定义圆提供的默认宽高, 直接作为他们实际显示在屏幕上宽高. 那么我们先前对自定义圆的默认宽度和默认高度分别得出的结论 (具体见: 默认宽度的结论 和 默认高度的结论 ) 也同样分别适用于该自定义圆的实际宽度和实际高度. 那么我们同样用表格的形式来总结一下吧:
从这两个表格中, 我们已经很清楚地知道了, 只要我们为自定义圆的 layout_width 或 layout_height 设置为 wrap_content , 那么该自定义圆的宽度或高度就必定充满整个屏幕的宽或高. 而如果设置为 dp/px 值, 只要该值不超过屏幕的宽/高, 那么自定义圆的宽/高就等于我们设定的这个 dp/px 值, 而不是充满整个屏幕的宽/高, 这样我们也就有可能看到 Button 了.
还记得我们曾在本文前边 标记0 处提出了一系列问题吗? 通过我们上述的分析, 想必这些问题都已经有了答案吧. 总结一下这些问题所提到的那个 bug 产生的根源, 就是在 onMeasure() 方法中, 我们对控件在 layout_width 或 layout_height 被设置为 wrap_content 时的情况没有提供我们自己的处理方式, 导致被系统按照 match_parent 的情况来处理了. 而如果看过系统原生控件源码的话, 就会发现, 这些控件都会在 onMeasure() 方法中对 wrap_content 的情况进行单独处理的.
既然我们已经知道了该 bug 产生的根源, 那么我们今后在自定义控件中, 如何避免该 bug 的产生呢? 显然, 我们需要重写 onMeasure() 方法, 并在该方法内对 wrap_content 的情况进行单独的处理. 但是, 该方法只提供了 widthMeasureSpec 和 heightMeasureSpec 这两个参数, 根据这两个参数, 我们还能计算出宽和高各自的 specMode 和 sepcSize, 我们无法进行更进一步的计算了, 但是只是得到 specMode 和 sepcSize 的值, 与我们要处理的 wrap_content 之间又有什么关系呢? 我们到底该如何处理 wrap_content 呢?
其实, 我们在前边曾提到过 自定义圆的宽/高的结论, 根据这个结论, 我们就能知道, 单独处理 wrap_content 的情况, 其实也就是要单独处理 specMode 为 AT_MOST 的情况. 所以我们就有了解决思路, 我们只需对宽高的 specMode 各自为 AT_MOST 时, 分别为宽/高设置一个默认的 dp/px 数值即可.
根据这个思路, 我们将前边没有重写 onMeasure() 方法的那个自定义圆的代码, 再重新整理一遍, 添加对 wrap_content 情况的处理, 代码如下:
上面是我们自定义圆的代码, 我们将xml布局文件中该圆的 layout_width 和 layout_height 都设置为 wrap_content, 然后运行看看实际效果:
从图中可以看出, wrap_content 的情况已经被我们成功处理了. 当然, 这里的自定义圆的代码还可以进一步完善, 比如: 可以增加自定义属性, 还可以在 onDraw() 方法中处理为控件设置的 padding 等, 这里就不做介绍了. 点击 这里 可以获取该demo例子的代码.
另外说明一下, 如果我们自定义的控件是直接继承自 Android SDK 中已有的控件 (比如: TextView, Button, LinearLayout, RelativeLayout 等), 由于这些控件自身已经处理了 wrap_content 的情况, 所以我们就无需再进行处理了. 但如果我们自定义的是继承自 View 类或 ViewGroup 类的控件, 那么我们就必须要处理 wrap_content 的情况.
好了, 本文的介绍就到此为止吧.
《Android开发艺术探索》第4章 View的工作原理.
前言:
这是一篇与 Android 自定义控件相关的源码分析的文章. 阅读本文前, 读者最好能对 Android 基础知识和自定义控件的基础知识 (例如: onMeasure(), onLayout(), onDraw(), MeasureSpec等) 都有一定的了解, 至少进行过一些简单自定义控件的设计, 否则建议你先学习一下这些基础知识并亲自动手实践设计一些简单的自定义控件, 然后再来阅读本文会更好.正文:
自定义控件有很多种分类, 这里就不做具体介绍了. 我们今天要讨论的是, 自定义继承自 View 类或 ViewGroup 类的情况. 下面以一个简单的自定义控件的例子, 来引出我们今天要深入讨论的这个话题.假如我们有如下需求: 要紧贴着手机屏幕的左上角放置一个圆, 要求该圆的半径为50px, 在圆的右边要放置一个Button (尺寸随意). 由于今天我们讨论的是自定义控件的话题, 所以我们就以自定义控件的方式来绘制这个圆. 那么, 既然是要以自定义控件的方式来实现, 那么自定义控件的一些初学者肯定会说, 这太简单了, 直接自定义一个继承自 View 类的圆, 在其 onDraw() 方法中设置圆心的 x, y 坐标都等于半径50px, 并以 50px 为半径画圆即可实现该自定义的圆. 而布局文件可以使用水平方向的 LinearLayout, 先将该圆添加进去, 由于我们已经在 onDraw() 方法中设置了圆心坐标和半径都为 50px 这个绝对数值, 这样我们就可以直接计算出圆的宽和高都为 100px 了, 那么在布局文件中我们就可以直接设置该圆的 layout_width 和 layout_height 都为 100px, 但是仔细一想, 万一今后需求变了, 改为要让该圆的半径可以由用户自由设定, 那我们岂不是又要改这里的 layout_width 和 layout_height ? 所以这里索性就设置为 wrap_content 吧, 包裹内容, 既满足需求又能满足扩展要求. 然后再添加一个 Button 到该布局文件中, OK 搞定了. 于是就可能有了如下的代码:
/** * 自定义圆 */ public class CustomCircleView extends View { private Paint mPaint; public CustomCircleView(Context context) { super(context); init(); } public CustomCircleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomCircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { if (mPaint == null) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.parseColor("#0DC65B")); // 圆内填充色为绿色 mPaint.setStyle(Paint.Style.FILL); } } @Override protected void onDraw(Canvas canvas) { // 圆心的 x, y坐标均为 50px, 半径也为 50px canvas.drawCircle(50, 50, 50, mPaint); } }
上面是自定义圆的代码.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/linearlayout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context=".MainActivity"> <com.example.custom_view.widget.CustomCircleView android:id="@+id/circle" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="button" android:textSize="30sp" android:textColor="#000000"/> </LinearLayout>
上面是布局文件.
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
Activity非常简单, 仅仅设置了该 Activity 对应的布局文件.
我们直接预览布局文件, 或者运行上述代码, 我们会发现, 屏幕中竟然只显示了圆, 而没有显示Button, 如下图所示:
太奇怪了, 不可能吧! 是不是哪里手误写错了? 于是又仔细检查了代码, 发现都没问题, 那这是怎么回事呢? 有些同学此刻可能已经陷入了疑惑和不解中, 但为了实现需求, 就直接把 wrap_content 改为了 100px, 而有些有钻研精神的同学, 可能暂时不会立即将 wrap_content 改为 100px, 而是考虑先试着来个 debug, 看能不能解决这个问题, 如果实在不能解决, 再改为 100px 也不迟啊. 那么我们就来试着调试一下这个 bug 吧.
为了调试该bug, 我们可以为自定义的圆添加一个黄色的背景, 为 Button 添加一个淡蓝色背景, 这样将各个子控件的背景颜色和布局文件中的父容器 LinearLayout 的白色背景加以区分, 便于我们诊断故障. 我们在布局文件中为我们的自定义圆添加如下代码:
android:background="#F8D21D" <!-- 设置自定义圆的背景色为黄色 -->
为 Button 添加如下代码:
android:background="#330000ff" <!-- 设置Button的背景色为淡蓝色 -->
再来看下布局预览或运行APP后的效果:
我们发现, 整个屏幕的背景都变为了黄色并且没有看到淡蓝色背景的Button. 这说明, 整个屏幕都被我们自定义的圆所占据了, 奇怪, 我们为自定义的圆设定的宽高明明都是 wrap_content, 为何实际上却占满了整个屏幕呢? 另外, 我们看不到 Button, 难道是因为我们自定义的圆将 Button 挤出了屏幕? 还是因为圆已经占满了屏幕, 使得 Button 压根儿就没有绘制呢?
上述两个问题, 我们先解决第二个. 即: 我们看不到 Button, 到底是因为 Button 被挤出了屏幕, 还是压根儿没被绘制.
我们分析一下, 即使是 Button 被挤出了屏幕, 那么也是需要对他进行绘制的. 只要他完成了绘制, 那么我们也依然能够获取到他的宽高以及四条边的坐标. 而如果Android系统压根儿就没有绘制这个 Button , 那么我们一定获取不到他的宽高, 也获取不到他的四条边的坐标. 这就是上述两个可能原因之间的区别. 我们可以从这个区别点入手, 就能分析 Button 消失的具体原因了.
为了计算 Button 的宽高以及四条边的坐标位置, 我们需要修改先前 MainActivity 的代码, 改为如下内容:
public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private Button mBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); setListener(); } private void initView() { mBtn = (Button) findViewById(R.id.btn); } private void setListener() { /* * 获取该Button的View树的观察者,并为该观察者设置布局变化的监听器. * 这样只要该Button的布局发生了变化(例如: 从无到有, 从有到无, 或者 * 其大小或位置发生了变化等), 就会触发该监听器内部的回调方法的执行. */ mBtn.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // 第一次回调通常都是该控件从无到有的过程, 这正是我们本次demo想要跟踪 // 的时间节点, 因此只要发生一次, 就证明该Button被绘制过, 我们就得到了 // 我们想要的结论, 因此可以取消掉该监听. // // 另外注意一点: removeGlobalOnLayoutListener()方法在 API-1 就被引入了, 所以可以兼容任何版本的设备, // 尤其是较低版本的设备, 该方法在API-16时被废弃, 从 API-16 开始, 官方建议使用 removeOnGlobalLayoutListener() // 方法来替代该方法, 废弃的原因我猜测就是因为命名不符合常规习惯吧, 因为查看 API-16 甚至 API-23 的源码可知, // 这个从 API-16 开始被废弃的方法的内部其实就是调用取代他的那个新方法的. 所以, removeOnGlobalLayoutListener() // 方法只能使用在 API-16 及以后版本的设备上. 如果你的 minSdk 版本低于 API-16, 也就是你的 APP 还需要兼容较低 // 版本的设备 (例如: Android 2.3, 4.0), 那么还是要使用 removeGlobalOnLayoutListener()方法, 也就是名称明显 // 不符合 Android 系统通常用于取消 Listener 所遵循的命名规则的那个方法. mBtn.getViewTreeObserver().removeGlobalOnLayoutListener(this); // 打印相关尺寸位置信息 printDimensions(); } }); } /** * 打印尺寸位置信息 */ private void printDimensions() { int btnWidth = mBtn.getWidth(); int btnLeft = mBtn.getLeft(); int btnRight = mBtn.getRight(); Log.i(TAG, "Button: 宽度 = " + btnWidth + "px, 左边框的位置 = " + btnLeft + "px, 右边框的位置 = " + btnRight + "px"); } }
这里顺便提一下, 上述代码中, 要想获取控件的宽高或四条边的位置, 我们不能在 Activity 的生命周期方法中去执行 View.getWidth(), View.getHeight(), View.getLeft(), View.getRight() 等方法, 因为 Activity 的加载和 View 的绘制是异步进行的. 所以我们必须要在确保 Button 确实完成了绘制后, 或者至少要完成了 layout 后, 我们再去获取其宽高和四条边的位置. 因此我们可以按照上述代码中 setListener() 方法那样, 在 onGlobalLayout() 回调方法中来执行上述操作. 代码中相关注释已经很详细了, 就不再详细介绍这个小细节了.
上述代码运行后, 我们看看打印的log:
看来, Button 成功回调了 onGlobalLayout() 方法, 说明系统确实是绘制了这个 Button. 并且其左右边框的位置都是 1080px, 而我们的代码刚好就是运行在分辨率为 1920*1080px 的手机上, 手机屏幕的宽度也是1080px, 这和 Button 的左右边框的位置值刚好相同. 所以, 看来 Button 还是绘制了, 只是宽度被挤成了0, 其左右边界都刚好与屏幕的右边界重合. 所以这种情况即可算作在屏幕内, 也可算作在屏幕外. 而此刻, 如果我们为 Button 添加一个正数的 leftMargin 值, 看看此时测量的 Button 的左右边界的坐标值是否会大于屏幕右边界的坐标呢? 我们在布局文件中为 Button 添加如下代码:
android:layout_marginLeft="30dp"
再次运行, 查看打印的log:
我们看到, Button 的左右边框都变为了 1170px, 这已经超出了手机屏幕右边框的 1080px了, 表明 Button 此时是在手机屏幕外. 而由于我们手机的分辨率 1920*1080px 属于 xxhdpi, 所以对于该手机来说, 1dp == 3px, 那么我们为 Button 添加的 30dp 的 marginLeft, 其实等于90px, 而 1170px - 90px = 1080px, 刚好等于手机屏幕右边框的 x 坐标, 而我们自定义的圆没有设置 marginLeft 和 marginRight, 所以这说明 Button 此时确实是在屏幕外. 其实我们也可以使用同样的方式, 来分别测量我们自定义圆以及布局文件中父容器 LinearLayout 二者的宽高和四条边的位置, 由于代码和测量 Button 的代码类似, 所以这里就不贴出了, 我们直接看结果:
从上图的结果中可以看出, 我们自定义圆的宽高和父容器 LinearLayout 的相等, 并且二者的左边框和上边框分别重合, 所以, 我们可以得出结论, 我们自定义的圆确实完全填充了他的父容器. (读者朋友此处不要纠结于高度 1701px 小于手机分辨率 1920*1080px 中的 1920px, 因为我也把 LinearLayout 他的父容器, 即: id为 android.R.id.content 的 FrameLayout 的尺寸和位置也打印出来了, 发现和其他二者是完全相同的, 所以我们就不要去纠结于高度不等于1920px了, 只要知道自定义的圆填满了整个 LinearLayout 这个结论就行了).
既然得出了这个结论, 那么我们就要分析一下, 为什么我们自定义的这个圆明明设置的宽高都是wrap_content, 但最后呈现出的效果却是填满整个父容器, 也就是 match_parent 的效果? 如果将该自定义圆的宽高改为 match_parent, 或者具体的尺寸数值, 又将会是什么样的效果呢?
于是, 我们首先验证将自定义圆的宽高都修改为 match_parent, 发现仍然是填充满整个父容器的效果, 这种反倒是正确的现象, 如果不填充满父容器反而不符合 match_parent 的含义了. (这里我就不贴出效果图了).
而将自定义圆的宽高改为具体的数值, 我们发现, 情况有所不同了. 例如: 我们将宽和高都改为 200dp, 直接预览布局文件或者运行代码, 效果图如下:
我们能看到紫色背景的 Button 了, 并且自定义圆的大小也不再是填充满整个屏幕了.
而如果我们只将宽度改为 200dp, 高度仍保持为 wrap_content, 效果图如下:
我们发现, 还是能看到 Button, 并且自定义圆的宽度不是填充满父容器的宽度, 但他的高度却仍然是填充满父容器的高度.
我们再将高度改为 200dp, 将宽度再改回到 wrap_content, 效果图如下:
我们发现, 这种情况下, 自定义圆的宽将填充满父容器的宽导致 Button 看不见了, 而自定义圆的高度则不会充满父容器的高.
通过上述三个测试, 我们能够发现一个可能的规律: 对于自定义圆来说, 其宽度和高度, 我们设置二者中的哪个尺寸为 wrap_content, 那么那个尺寸实际上呈现出的效果将不是包裹内容, 而是充满他所在父容器的那个尺寸, 也就是在那个尺寸上将呈现出 match_parent 的效果. 而设置为具体的dp/px数值 (只要尺寸数值不超过父容器的相应尺寸), 则不会出现这种异常现象.
但是, 这只是该 bug 的现象, 那么该 bug 的真正原因到底是什么呢? 这个问题我们将在后边给出答案. 这里先暂时做个标记.———- 标记0
要想知道这个问题的答案, 我们就必须要先了解一下 View 系统的绘制原理或流程. Android 中整个 View 系统可以用下图来表示:
另外, 为了便于接下来的一小段理论分析, 我再把我们前面自定义圆的那个 demo 工程使用 Hierarchy Viewer工具生成的 View 树也贴出来, 如下图所示:
我们的一个 Activity 内一般都是包含着一个Window (翻译为”窗户”, 对于手机来说, 就是 PhoneWindow这个子类), PhoneWindow 内有个名叫 DecorView 的 FrameLayout (见上图中的①). decor 的意思是”装饰物”, 所以这个 DecorView 就是用来装饰手机屏幕这扇窗户的, DecorView 本身是一个 FrameLayout, 所以他也是一种存放控件的容器, 该容器内部又是由一个竖直方向的 LinearLayout (见上图中的②) 容器来进行统一管理. 该 LinearLayout 内部分为上下两部分, 上边用来放置 ActionBar (见上图中的③), 但这个 ActionBar 被系统设置为 ViewStub 的惰性加载形式, 也就是默认情况下是不会被加载的, 可以认为默认情况下其体积为0, 不占据任何空间. 下边是一个id为 android.R.id.content 的 FrameLayout (见上图中的④), 而④这个 FrameLayout 容器, 才是系统开放给我们应用开发者的, 至于他内部摆放什么控件以及如何摆放这些控件, 则完全由我们开发者自己来决定. 我们通常把④这个 FrameLayout 容器内部摆放的所有控件的整体称作这个容器的 content view, 也就是他内部的 View. 这个 content view 可以有多种提供方式. 我们既可以先将我们要提供的控件写在 xml 布局文件中, 然后借助于 Activity 来为④设置 content view; 也可以直接在 Activity 中通过 java 代码创建控件对象, 并通过一系列布局方法设置这些控件属性的方式来提供; 还可以同时结合上述两种方式共同提供. 在我们自定义圆的 demo 项目中, 我们是使用第一种方式, 也就是 xml 布局文件的方式来提供容器④的content view, 布局文件中最外层的 LinearLayout 其实就是上图中的 ⑤, 我们自定义的圆就是上图中的⑥, Button 就是上图中的⑦.
其实上图就是一棵View树, 只是这棵树倒在地上了而已. 我们按照数据结构中对树的定义, 来重新绘制这棵树, 如下图所示:
介绍完 Android 控件的树形系统之后, 再来简要介绍下 Android 控件这棵树的测量机制.
Android 控件树 (也可以叫做 View 树) 的测量流程必定是从 DecorView 开始的. 任何一个控件的测量都是走的 measure(int widthMeasureSpec, int heightMeasureSpec) 这个方法. 该方法有两个参数 widthMeasureSpec 和 heightMeasureSpec, 这两个参数分别表示该控件的父容器对该控件在宽/高方面的限制条件, 都是32位的 int 型数. 他们的高2位都表示 specMode (即: 父容器对该控件在测量模式方面的限制, 有3个可取的值: UNSPECIFIED, EXACTLY, AT_MOST), 低30位表示specSize (即: 父容器对该控件在测量尺寸方面的限制).
此时的我们不禁要问, 系统为 DecorView 的 measure()方法中那两个参数到底传的是什么数值呢?
其实, View 系统的测量过程是在 ViewRoot 类中的 performTraversals() 方法中进行的 (我这里分析的是 Android 2.2.2 版本的源码, 其他版本的源码可能略有不同), 看下其源码:
private void performTraversals() { // cache mView since it is used so much below... final View host = mView; // ...省略大量代码 WindowManager.LayoutParams lp = mWindowAttributes; int desiredWindowWidth; int desiredWindowHeight; int childWidthMeasureSpec; int childHeightMeasureSpec; // ...省略大量代码 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // ...省略大量代码 host.measure(childWidthMeasureSpec, childHeightMeasureSpec); // ...省略大量代码 }
第14行, 就是根视图的测量过程. 而从第3行可知, host 其实就是 mView. 那么 mView 到底是什么呢? 我们在 ViewRoot 类的源码中找找对该变量的赋值语句吧. 我们找到了很多个包含对 mView 的赋值语句的方法. 但是由于我们这里分析的是 View 的测量过程, 所以类似于 dispatchDetachedFromWindow() 这样的方法从名称来看就与我们正在分析的情景不符, 所以直接被我们忽略掉, 最后, 我们发现 setView()方法相对来说是最符合我们的情景的, 于是我们看下这个方法的源码:
/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; // ...省略大量代码 // this 就是 ViewRoot view.assignParent(this); // ...省略大量代码 } } }
这个方法内部, 从整体上来看就是个同步代码块, 所以在同一个时刻, 该方法只可能被一个线程执行. 而在同步代码块的内部, 相当于是一个单例的操作, 即: 只能为 mView 赋一次值, 一旦进行过第一次的赋值后, 下一次再去执行该方法时由于 mView 已经不为 null 了导致 if 代码块内的逻辑无法执行. 所以这个方法的含义其实就是保证了 mView 这个变量不论在任何线程环境下都是单例的, ViewRoot 只有 mView 这一个孩子. 另外, 该方法的注释也能证明我们的结论是正确的.
看来 ViewRoot 的 setView() 方法是相当重要啊, 万一不小心给他设置了错误的孩子, 那么我们此后想补救都没办法, 所以我们就来看看哪些地方调用了该方法. 经过搜索, 我们发现, 只有 WindowManagerImpl 类的 addView() 方法调用了该方法, 那么我们就来看看 addView() 方法的源码吧:
private void addView(View view, ViewGroup.LayoutParams params, boolean nest) { // ...省略大量代码 ViewRoot root; // ...省略大量代码 // do this last because it fires off messages to start doing things root.setView(view, wparams, panelParentView); }
第7行, setView() 方法中传递的参数 view 是来自于 addView() 方法的参数. 所以, 我们再次搜索哪些地方调用了 WindowManagerImpl 类的这个 addView() 方法. 由于我们要分析的是系统刚加载后, 整个View树要添加进去的第一个View, 所以, 经过排除掉一些搜索结果后, 我们发现, 只有两处调用比较符合我们的要求:
1. ActivityThread 类中的 handleResumeActivity() 方法.
2. Activity 类中的 makeVisible() 方法.
而1这个方法其实也会调用2的方法. 而我们知道一个 Activity 启动后只有完成了 onResume() 的过程之后才会将屏幕中的控件呈现出来, 所以从方法名称来看, 1中提到的 ActivityThread 类的 handleResumeActivity() 方法更符合我们当前分析的情景. 所以, 我们来看看这个方法的源码:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { // ...省略代码 ActivityRecord r = performResumeActivity(token, clearHide); // ...省略大量代码 if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } } }
从第16行可知, addView()方法要设置的 mView 其实就是参数decor, 从第7行中的方法名称可知, 这个 decor 就是我们前面介绍过的 DecorView. 而先前我们提到过, mView 是 ViewRoot 唯一的孩子, 所以整个 ViewRoot 的唯一孩子就是 DecorView. 另外, 从第16行还能知道, 我们为该 DecorView 设置的布局参数为变量 l, 从第10行可知, 变量 l 就是 r.window.getAttributes(), 也就是调用 Window 类中 getAttributes() 方法的返回值. 我们看看该 getAttributes() 方法的源码:
/** * Retrieve the current window attributes associated with this panel. * * @return WindowManager.LayoutParams Either the existing window * attributes object, or a freshly created one if there is none. */ public final WindowManager.LayoutParams getAttributes() { return mWindowAttributes; }
从该源码可知, 我们为 DecorView 设置的布局参数其实就是 Window 类中的 mWindowAttributes. 而 Window 类对于 mWindowAttributes 的赋值如下:
// The current window attributes. private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
该变量的值就是 WindowManager.LayoutParams 这个类的无参构造方法所创建的实例. 该构造方法源码如下:
public LayoutParams(int _type, int _flags) { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = _type; flags = _flags; format = PixelFormat.OPAQUE; }
留意上述源码第2行, 他会调用其父类 ViewGroup.LayoutParams 含有两个int型参数的构造方法, 并为这两个参数都传入 LayoutParams.MATCH_PARENT 的数值. 而查看其父类 ViewGroup.LayoutParams 的该构造方法的源码:
public LayoutParams(int width, int height) { this.width = width; this.height = height; }
可知, 所传入的两个 LayoutParams.MATCH_PARENT 分别赋给了 width 和 height, 也就是说, 系统为 DecorView 设定的 width 和 height 的数值均为 LayoutParams.MATCH_PARENT.
我们再回到 ViewRoot 类的 performTraversals() 方法, 再次查看他的源码如下:
private void performTraversals() { // cache mView since it is used so much below... final View host = mView; // ...省略大量代码 WindowManager.LayoutParams lp = mWindowAttributes; int desiredWindowWidth; int desiredWindowHeight; int childWidthMeasureSpec; int childHeightMeasureSpec; // ...省略大量代码 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // ...省略大量代码 // 根据前面的分析可知, 这里的 host 就是 DecorView host.measure(childWidthMeasureSpec, childHeightMeasureSpec); // ...省略大量代码 }
再次看这个源码, 我们知道了系统第一个进行测量的控件 host, 其实就是 DecorView. 第15行就是对 DecorView 的测量, 该测量又涉及到 childWidthMeasureSpec 和 childHeightMeasureSpec 这两个参数, 二者分别在第11, 12行进行了赋值. 我们分析一下11行 childWidthMeasureSpec 的计算吧. 我们先计算出 desiredWindowWidth 和 lp.width 的具体数值, 然后再去分析 getRootMeasureSpec()方法.
desiredWindowWidth 其实就是整个手机屏幕的宽度, 这个就不做具体分析了. 我们重点分析下 lp.width. 从第5行可知, lp 其实就是 mWindowAttributes, 而 mWindowAttributes 在 ViewRoot 类中的赋值如下:
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
也就是说, lp = mWindowAttributes = new WindowManager.LayoutParams().
而我们前面曾经分析过, WindowManager.LayoutParams 类的无参构造方法, 其实就是设置width 和 height 都为 match_parent. 所以, 在 performTraversals() 方法第11行中的 lp.width 就是 match_parent, 第12行的 lp.height 也是 match_parent. 所以这里其实也再次印证了我们前面总结的这个结论.
知道了参数具体赋的数值以后, 接下来我们就来分析下第11, 12行都调用的 getRootMeasureSpec() 方法的内部细节了, 源码如下:
/** * Figures out the measure spec for the root view in a window based on it's * layout params. * * @param windowSize * The available width or height of the window * * @param rootDimension * The layout params for one dimension (width or height) of the * window. * * @return The measure spec to use to measure the root view. */ private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
我们为该方法的参数赋值是:
windowSize = 手机屏幕的宽度或高度,
rootDimension = MATCH_PARENT, 那么该方法返回值的计算就来自于第20行.
所以, 我们其实可以将前边 performTraversals() 方法源码中第11, 12行等效替换为如下内容:
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(手机屏幕的宽度, MeasureSpec.EXACTLY); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(手机屏幕的高度, MeasureSpec.EXACTLY);
而 MeasureSpec.makeMeasureSpec() 方法其实就是将两个参数相叠加或者按位或的过程. 也就是说,
childWidthMeasureSpec = 手机屏幕的宽度 + MeasureSpec.EXACTLY childHeightMeasureSpec = 手机屏幕的高度 + MeasureSpec.EXACTLY
计算出了 childWidthMeasureSpec 和 childHeightMeasureSpec 的数值后, 我们再回到 performTraversals() 方法的源码, 此时就可以分析第15行, 也就是 DecorView 的 measure() 方法了, 该方法的作用就是测量 DecorView 的尺寸, 该方法的两个参数也已经计算出来了(看这里). 接下来我们分析下 DecorView 中 measure() 方法的源码. 由于 DecorView 是 FrameLayout 的子类, 在 FrameLayout 类以及其父类 ViewGroup 类中都没有找到 measure() 方法的定义, 所以继续向父类查找, 最后在 android.view.View 类中找到了该方法的定义, 我们来看看其源码:
/** * <p> * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * </p> * * <p> * The actual mesurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overriden by subclasses. * </p> * * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
从源码可知, 该方法是 final 的, 表示我们不能重写该方法, 控件的测量过程必须要按照这个方法内规定的步骤来执行. 其实, 控件的具体测量过程是在第34行的 onMeasure() 方法中进行的. 对于我们现在分析的 DecorView 来说, 其测量过程就是在 onMeasure() 方法中进行的, 并且根据前边的结论可知, 我们为 onMeasure() 方法传递的参数分别为:
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY). 下面我们再来看看 onMeasure() 方法的具体过程. 下面是 FrameLayout 类中 onMeasure() 方法的源码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); int maxHeight = 0; int maxWidth = 0; // Find rightmost and bottommost child for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); } } // Account for padding too maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight; maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom; // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), resolveSize(maxHeight, heightMeasureSpec)); }
经过前面的分析, 我们为该方法传递的参数分别是:
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY).
上述源码中第9~16行, 其实是在循环遍历该 DecorView 中所包含的所有子 View, 只要系统允许对所有子 View 都进行测量, 或者虽然系统不允许测量全部子 View, 但只要某个子 View 的 visibility 不为 GONE, 就会对那个子 View 执行 measureChildWithMargins() 方法. 在该方法中, 将我们为 onMeasure() 方法的两个参数 widthMeasureSpec 和 heightMeasureSpec所传递的数值 (数值见这里的结论) 又传递给了这个 measureChildWithMargins() 方法. 根据方法名称可以猜测, measureChildWithMargins() 方法的作用是将子 View 四周的 margin 值都考虑在内, 然后对他进行测量. 我们看下这个方法的源码:
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * -----this view 指的是要进行宽度测量的子View(即:child)的父亲, 而不是子View * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * -----this view 指的是要进行高度测量的子View(即:child)的父亲, 而不是子View * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 步骤1: 获取当前子 View 的 LayoutParams (也叫作: 布局参数), 主要是指当前子 View 的 width 和 height. final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 步骤2: // 根据当前 ViewGroup 的父亲(也就是当前子 View 的爷爷) 对当前 ViewGroup 宽度的限制条件, // 以及当前子 View 的布局参数, 计算出该 ViewGroup 对当前子 View 在宽度方面的限制条件. final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); // 根据当前 ViewGroup 的父亲(也就是当前子 View 的爷爷) 对当前 ViewGroup 高度的限制条件, // 以及当前子 View 的布局参数, 计算出该 ViewGroup 对当前子 View 在高度方面的限制条件. final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); + // 步骤3: 将步骤2中得到的当前子 View 宽高各自的 measureSpec 作为参数, 来调用其 measure()方法. child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
上述代码一共就四句话, 其中第27和33行中的 getChildMeasureSpec() 方法的作用是: 根据当前 ViewGroup 的父亲(也就是当前子 View 的爷爷) 对当前 ViewGroup 宽度或高度的限制条件, 以及当前子 View 的布局参数, 计算出该 ViewGroup 对当前子 View 在宽度或高度方面的限制条件. 而在执行该方法时, 将我们为 onMeasure() 方法传递的两个参数的数值
(手机屏幕的宽度 + MeasureSpec.EXACTLY) 和 (手机屏幕的高度 + MeasureSpec.EXACTLY)
又原封不动地分别传给了第27行测量 childWidthMeasureSpec 的 getChildMeasureSpec() 方法, 以及第33行测量 childHeightMeasureSpec 的 getChildMeasureSpec() 方法. 我们就只分析 childWidthMeasureSpec 的计算吧, childHeightMeasureSpec 的计算是类似的. 我们查看 getChildMeasureSpec() 方法的源码如下:
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
前面我们提到过, getChildMeasureSpec() 方法的作用是: 根据当前 ViewGroup 的父亲(也就是该 ViewGroup 中当前正在进行测量的那个子 View 的爷爷) 对当前 ViewGroup 宽度或高度的限制条件, 以及该 子 View 的布局参数, 计算出该 ViewGroup 对该子 View 在宽度或高度方面的限制条件. 而上面的源码就是对这一结论的具体描述, 由于 specMode 有3种取值, childDimension 也有3种取值, 二者之间互不影响, 所以对他们进行排列组合可知, 一共有9种情况需要讨论, 而任玉刚主席在他的《Android开发艺术探索》这本书中已经将该方法中的这9种情况使用了一个表格进行了一番总结, 使得该方法的逻辑变得更直观. 我这里就直接借用这个表格吧, 如下图所示:
对照上图可知, 在计算 DecorView 宽度的 MeasureSpec 时, 传递的 parentSpecMode 是: MeasureSpec.EXACTLY, parentSpecSize 是手机屏幕的宽度, 而我们在前面也对 DecorView 的布局参数得出过这样的结论: 系统为 DecorView 设定的 width 和 height 的数值均为 LayoutParams.MATCH_PARENT (如果忘了, 请看这里). 所以, 对照上边的表格, 我们就可以知道, DecorView 的父亲测量 DecorView 的宽度和高度上所做出的限制都是: parentSpecMode = EXACTLY . 而 DecorView 的宽和高都是 match_parent (即: 其宽和高的 childLayoutParams 都等于 match_parent ). 所以根据上面的表格, 我们可以知道, DecorView 宽和高的 measureSpec 的值都是 EXACTLY + parentSize. 而 parentSize 对应宽和高来说, 分别是手机屏幕的宽度和高度. 所以, 我们可以得出如下结论:
DecorView 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
DecorView 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
上述等式中的 widthMeasureSpec 和 heightMeasureSpec 的数值, 正是我们前面还未分析完的 FrameLayout 类的 measureChildWithMargins() 方法中的变量 childWidthMeasureSpec 和 childHeightMeasureSpec 的具体值. 我们回到该方法的源码, 继续来分析 DecorView 的测量过程. 我们接下来要分析该方法中的最后一句代码了:
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
而对于 DecorView 来说, 这句代码其实等效于:
DecorView内的子View.measure(EXACTLY + 手机屏幕的宽度, EXACTLY + 手机屏幕的高度);
而我们先前介绍过, DecorView 中只有一个子 View, 是一个竖直方向的 LinearLayout (也就是这个图中的②), 在该 LinearLayout 中包含了两个子View, 一个是默认不会加载的ActionBar (体积为0, 不占据空间. 见这个图中的③), 另一个是 id 为 android.R.id.content 的 FrameLayout (见这个图中的④), 我们完全可以认为该 LinearLayout 中只包含后者这一个子 View. 于是接下来我们就要来分析这个 LinearLayout 的 measure() 方法了. 其实, LinearLayout 的 measure() 方法的分析思路和过程, 与我们分析 DecorView 这个 FrameLayout 的 measure() 方法是相同的, 也和他们共同的父类 ViewGroup 的分析思路相同, 都是遍历他们内部的每一个 visibility 不为 GONE 的子 View, 并逐一进行测量. 所以, 我这里就不再具体分析. 直接给出如下结论:
id 为 android.R.id.content 的 FrameLayout 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
id 为 android.R.id.content 的 FrameLayout 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
而这个id为 android.R.id.content 的 FrameLayout (见这个图中的④) 内只有一个子View, 也就是我们在xml布局文件中设置的 LinearLayout (见这个图中的⑤) , 并且我们设置其 layout_width 和 layout_height 都是 match_parent, 该 LinearLayout 的各个参数与先前分析的那个 LinearLayout (也就是这个图中的②) 是完全一样的, 所以我们也可直接得出如下结论:
xml布局文件中的 LinearLayout 的 widthMeasureSpec = EXACTLY + 手机屏幕的宽度
xml布局文件中的 LinearLayout 的 heightMeasureSpec = EXACTLY + 手机屏幕的高度
而在得到上述两个 measureSpec 变量的值后, 接下来, 我们要做的就是, 执行xml布局文件中 LinearLayout (也就是这个图中的②)) 的 measure() 方法, 并将上述结论中的 widthMeasureSpec 和 heightMeasureSpec 作为参数传入该方法. 而 measure()方法的执行流程, 我们前面已经描述过, 也就是对非 GONE 的子 View 逐一执行该 LinearLayout 的 measureChildWithMargins() 方法. 而 measureChildWithMargins() 方法的执行步骤仍然是:
1. 获取到当前正在测量的子 View 的布局参数 (通常是布局文件中该子 View 的 layout_width 和 layout_height 的值)
2. 根据该 LinearLayout 宽/高的 measureSpec 值和步骤1中的布局参数分别计算出该子 View 宽/高的 measureSpec 值.
(备注: 可以根据前边贴的那张表格截图来分别计算)
3. 调用该子 View 的 measure() 方法, 并将步骤2中计算出的该子 View 宽/高的 measureSpec 值作为参数一起传递给该方法.
而在遍历测量各个子 View 的过程中, 在xml布局文件中先添加的子 View总是先被访问, 所以, 在我们自定义圆的例子中, 会先进行自定义圆的测量, 后进行 Button 的测量.
我们先来分析xml布局文件中的 LinearLayout 对我们自定义圆进行 measureChildWithMargins() 测量的过程吧.
我们按照上述3个步骤来进行.
1. 获取当前正在测量的子 View 的布局参数 (通常是布局文件中该子 View 的 layout_width 和 layout_height 的值).
可以通过布局文件中我们为自定义圆设定的 layout_width 和 layout_width 这两个属性的值来得到该布局参数, 一共有3种可能的取值: dp/px数值, wrap_content, match_parent.
2. 根据该 LinearLayout 在宽和高两方面各自的 measureSpec 值和步骤1中的布局参数分别计算出该自定义圆在宽和高两方面各自的 measureSpec 值.
先看宽度: 我们在前面得出过结论: xml布局文件中的 LinearLayout 在宽度方面的 measureSpec 值等于 (EXACTLY + 手机屏幕的宽度), 再结合步骤1中得到的自定义圆的布局参数, 根据 getChildMeasureSpec() 方法或者直接由我们前边贴出的那个表格, 就能得到该自定义圆在宽度方面的 measureSpec 值了. 该自定义圆在高度方面的 measureSpec 值也同理可得. 为便于具体分析我们这个自定义圆的例子, 我这里把对 getChildMeasureSpec() 方法内部流程的那张总结表格再次贴出来:
在我们这个例子中, 自定义圆在宽度方面的 parentSpecMode (也就是 xml布局文件中 LinearLayout 的 widthSpecMode ) = EXACTLY, parentSpecSize (也就是 xml布局文件中 LinearLayout 的 widthSpecSize ) = 手机屏幕的宽度, 而宽度方面的布局参数 childLayoutParams (也就是布局文件中为自定义圆设置的 layout_width ) 的数值由我们自己设定. 根据该表格中标红的那一列可知,
自定义圆在宽度方面的 measureSpec (通常也称作 widthMeasureSpec) 可以分为如下3种情况:
如果我们将自定义圆的 layout_width 设置为具体的 dp/px 数值, 那么自定义圆的 widthMeasureSpec 就是
EXACTLY + childSize (也就是我们设置的 dp/px 数值).
如果我们将自定义圆的 layout_width 设置为 match_parent, 那么自定义圆的 widthMeasureSpec 就是
EXACTLY + parentSize (即: 手机屏幕的宽度).
如果我们将自定义圆的 layout_width 设置为 wrap_content, 那么自定义圆的 widthMeasureSpec 就是
AT_MOST + parentSize (即: 手机屏幕的宽度).
从上述3种情况, 我们可以得出自定义圆在宽度方面的如下结论:
如果我们为自定义圆的 layout_width 设置的是 wrap_content, 那么该自定义圆的 widthMeasureSpec 中的 specMode (也就是 widthSpecMode) = AT_MOST.
如果我们自定义圆的 widthSpecMode = AT_MOST, 那么该圆的 layout_width 数值一定是 wrap_content.
总结来说, 我们自定义圆的 layout_width = wrap_content 与 widthSpecMode = AT_MOST 是一一对应关系.
同理可知, 自定义圆在高度方面的 measureSpec (通常也称作 heightMeasureSpec) 也可以分为如下3种情况:
如果我们将自定义圆的 layout_height 设置为具体的 dp/px 数值, 那么自定义圆的 heightMeasureSpec 就是
EXACTLY + childSize (也就是我们设置的 dp/px 数值).
如果我们将自定义圆的 layout_height 设置为 match_parent, 那么自定义圆的 heightMeasureSpec 就是
EXACTLY + parentSize (即: 手机屏幕的高度).
如果我们将自定义圆的 layout_height 设置为 wrap_content, 那么自定义圆的 heightMeasureSpec 就是
AT_MOST + parentSize (即: 手机屏幕的高度).
同样地, 从上述3种情况, 我们也可以得出自定义圆在高度方面的如下结论:
如果我们为自定义圆的 layout_height 设置的是 wrap_content, 那么该自定义圆的 heightMeasureSpec 中的 specMode (也就是 heightSpecMode) = AT_MOST.
如果我们自定义圆的 heightSpecMode = AT_MOST, 那么该圆的 layout_height 数值一定是 wrap_content.
总结来说, 我们自定义圆的 layout_height = wrap_content 与 heightSpecMode = AT_MOST 是一一对应关系.
我们将前面得出的自定义圆在 宽度方面的结论 和 高度方面的结论 归纳一下, 得出如下结论:
自定义圆的宽/高的结论:
对于自定义圆的宽度和高度中的任意一个尺寸来说, 只要在xml布局文件中设置的是 wrap_content, 那么该尺寸的 specMode 就一定是 AT_MOST. 相反, 只要某个尺寸的 specMode 是 AT_MOST, 那么该尺寸的数值就一定是 wrap_content.
3. 调用自定义圆的 measure() 方法, 并将步骤2中计算出的 widthMeasureSpec 和 heightMeasureSpec 作为参数一起传递给该方法.
自定义圆的类名是 CustomCircleView, 该类中没有 measure() 方法, 所以会去调用其父类(即: View类)中的 measure() 方法. 前面我们介绍过, View类中的 measure()方法的核心步骤是 onMeasure()方法, 该方法可以由用户来重写以提供自定义的测量过程. 而由于我们的 CustomCircleView 类中没有重写 onMeasure() 方法, 所以还是会调用其父类 (即: View 类) 中的该方法, 并且将步骤2中计算出的 widthMeasureSpec 和 heightMeasureSpec 作为参数传递给该方法. 我们来看看该方法的源码:
/** * <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 overriden 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)); }
该方法会直接调用 setMeasuredDimension() 方法. 而 setMeasuredDimension() 方法的作用是设置该控件的”测量宽高” (当然, 你可以将”测量宽高”简单理解为就是该控件最终呈现出来的宽高. 这种观点在一般情况下都是正确的, 只有在极少数特殊情况下才是错误的. 此处不详细介绍). 那么, 也就是说, setMeasuredDimension() 方法中传入的两个 getDefaultSize() 方法的返回值将分别是该控件的测量宽度 (通常用 measuredWidth 表示) 和 测量高度 (通常用 measuredHeight 表示). 而上边源码中, 我们直接将 onMeasure() 方法的 widthMeasureSpec 和 heightMeasureSpec 这两个参数 (他们都是在步骤2中计算的 ) 原封不动地分别传递给该方法内的两个 getDefaultSize() 方法. 我们就来看看这个 getDefaultSize() 方法的源码:
/** * 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 size, int measureSpec) { int result = size; // 计算父容器对该子控件的测量模式, 表示父容器对该子控件在测量尺寸(宽/高)方面所施加的限制性要求, // 一共有3种可能的要求: // ① UNSPECIFIED: 表示子控件想要多大尺寸, 父容器就给他分配多大尺寸 // ② AT_MOST: 表示子控件的尺寸只能小于或等于某个数值, 不能大于该数值 // ③ EXACTLY: 表示子控件的尺寸必须等于某个数值 int specMode = MeasureSpec.getMode(measureSpec); // 计算在父容器为该子控件的尺寸方面施加限制性要求的情况下, 父容器强制为该子控件分配的某个尺寸(宽/高)值. int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { // 表示子控件想要多大尺寸, 父容器就给他分配多大尺寸 case MeasureSpec.UNSPECIFIED: // 子控件想要的尺寸大小是 size, 所以父容器就给他分配了数值大小为 size 的尺寸. result = size; break; // 表示子控件的尺寸只能小于或等于某个数值, 不能大于该数值 case MeasureSpec.AT_MOST: // 表示子控件的尺寸必须等于某个数值 case MeasureSpec.EXACTLY: // 这里实际上执行的是一个强制赋值的操作, 也就相当于是 EXACTLY 的情况, 而并没有去考虑 AT_MOST 的情况. // 换句话说, AT_MOST 的情况被按照 EXACTLY 来执行了. 所以, 其实这里更合理的做法应该是, // 把 AT_MOST 和 EXACTLY 的情况分开处理. result = specSize; break; } return result; }
从该方法的名称就可以看出, 该方法的作用是, 获取系统分配给该子控件的默认尺寸 (默认宽度或默认高度) . 源码中的注释已经非常详细了, 所以就不再对源码本身做出解释了. 我们就以获取默认宽度为例来进行分析. 由于在宽度方面, 我们为该方法第二个参数 measureSpec 传入的数值其实就是在步骤2中计算出的 widthMeasureSpec 的数值, 在步骤2中我们知道, widthMeasureSpec 一共有两种可能的取值, 一种是 EXACTLY + dp/px 数值, 另一种是 EXACTLY + 手机屏幕的宽度. 至于取哪个值, 取决于我们在 xml 布局文件中为自定义圆的 layout_width 属性所赋的值. 但有一点是确定的, 即: widthMeasureSpec 中的 specMode 一定是 EXACTLY, 而只是 specSize 需要根据情况而定. 而再看 getDefaultSize() 的源码可知, EXACTLY 对应的结果是 specSize. 所以 getDefaultSize() 方法的返回值一定是 specSize, 即: 我们自定义圆的默认宽度一定是 specSize, 而 specSize 的数值是根据我们在xml布局文件中为 layout_width 属性设定的数值而定的, 如果我们为 layout_width 设置为具体的 dp/px 数值, 则 specSize 的值 (也即: 我们自定义圆的默认宽度) 就等于该 dp/px 值, 而如果为 layout_width 设置的是 wrap_content 或 match_parent, 则 specSize 的值 (也即: 我们自定义圆的默认宽度) 就是手机屏幕的宽度. 即:
我们自定义圆的默认宽度是:
如果为自定义圆的 layout_width 设置的是 dp/px 数值, 则自定义圆的默认宽度 = 该 dp/px 值.
如果为自定义圆的 layout_width 设置的是 wrap_content 或 match_parent, 则自定义圆的默认宽度 =
手机屏幕的宽度
用表格表示就是:
同理, 我们也可以得出我们自定义圆的默认高度:
我们自定义圆的默认高度是:
如果为自定义圆的 layout_height 设置的是 dp/px 数值, 则自定义圆的默认高度 = 该 dp/px 值.
如果为自定义圆的 layout_height 设置的是 wrap_content 或 match_parent, 则自定义圆的默认高度 = 手机屏幕的高度
用表格表示就是:
分析完系统为控件宽和高所提供的默认尺寸的获取方法以后, 我们再回头去看 View 类中的 onMeasure() 方法, 我们看到, 该方法直接调用了 setMeasuredDimension() 方法, 并且把系统为控件提供的默认宽和高直接作为该方法的参数, 而前面也提到, setMeasuredDimension() 方法中的两个参数分别表示控件的 “测量宽度”和 “测量高度”, 并且我们还提到过, 一个控件的 “测量宽度”/”测量高度”一般来说就是它最终在屏幕上呈现出来的实际宽度/高度. 所以, onMeasure() 方法的含义其实是, 将系统为我们自定义圆提供的默认宽高, 直接作为他们实际显示在屏幕上宽高. 那么我们先前对自定义圆的默认宽度和默认高度分别得出的结论 (具体见: 默认宽度的结论 和 默认高度的结论 ) 也同样分别适用于该自定义圆的实际宽度和实际高度. 那么我们同样用表格的形式来总结一下吧:
从这两个表格中, 我们已经很清楚地知道了, 只要我们为自定义圆的 layout_width 或 layout_height 设置为 wrap_content , 那么该自定义圆的宽度或高度就必定充满整个屏幕的宽或高. 而如果设置为 dp/px 值, 只要该值不超过屏幕的宽/高, 那么自定义圆的宽/高就等于我们设定的这个 dp/px 值, 而不是充满整个屏幕的宽/高, 这样我们也就有可能看到 Button 了.
还记得我们曾在本文前边 标记0 处提出了一系列问题吗? 通过我们上述的分析, 想必这些问题都已经有了答案吧. 总结一下这些问题所提到的那个 bug 产生的根源, 就是在 onMeasure() 方法中, 我们对控件在 layout_width 或 layout_height 被设置为 wrap_content 时的情况没有提供我们自己的处理方式, 导致被系统按照 match_parent 的情况来处理了. 而如果看过系统原生控件源码的话, 就会发现, 这些控件都会在 onMeasure() 方法中对 wrap_content 的情况进行单独处理的.
既然我们已经知道了该 bug 产生的根源, 那么我们今后在自定义控件中, 如何避免该 bug 的产生呢? 显然, 我们需要重写 onMeasure() 方法, 并在该方法内对 wrap_content 的情况进行单独的处理. 但是, 该方法只提供了 widthMeasureSpec 和 heightMeasureSpec 这两个参数, 根据这两个参数, 我们还能计算出宽和高各自的 specMode 和 sepcSize, 我们无法进行更进一步的计算了, 但是只是得到 specMode 和 sepcSize 的值, 与我们要处理的 wrap_content 之间又有什么关系呢? 我们到底该如何处理 wrap_content 呢?
其实, 我们在前边曾提到过 自定义圆的宽/高的结论, 根据这个结论, 我们就能知道, 单独处理 wrap_content 的情况, 其实也就是要单独处理 specMode 为 AT_MOST 的情况. 所以我们就有了解决思路, 我们只需对宽高的 specMode 各自为 AT_MOST 时, 分别为宽/高设置一个默认的 dp/px 数值即可.
根据这个思路, 我们将前边没有重写 onMeasure() 方法的那个自定义圆的代码, 再重新整理一遍, 添加对 wrap_content 情况的处理, 代码如下:
/** * 自定义圆 */ public class CustomCircleView extends View { // 默认的圆心x坐标 private static final float DEFAULT_CENTER_X = 50; // 默认的圆心y坐标 private static final float DEFAULT_CENTER_Y = 50; // 默认的半径 private static final float DEFAULT_RADIUS = 50; // 默认宽度 private static final int DEFAULT_WIDTH = ((int)(DEFAULT_RADIUS + 1)) << 1; // 默认高度 private static final int DEFAULT_HEIGHT = DEFAULT_WIDTH; private Paint mPaint; public CustomCircleView(Context context) { super(context); init(); } public CustomCircleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomCircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { if (mPaint == null) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.parseColor("#0DC65B")); mPaint.setStyle(Paint.Style.FILL); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); // 处理宽高都为 wrap_content 的情况 if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } // 处理宽为 wrap_content 的情况 else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, heightSpecSize); } // 处理高为 wrap_content 的情况 else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, DEFAULT_HEIGHT); } } @Override protected void onDraw(Canvas canvas) { canvas.drawCircle(DEFAULT_CENTER_X, DEFAULT_CENTER_Y, DEFAULT_RADIUS, mPaint); } }
上面是我们自定义圆的代码, 我们将xml布局文件中该圆的 layout_width 和 layout_height 都设置为 wrap_content, 然后运行看看实际效果:
从图中可以看出, wrap_content 的情况已经被我们成功处理了. 当然, 这里的自定义圆的代码还可以进一步完善, 比如: 可以增加自定义属性, 还可以在 onDraw() 方法中处理为控件设置的 padding 等, 这里就不做介绍了. 点击 这里 可以获取该demo例子的代码.
另外说明一下, 如果我们自定义的控件是直接继承自 Android SDK 中已有的控件 (比如: TextView, Button, LinearLayout, RelativeLayout 等), 由于这些控件自身已经处理了 wrap_content 的情况, 所以我们就无需再进行处理了. 但如果我们自定义的是继承自 View 类或 ViewGroup 类的控件, 那么我们就必须要处理 wrap_content 的情况.
好了, 本文的介绍就到此为止吧.
参考文章或书籍:
自定义控件其实很简单7/12《Android开发艺术探索》第4章 View的工作原理.
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories