自定义View Android最易懂的Measure过程
测量 View 就是测量一个矩形
透过另一个视角来观察,所有的 Widget,我们使用的小控件都是Widget。如果TextView和Buttton等
因此,自定义 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 种取值可能。
android:layout_height="wrap_content" //View 本身的内容决定高度
android:layout_height="match_parent" //与父视图等高
android:layout_height="fill_parent" //与父视图等高
android:layout_height="100dip" //精确设置高度值为 100dip
我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。
一
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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。我们再换一种情况。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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>
三
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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>
四
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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 中一个静态类
/**
* MeasureSpec类的源码分析
**/
public class MeasureSpec {
// 进位大小 = 2的30次方
// int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
private static final int MODE_SHIFT = 30;
// 运算遮罩:0x3为16进制,10进制为3,二进制为11
// 3向左进位30 = 11 00000000000(11后跟30个0)
// 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
// 通过高2位
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
**/
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
// 设计目的:使用一个32位的二进制数,其中:第32和第31位代表测量模式(mode)、后30位代表测量大小(size)
}
/**
* getMode()方法
* 作用:通过measureSpec获得测量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:测量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
//原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
}
/**
* getSize方法
* 作用:通过measureSpec获得测量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
}
}
MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。
实际使用
/**
* MeasureSpec类的具体使用
**/
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
结论:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:
- 子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
下面,我们来看getChildMeasureSpec()的源码分析:
/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
// 当父View的模式为EXACITY时,父view强加给子View确切的值
//一般是父View设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子View的LayoutParams>0,即有确切的值
if (childDimension >= 0) {
//子View大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子View的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父View的模式为AT_MOST时,父view强加给子View一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父View的模式为UNSPECIFIED时,父容器不对View有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父View为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
MeasureSpec.UNSPECIFIED
子元素告诉父容器它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。
MeasureSpec.EXACTLY
此模式说明可以给子元素一个精确的数值
MeasureSpec.AT_MOST
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。
此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是子视图不能超过ViewGroup 提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。
自定义 View
我的目标是定义一个文本框,中间显示黑色文字,背景色为绿色。
我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TestView">
<attr name="android:text" format="string" />
<attr name="android:textSize" format="dimension"/>
</declare-styleable>
</resources>
TestView.java
public class TestView extends View {
private int mTextSize;
private 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.setAntiAlias(true);
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"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_margin="20dp"
android:layout_height="match_parent">
<com.example.improve.TestView
android:layout_width="100dp"
android:layout_height="100dp"
android:text="test" />
</RelativeLayout>
我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。
<com.example.improve.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.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="test" />
效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。
<com.example.improve.TestView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="test"/>
宽度正常,高度却和 ViewGroup 一样了。
再看一种情况
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp">
<com.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>
这次可以看到,宽高都和 ViewGroup 一致了。
但是,这不是我想要的啊!
wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。
TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
我们的思路可以如下:
- 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
- 对于 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 = Math.min(contentW, widthSize);
}
}
if (heightMode == MeasureSpec.AT_MOST) {
if (!TextUtils.isEmpty(mText)) {
contentH = mTextSize;
contentH += getPaddingTop() + getPaddingBottom();
resultH = Math.min(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;
Paint.FontMetrics metrics = mPaint.getFontMetrics();
cy += metrics.descent;
canvas.drawColor(Color.GREEN);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText, cx, cy, mPaint);
}
代码并不难,我们可以做验证。
<com.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:text="test"
android:textSize="24sp" />
在 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 测量成功后,就需要布局了。自定义 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 布局文件中进行测试。
<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.example.improve.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:background="#00ff40"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test" />
</com.example.improve.TestViewGroup>
再试验一下给 TestViewGroup 加上固定宽高。
<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="350dp"
android:layout_height="400dp"
android:background="#c3c3c3">
<com.example.improve.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:background="#00ff40"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test" />
</com.example.improve.TestViewGroup>
结果如下:
自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。
但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。
TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义方法。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 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);
}
}
}
/**
* 分析2:measureChild()
* 作用:a. 计算单个子View的MeasureSpec
* b. 测量每个子View最后的宽 / 高:调用子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();
// 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);
// 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
// 下面的流程即类似单一View的过程,此处不作过多描述
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);
我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。
/**
* 源码分析:measure()
* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
* 作用:基本测量逻辑的判断
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算视图大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//mMinWidth = android:minWidth属性所指定的值;
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight()同理
/**
* 分析2:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍
/**
* 分析3:getDefaultSize()
* 作用:根据View宽/高的测量规格计算View的宽/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 参数说明:
// size:提供的默认大小
// measureSpec:宽/高的测量规格(含模式 & 测量大小)
// 设置默认大小
int result = size;
// 获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的宽/高值
return result;
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度
// 若无原始宽度,则为0;
// 注:BitmapDrawable有原始宽度,而ShapeDrawable没有
最后,我们在看看测量的流程图
Activity 中的道,最顶层的那个 View?
道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》
我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?
从 setContentView 说起
我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
public Window getWindow() {
return mWindow;
}
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.policy.PhoneWindow, which you should instantiate when needing a
* Window. Eventually that class will be refactored and a factory method
* added for creating Window instances without knowing about a particular
* implementation.
*/
public abstract class Window {
}
public class PhoneWindow extends Window implements MenuBuilder.Callback {
}
可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。
PhoneWindow.java
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。
它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
} else {
mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
}
}
}
代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
}
我们回到 generate() 方法
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
WindowManager.LayoutParams params = getAttributes();
// Inflate the window decor.
// Embedded, so no decoration is needed.
layoutResource = com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
mDecor.finishChanging();
return contentParent;
}
原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。
总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。 mContentParent加载Activity 通过 setContentView 传递进来的 View,所以整个结构呼之欲出。
注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。
DecorView 才是 Activity 中整个控件树的根。
谁测绘了顶级 View ?
既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?
我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?
我们再回到 PhoneWindow 的 setContentView() 中来。
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我们看看 mContentParent.addView(view, params) 的时候发生了什么。
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
它调用了 requestLayout(),正好要探索它,所以说 setContentView 之后,控件树会进行一次测绘。不过这里是结论,其过程需要我们来验证。
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA