最近做了几个自定义布局,就想扯扯Android布局的这些内容
Android布局其实只有两个步骤
第一步,测量各个View的大小。
第二步,把View布局到指定的位置。
测量
先记住测量的起点是View的measure()方法,然后在measure()方法调用onMeasure()方法来自己测量。
所以主要牵涉的二个方法,measure()和onMeasure()。
我们先看一下FrameLayout代码。假设FrameLayout要求被自我测量,那么它就会调用onMeasure()。
//FrameLayout.class
//
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
......
//获取子View,告诉他们“你们要调用自己测量了。调用方法measureChildWithMargins()”
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
....
}
}
......
//等到所有的子View测量完毕,就进行设置自己的大小;如果有需要也可能会进行二次测量
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
//ViewGroup.class
//
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
......
//父亲让子类测量时,调用的就是View的measure()。这里传的两个参数就是父类安排子类的大小
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//View.class
//
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (forceLayout || needsLayout) {
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
......
//子类进行自我测量中...
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
}
}
}
这里的FrameLayout是继承了ViewGroup。
- View中的onMeasure()主要是用来计算自己的尺寸。
- ViewGroup中的onMeasure()主要是用来调用子类的measure()进行自我测量。同时计算自己的尺寸。
在onMeasure()自己计算尺寸的时候一定要注意是是需要要符合父类的限制的,这个限制就是onMeasure()方法中传入的widthMeasureSpec和heightMeasureSpec。这两个参数中存储着两个信息,大小和限制的类型。
获取大小和限制类型的方式
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
限制的类型一个是三个
MeasureSpec.AT_MOST:老爸告诉你,你只能这么大
MeasureSpec.EXACTLY:老爸告诉你,你就这么大,不能讨教还价
MeasureSpec.UNSPECIFIED:老爸告诉你,你想这么着就怎么着
如何设置当前View的尺寸
一般计算完当前View的尺寸后可以通过调用resolveSize()或resolveSizeAndState()方法计算出当前的大小用setMeasuredDimension
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
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);
}
来,我们举几个例子
1.如果我希望ImageView一直是方
public class SquareImageView extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量出大小
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//然后根据测量的结果重新设置大小
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width > height)
setMeasuredDimension(width,width);
else
setMeasuredDimension(height,height);
}
}
第一步,在调用super.onMeasure()方法后其实已经测量出当前View的大小。
第二步,为了使ImageView变成方的又进行了高宽。
第三步,调用setMeasuredDimension(),重新设置了测量的大小。
2.如果就是想自己计算
public class CalculationView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//去掉super.onMeasure()方法
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//通过一顿计算得出当前View的大小
int width = ......
int height = ......
width = resolveSize(width,widthMeasureSpec);
height = resolveSize(height,heightMeasureSpec);
setMeasuredDimension(width,height);
}
}
如果是纯粹的自己重新计算大小,就不需要调用super.onMeasure()。
第一步,计算自己应该有的大小。
第二步,用resolveSize方法结合父类的限制获取大小。
第三步,调用setMeasuredDimension()设置大小。
3.如果是对ViewGroup进行计算一般的流程
public class CalculationView extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++){
View view = getChildAt(i);
LayoutParams lp = view.getLayoutParams();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**
* 进行一顿就算操作得出各种结论
* 比如,最宽的是多宽,最高的是多高等信息
*/
}
/**
* 设置当前View的大小
*/
setMeasuredDimension(measuredWidth,measuredHeight);
for (int i=0;i<count;i++) {
/**
*
* 然后通过自己的widthMode,widthSize,heightMode,heightSize和lp进行各种计算
*
* 比如最终确定高是height,宽是width。
*
*/
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
view.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
}
}
在继承ViewGroup的类中比继承View的类多一个让子类调用measure()的步骤
第一步,获取子类的大小,结合自己布局的规则进行测量和保存必要的数据。
第二步,通过第一步的计算已经知道了自己的大小,用setMeasuredDimension()方法进行设置。
第三步,对各个子类进行限制并且调用view.measure()方法。
黄金分割线
就上面所有的内容只是测量,仅仅是测量,还没有到真正布局的步骤。
黄金分割线下就是布局了。
布局
先记住布局的起点是View的layout()方法,然后layout()方法中会调用onLayout()方法。
我们先看看FrameLayout代码,假设要执行布局就会调用onLayout()方法。
//FrameLayout.class
//
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
......
//获取测量出来的大小
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
......
//对各子View进行布局操作
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
//View.class
//
public void layout(int l, int t, int r, int b) {
//设置布局的位置和大小
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//对子类布局进行计算
onLayout(changed, l, t, r, b);
}
}
//View.class
//
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//设置View的位置大小
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
return changed;
}
布局的逻辑其实很简单,就是递归调用onLayout()和layout()方法
第一步,在onLayout方法中进行必要的计算,主要是针对当前布局。
第二步,获取每一个View的LayoutParams的设置参数和大小,一顿计算和操作。
第三步,调用子View的layout()方法,调用对View位置起作用的就是setFrame()方法
第四步,继续调用子类的onLayout()方法。
在View的onLayout()方法是空的,ViewGroup的onLayout是一个抽象方法。
总结
测量
- 测量涉及measure()和onMeasure()方法。
- measure()主要的作用就是优化和调用onMeasure()方法。
- 重要的是onMeasure()方法。根据不同需求在方法中一顿计算、测量、设置测量后的大小并且可能会调用子类的measure()方法。
- 记住MeasureSpec类和里面的方法、参数。记住View.resolveSize()。
布局
- 布局涉及layout()和onLayout()方法。
- layout()主要作用就是设置当前View的位置和调用onLayout()方法。
- 重要的是onLayout()方法。根据不同布局需求利用子类的LayoutParams参数和大小在方法中一顿计算,然后调用子类的layout()方法。
我叫陆大旭。
一个懂点心理学的无聊程序员大叔。
看完文章无论有没有收获,记得打赏、关注和点赞!
网友评论