[TOC]
布局
含义
布局过程,就是把界面中的所有控件,用他们的要求的大小,摆放在正确的位置。更通俗的将,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程。
安卓的布局过程
包括两个阶段:测量阶段和布局阶段。
- 测量阶段:从上到下递归地测量每一级,每一个子 View 的尺寸并计算它们的位置;
- 布局阶段:依然是从最顶级的 View 向下递归地把测得的它们的
尺寸
和位置
赋值给它们。
这样每个 View 的尺寸和位置就确定了,布局过程就算完成了。接下来就是绘制规程了,每个 View 会根据这些得到的尺寸和位置来进行自我绘制。
测量过程的调用
当测量过程到来时,一个 View 的
measure()
方法会被它的父节点调用(根节点的 View 不是一个 View 对象)。 通知该 View 进行自我测量,而真正进行测量不是measure()
方法,而是调用自己的onMeasure()
方法。measure
是一个调度方法,它会做一些测量的预处理工作,然后调用onMeasure()
来进行真正的测量。而
onMeasure()
的自我测量包含两部分内容,要看这个 View 是 简单的 View 还是 ViewGroup- 如果它是一个简单的 View, 它做的事情只有一件,测量出自己的尺寸。
- 如果它是一个 ViewGroup,它会先调用所有子 View 的
measure()
方法。让他们都进行自我测量。然后根据这些子 View 自我测量出的尺寸来计算出他们的位置,并且把他们的尺寸和位置保存下来。同时,根据这些子 View 的尺寸和位置,最终计算出自己的尺寸。 也就是说,ViewGroup 的尺寸主要有子 View 的尺寸和位置确定的。
布局截断的调用
一个 View 的 layout(...)
方法会被它的父节点所调用,用于对 View 进行内部布局。和 measure()
方法一样,layout(...)
也只是一个调度方法,实际进行布局的是 onLayout
方法。
onLayout
内部会做两件事。 首先 layout
方法是有参数的,它的父几点在调用它的时候回把之前测量截断保存下来的这个 View 的尺寸和位置通过参数给传进来,而 layout
做的第一件事就是把这个尺寸和位置保存下来。测量阶段是父 View 统一保存所有子 View 的尺寸和位置,而到了布局阶段就是 View 保存自己的尺寸和位置了。
第二件事是它会调用自己的 onLayout
方法,这 onLayout
对自己进行真正的内部布局。
内部布局的意思是,它会调用每一个子 View 的 layout
方法,并且把他们的尺寸和位置作为参数传递给它们。对于简单的 View 来处,它的 onLayout
什么也不用做,就是一个空方法,之所以也要调用,是逻辑的统一。
布局过程的自定义
就是重写 View 的测量过程和布局过程的相关方法,以此来定制自己想要的尺寸和排放效果,
注意: 重写的是
onMeasure
和onLayout
方法,因为measure
和layout
是用来调度的,而真实进行测量的布局的是onMeasure
和onLayout
方法。
具体过程可以分为三类
- 重写
onMeasure
来修改已有的View
的尺寸。 - 重写
onMeasure
来全新计算自定义View
的尺寸。 - 重写
onMeasure
和onLayout
来全新计算自定ViewGroup
的内部布局。1. 重写
onMeasure
来修改已有的View
的尺寸
这一类是对一些已有的 View 进行修改尺寸。例如,修改 ImageView 的尺寸就属于这一类。他已经有自己的尺寸计算算法了,它的 onMeasure
已经正确计算出它的尺寸和位置,你不需要从新进行计算一遍。 只需要根据自己的需要进行相应的调整。做法
- 在重写的
onMeasure
方法中先调用super.onMeasure
方法让它进行一次原有的测量。 - 然后增加代码,计算得到想要的尺寸。通过
getMeasuredWidth
和getMeasuredHeight
获得测得的尺寸,然后重新计算尺寸。 - 保存新尺寸。计算尺寸并不是通过返回值返回给父 View 的,而是通过
setMeasuredDimension
方法把它存在自己内部。所有取的时候也是要用相应的方法来取:getMeasuredWidth
和getMeasuredHeight
。
注意,setMeasuredDimension 保存的是一个测得的尺寸,它会之后通过 layout 方法传进来的那个尺寸未必是相等的。这个 setMeasuredDimension 保存的尺寸是一个 View 对自己尺寸的期望值。然后父 View 会根据这个期望值再去判断,至于最终它是否会同意你这么大,还是要求你再去测量一次,或者是给你指派一个新尺寸,这个由父 View 来决定。最终它会通过 layout 的方法的参数来传给你。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先执行原测量算法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获得原先的测量结果
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
// 利用原先的测量结果计算出新尺寸。
if (measuredWidth > measuredHeight) {
measuredWidth = measuredHeight;
} else {
measuredHeight = measuredWidth;
}
// 保存计算后的尺寸。
setMeasuredDimension(measuredWidth, measuredHeight);
}
2. 重写 onMeasure
来全新计算自定义 View
的尺寸
主要用于自定义 View, 这些 View 的绘制完全自己写的,这时,在尺寸上就没有现有的可作为基准的值修改使用。需要完全自己去计算它的尺寸。这种测量和第一种是有点不一样的:
- 不需要调用父类的
onMeasure
方法,而是完全自己计算尺寸。计算所有内部绘制的他们的间距,边距,得到最后的尺寸即可。 全新计算 View 尺寸,需要保证自己计算出的尺寸满足父布局的限制: 就是
onMeasure
方法的参数,父布局在调用子 view 的measure
方法时,会将父布局对子 View 的限制作为参数传递过来, measure 方法在调用onMeasure
方法时,又会原封不动地将其传递给onMeasure
方法。- 限制是怎么来的?父 View 为什么会对子 View 进行限制?
父 View 把开发者对子 View 的尺寸要求(就是开发者在 xml 文件中这个View写的以
layout_
开头的属性限制,它们使用来设置这个 View 的位置和尺寸的。)进行处理计算之后所得到的更精确的要求。layout_
开头的属性不是给 View 自己看的,而是给它的父 View 看的。也就是说在程序运行显示界面的时候,没一个 ViewGroup 会读取它的子 View 的layout_
开头属性,然后用他们进行处理和计算,得出一个限制。这个限制分为三种:不限制,设置上限,固定值(为了流程的通一,要求子View 计算一遍)。如果不遵循这个限制,就会产生 bug,例如,这个现实是 500,偏要使用400,这时使用者写了match_parent
属性,却发现并没有填充满父布局的可用尺寸,从而属性无效,达不到开发者的要求。 - 子 View 的 onMeasure 方法里面应该怎么做,才能满足父布局的限制。
很简单,在计算完尺寸后,调用一个
resolveSize
来调整计算的数值,返回值就是修正之后的尺寸。然后把修正之后尺寸用setMeasuredDimension
保存起来就行了。- 限制是怎么来的?父 View 为什么会对子 View 进行限制?
父 View 把开发者对子 View 的尺寸要求(就是开发者在 xml 文件中这个View写的以
setMeasuredDimension 方法的处理过程
public static int resolveSize(int size, int measureSpec, int childMeasuredState) {
// 父 View 传过来的宽度和高度限制都是一个压缩数据,包括限制类型和尺寸两部分。可以通过 `MeasureSpec.getMode` 来获取限制类型,`MeasureSpec.getSize` 获得限制尺寸值。
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
// 对于限制上限,取较小的值。
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
// 对于指定值,返回指定尺寸。
case MeasureSpec.EXACTLY:
result = specSize;
break;
// 对与不限制的, 直接返回计算的值,
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
3. 重写 onMeasure
和 onLayout
来全新计算自定 ViewGroup
的内部布局
用于重写 ViewGroup
类型的View.
- 重写
onMeasure
来计算内部元素的尺寸和位置,以及自己的尺寸。分三步:- 调用每个子 View 的
measure
方法,来让你子 View 自我测量。 - 根据子 View 计算出的尺寸,得出子 View 的位置,并保存它们的尺寸和位置。
- 根据子 View 的尺寸和位置计算出自己的尺寸,并用 setMeasuredDimension 方法保存下来。
- 调用每个子 View 的
- 重写
onLayout
来摆放内部元素。
1. 重写 onMeasure
来计算内部元素的尺寸和位置,以及自己的尺寸
第一步:调用每个子 View 的 measure
方法,来让你子 View 自我测量
使用 for 循环调用子 view 的 measure
方法,而这个方法有两个尺寸限制参数,而这个限制并不是现成的,需要根据子 View 的属性设置(xml文件)和自己的可用空间(剩余)限制,计算出来。开发者的要求(属性设置)在地位上要绝对高于可用空间。例如,开发者写了 layout_width="48dp"
那就不用管剩余空间有没有 48dp 了,直接设置子 View 的尺寸是固定 48dp
就好了。即,mode 是 EXACTLY
,width 是 48dp
对应的像素值。
获取子 View 的 layout_
开头的属性,可以使用子 View 的 getLayoutParams()
方法,获得一个 LayoutParams
对象,它包含了 xml 文件里 layout_ 开头的参数的对应值。LayoutParams
对象的 width
和 height
属性值,就对应了 layout_width
和 layout_height
值。它的值可以是 WRAP_CONTENT
或 MATCH_PARENT
这两个常量,或者准换后的具体像素值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int usedWidth, usedHeight;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
ViewGroup.LayoutParams lp = childView.getLayoutParams();
int childWidthSpec;
int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int selfWidthSpacSize = MeasureSpec.getSize(widthMeasureSpec);
switch (lp.width) {
// 如果是填满父布局,则也使用固定尺寸,尺寸是自己的可用宽度/高度。这时候自己的尺寸还没有计算出来。
// 但是有父布局传过来的 `widthMeasureSpec` 和 `heightMeasureSpec` 尺寸限制。这个尺寸限制,
// 虽然不是自己的最终实际尺寸,但依据这个限制,自己可以得到一个可用空间。得出自己最多有多啊到地方给
// 子 View 用。
case MATCH_PARENT:
if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpacSize - usedWidth, EXACTLY);
} else {
// 较新版本的 Android, UNSPECIFIED 的 size 也有自己的作用,后期研究。
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
// 如果开发者在子 view 写的是固定尺寸值,直接设置为 `EXACTLY`。压缩方法使用 MeasureSpec.makeMeasureSpec
break;
// WRAP_CONTENT 虽然没有说父布局没有限制,但包含一个隐含条件,就是不能草果不父布局的可用宽度。
case WRAP_CONTENT:
if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpacSize - usedWidth, MeasureSpec.AT_MOST);
} else {
// 较新版本的 Android, UNSPECIFIED 的 size 也有自己的作用,后期研究。
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
default:
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
break;
}
... // height 同样计算
}
}
第二步:根据子 View 计算出的尺寸,得出子 View 的位置,并保存它们的尺寸和位置。
调用子 View 的 测量方法,就能得出子 View 的尺寸,绝大多数情况,每一个View,它测得的尺寸就是它的最终尺寸,要用到的时候,调用它的 getMeasuredWidth
和 getMeasuredHeight
就行。
为什么要保存这些尺寸?
因为现在是测量阶段,到布局阶段,这些尺寸和位置才会最终传给子 view。有两点注意:
- 并不是所有的 Layout 都要保存子 View 的位置。比如 LinearLayout,它的内容都是横向或纵向一字排开的。那么子 View 的位置就可以在布局阶段通过一个一个的把尺寸累加起来得到。这种就不需要保存位置了。
- 在某些时候,对子 View 进行一次测量是不够的。可能进行多次测量才能得到正确的尺寸和位置。根据自己想要达到的目的,可以增加测量的次数,以及选择性的测量某些View。
第三步:根据子 View 的尺寸和位置计算出自己的尺寸,并用 setMeasuredDimension 方法保存下来
根据子 View 的尺寸和排布,计算出边界,保存即可。
2. 重写 onLayout
来摆放内部元素
重写 onLayout
很简单,只需要调用每一个子 view 的 layout 方法,把 onMeasure 保存的位置和尺寸传进去即可。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View viewChild = getChildAt(i);
viewChild.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
}
}
子 View 的 layout 参数是它在父 View 中的相对坐标。需要把位置和尺寸转化一下。