长谈:关于 View Measure 测量机制,让我一次把话说完
Posted frank909
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了长谈:关于 View Measure 测量机制,让我一次把话说完相关的知识,希望对你有一定的参考价值。
《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。张三丰又慢吞吞使了一遍,问他记住多少,张无忌说只记得几招了。张三丰最后又示范了一遍,张无忌想了想说,这次全忘光了。张三丰很满意,于是放心让张无忌与八臂神剑去比试。
首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想放弃,想放弃的原因不是我自己没有弄清楚,而是觉得自己叙事脉络已经紊乱了,感觉无法让读者整明白,怕把读者带到沟里面去,怕自己让人觉得罗嗦废话。但最后,我决定还是坚持下去,因为在反复纠结 –> 不甘 –> 探索 –> 论证 –> 质疑的过程循环中,我完成了对自己的升华,弄明白长久以来的一些困惑。所以,此文最大的目的是给自己作为一些学习记录,如果有幸帮助你解决一些困惑,那么我心宽慰。如果有错的地方,也欢迎指出批评。
如果你有这样的困扰:
1. 一个 View 的 parent 一定是 ViewGroup 吗?
Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。
Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。
在 xml 中设置一个 View 的属性 layout_width 为 wrap_content 或者 match_parent 而不是具体数值 50dp 时,为什么 view 也有正常的尺寸。
你或多或者知道 Android 测量时的 3 种布局模式:MeasureSpec.EXACTLY、Measure.AT_MOST、Measure.UNSPECIFIED。但你不大能够把握它们。
你不但对自定义 View 没有问题,对于自定义 ViewGroup 也不在话下,你明白 Android 给出的 3 种测量模式的含义,但是你还是没有来得及去思考,3 种测量模式本身是什么。
你也许没有想过 Activity 最外层的 View 是什么。
你也许知道 Activity 最外层的 View 叫做 DecorView。明白它与 PhoneWindow 及 Activity.setContentView() 的联系。但你不知道谁对 DecorView 进行了尺寸测量。
好了,文章正式开始。请深吸一口气,take it easy!
无法忽视的自定义 View 话题
Android 应用层开发绕不开自定义 View 这个话题,在 Android 中官方称之为 Widget,所以本文中的 View 其实与 Widget也就一个意思。虽然现在 Github 上有形形色色的开源库供大家使用,但是作为一名有想法的开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,如果现有的 Widget 无法完成任务,那么我们就应该想到要自定义一个 View 了。
我们或多或少知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
- 测量 onMeasure()
- 布局 onLayout()
- 绘制 onDraw()
没有办法说这三个阶段,那个阶段最重要,只是相对而言,测量阶段对于大多开发者而言难度相对其它两个要大,处理的细节也要多得多,自定义一个 View,正确的测量是第一步,正因为如此今天本文的主题就是讨论 View 中的测量机制和细节。
测量 View 就是测量一个矩形
得益于人们的想象力,Android 系统平台上出现了各种各样的 View。有 Button、TextView、ListView 等系统自带的组件,也有更多开发者自定义的 View。
上面是 Android 系统自带的 Widget 表现,它们用来完成不同功能的交互与效果展示,但对于开发者而言,上面的界面还有这样的一面。
透过另一个视角来观察,所有的 Widget
世界万物都有某些运行的规则,或者是突破不了的樊篱。对于一个 View 而言,它本质上就是一个矩形,一块四方的区域,铺开一张画布,然后利用所有的资源,在现有的规则之下天马行空。
因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!
既然是矩形,那么它肯定有明确的宽高和位置坐标。宽高是在测量阶段得出,然后在布局阶段,根据实际需要确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,它是画家,这个我们很放心。
打个比方,政府做城市规划时,房地产商们告诉政府他们希望的用地面积,政府综合政策和用地面积的实际情况,给地产商划分土地面积,地图上就是一个个圈圈。地产商们拿到明确的地域范围信息后,在规定好的区域建造自己的高楼或者大厦。而自定义 View 就是拿到这个类似政府规划的区域范围参数,只不过现实世界中,政府规划给地产商的土地不一定是四四方方的矩形,但是在 Android 中 View 拿到的区域一定是矩形。
好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。
View 设置尺寸的基本方法
接下来的过程,我将会用一系列比较细致的实验来说明问题,觉得罗嗦无聊的同学可以直接跳过这一小节。
我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
这样屏幕上就出现了一个按钮。
我们再把宽高固定。
<Button
android:layout_width="200dp"
android:layout_height="50dp"
android:text="test"/>
再换一种情况,将按钮的宽度由父容器决定
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="test"/>
上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。
- match_parent 代表这个维度上的值与父窗口一样
- wrap_content 表示这个维度上的值由 View 本身的内容所决定
- 具体数值如 5dp 表示这个维度上 View 给出了精确的值。
实验1
我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。
实验2
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
实验3
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
实验4
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="1000dp"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园问光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。
Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。
山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。
那么,它们的协议是什么?
View 和 ViewGroup 之间的测量协议 MeasureSpec
我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。
public class TestView extends View {
public TestView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。
它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。
MeasureSpec
MeasureSpec 是 View.java 中一个静态类
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;
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);
}
}
......
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
......
}
MeasureSpec 的代码并不是很多,它有最重要的三个静态常量和三个最重要的静态方法。
MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST
MeasureSpec.makeMeasureSpec()
MeasureSpec.getMode()
MeasureSpec.getSize()
MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。
通过 makeMeasureSpec() 方法将 Mode 和 Size 组合成一个 measureSpec 数值。
而通过 getMode() 和 getSize() 却可以逆向地将一个 measureSpec 数值解析出它的 Mode 和 Size。
下面讲解 MeasureSpec 的 3 种测量模式。
MeasureSpec.UNSPECIFIED
此种模式表示无限制,子元素告诉父容器它希望它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。
MeasureSpec.EXACTLY
此模式说明可以给子元素一个精确的数值。
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="test"/>
当 layout_width 或者 layout_height 的取值为 match_parent 或者 明确的数值如 100dp 时,表明这个维度上的测量模式就是 MeasureSpec.EXACTLY。为什么 match_parent 也有精确的值呢?我们可以合理推断一下,子 View 希望和 父 ViewGroup 一样的宽或者高,对于一个 ViewGroup 而言它显然是可以决定自己的宽高的,所以当它的子 View 提出 match_parent 的要求时,它就可以将自己的宽高值设置下去。
MeasureSpec.AT_MOST
此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是你不能超过我能提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。
了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。
自定义 View
我的目标是定义一个文本框,中间显示黑色文字,背景色为红色。
我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TestView">
<attr name="android:text" />
<attr name="android:textSize" />
</declare-styleable>
</resources>
TestView.java
public class TestView extends View {
private int mTextSize;
TextPaint mPaint;
private String mText;
public TestView(Context context) {
this(context,null);
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
mText = ta.getString(R.styleable.TestView_android_text);
mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
ta.recycle();
mPaint = new TextPaint();
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
canvas.drawColor(Color.RED);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText,cx,cy,mPaint);
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.frank.measuredemo.MainActivity">
<com.frank.measuredemo.TestView
android:layout_width="100dp"
android:layout_height="100dp"
android:text="test"/>
</RelativeLayout>
效果
我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。
<com.frank.measuredemo.TestView
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="test"/>
将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。
我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。
那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?
我们继续测试观察。
<com.frank.measuredemo.TestView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="test"/>
效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。
<com.frank.measuredemo.TestView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="test"/>
宽度正常,高度却和 ViewGroup 一样了。
再看一种情况
<com.frank.measuredemo.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
这次可以看到,宽高都和 ViewGroup 一致了。
但是,这不是我想要的啊!
wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。
TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
我们的思路可以如下:
1. 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
2. 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
int resultW = widthSize;
int resultH = heightSize;
int contentW = 0;
int contentH = 0;
/**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
* 建议数值
* */
if ( widthMode == MeasureSpec.AT_MOST ) {
if (!TextUtils.isEmpty(mText)){
contentW = (int) mPaint.measureText(mText);
contentW += getPaddingLeft() + getPaddingRight();
resultW = contentW < widthSize ? contentW : widthSize;
}
}
if ( heightMode == MeasureSpec.AT_MOST ) {
if (!TextUtils.isEmpty(mText)){
contentH = mTextSize;
contentH += getPaddingTop() + getPaddingBottom();
resultH = contentH < widthSize ? contentH : heightSize;
}
}
//一定要设置这个函数,不然会报错
setMeasuredDimension(resultW,resultH);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
metrics = mPaint.getFontMetrics();
cy += metrics.descent;
canvas.drawColor(Color.RED);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText,cx,cy,mPaint);
}
代码并不难,我们可以做验证。
<com.frank.measuredemo.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="24sp"
android:text="test"/>
可以看到这才是我们想要的效果。它现在完成了对 MeasureSpec.AT_MOST 模式的适配。我们再验证一下另外一种情况
<com.frank.measuredemo.TestView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="48sp"
android:text="test"/>
在 MeasureSpec.EXACTLY 模式下同样没有问题。
现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。
但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。
View 和 ViewGroup,鸡生蛋,蛋生鸡的关系
ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?
自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,如下图:
前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?
我们还是需要仔细讨论。
- 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
- 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算这个维度上的尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是在一个维度上用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
- 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,因为要计算子元素的尺寸,所以如何准确得到子元素的尺寸也是至关重要的事情。好在 Android 提供了现成的 API。
- 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。但本篇文章不是讨论布局技巧的,只是告诉大家布局其实相对而言更简单一点,无非是确定好子元素的坐标然后进行布局。
接下来,我们就可以具体编码了。
public class TestViewGroup extends ViewGroup {
public TestViewGroup(Context context) {
this(context,null);
}
public TestViewGroup(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
return new MarginLayoutParams(getContext(),attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
int resultW = widthSize;
int resultH = heightSize;
/**计算尺寸的时候要将自身的 padding 考虑进去*/
int contentW = getPaddingLeft() + getPaddingRight();
int contentH = getPaddingTop() + getPaddingBottom();
/**对子元素进行尺寸的测量,这一步必不可少*/
measureChildren(widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
if ( child.getVisibility() == View.GONE ) {
continue;
}
contentW += child.getMeasuredWidth()
+ layoutParams.leftMargin + layoutParams.rightMargin;
contentH += child.getMeasuredHeight()
+ layoutParams.topMargin + layoutParams.bottomMargin;
}
/**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
* ViewGroup 给出的建议数值
* */
if ( widthMode == MeasureSpec.AT_MOST ) {
resultW = contentW < widthSize ? contentW : widthSize;
}
if ( heightMode == MeasureSpec.AT_MOST ) {
resultH = contentH < heightSize ? contentH : heightSize;
}
//一定要设置这个函数,不然会报错
setMeasuredDimension(resultW,resultH);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int topStart = getPaddingTop();
int leftStart = getPaddingLeft();
int childW = 0;
int childH = 0;
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
if ( child.getVisibility() == View.GONE ) {
continue;
}
childW = child.getMeasuredWidth();
childH = child.getMeasuredHeight();
leftStart += layoutParams.leftMargin;
topStart += layoutParams.topMargin;
child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
leftStart += childW + layoutParams.rightMargin;
topStart += childH + layoutParams.bottomMargin;
}
}
}
然后我们将之添加进 xml 布局文件中进行测试。
<com.frank.measuredemo.TestViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.frank.measuredemo.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="24sp"
android:text="test"/>
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="24sp"
android:background="#00ff40"
android:text="test"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test"/>
</com.frank.measuredemo.TestViewGroup>
那么实际效果如何呢?
再试验一下给 TestViewGroup 加上固定宽高。
<com.frank.measuredemo.TestViewGroup
android:layout_width="350dp"
android:layout_height="600dp"
android:background="#c3c3c3">
<com.frank.measuredemo.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="24sp"
android:text="test"/>
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingTop="2dp"
android:textSize="24sp"
android:background="#00ff40"
android:text="test"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test"/>
</com.frank.measuredemo.TestViewGroup>
结果如下:
自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。
但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。
TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义套路。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。
我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践的时候,你都会思考,在思考和实验的过程你才会深刻的理解测量机制的用途。
不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。下面是我自定义 ViewGroup 的一个实例截图。
洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。
问题1:到底是谁在测量 View ?
问题2:到底是什么时候需要测量 View ?
针对问题 1:
我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 measureChildren()。这个方法进行了什么样的处理呢?我们可以去看看。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
硬着头皮再去看 getChildMeasureSpec() 方法的实现。
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
// 父类本身就是 EXACTLY 模式下
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
// child 希望尺寸和 parent 一样以上是关于长谈:关于 View Measure 测量机制,让我一次把话说完的主要内容,如果未能解决你的问题,请参考以下文章
Android View的绘制流程三部曲 —— Measure