Android 页面 Layout 布局
布局过程,就是把界面中的所有控件,用他们的要求的大小,摆放在正确的位置。更通俗的将,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程。
安卓的布局过程
包括两个阶段:测量阶段和布局阶段。
- 测量阶段:从上到下递归地测量每一级,每一个子 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 中的相对坐标。需要把位置和尺寸转化一下。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论