Android自定义view-测量与布局

基础知识

View/ViewGroup 绘制流程分三步,分别是 测量onMeasure() , 布局onLayout(),绘制onDraw()。getMeasuredWidth()是在measure过程后就可以获取到的,getWidth()是在layout()过程结束后才能获得到的

测量onMeasure

测量当前控件大小,为onLayout提供参考值,真正布局是在onLayout实现,传进来的int widthMeasureSpec, int heightMeasureSpec 包含宽/高 布局模式和值。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//可以通过以下方法获取模式和值,以下是Mode的值的解释
//MeasureSpec.AT_MOST 给出可用的最大尺寸(wrap_content)
//MeasureSpec.EXACTLY 给出指定的尺寸(match_parent 或者用户指定大小的情况)
//MeasureSpec.UNSPECIFIED 未指定尺寸(ScrollView等控件下返回)
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
}

自定义View的情况

我们就要根据不同的Mode的情况去设定我们的宽高,例如在MeasureSpec.AT_MOST下给出我们自己的最小宽高,在MeasureSpec.EXACTLY的情况下直接传给出的值等等。其实android已经给我们封装好方法resolveSize,我们直接调用即可。

1
2
3
4
5
6
7
8
9
10
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int width = (PADDING + RADIUS) ;//wrap_content 下的尺寸
int height = (PADDING + RADIUS) ;//wrap_content 下的尺寸
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, widthMeasureSpec);
setMeasuredDimension(width, height);//用来设置View的宽高 保存测的的尺寸 之后我们可以调用 getMeasuredWidth()和 getMeasuredHeight()方法获取这个宽度和高度值
}

自定义ViewGroup的情况

我们需要在ViewGroup的onMeasure()去测量自己的子控件的宽高,从而保存ViewGroup自身的尺寸,然后在onLayout()下对子控件进行排列位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
//计算后ViewGroup的占的宽高
int height = 0;
int width = 0;
int count = getChildCount();
//获取子控件
for (int i=0;i<count;i++){
View child = getChildAt(i);
//measureChild(child,widthMeasureSpec,heightMeasureSpec); 不考虑 margin的测量
//调用子view的onMeasure measureChildWithMargins 测算出来控件宽高会考虑 margin
// 第一个参数是子view 第三个参数是已用宽度 第五个参数是已用高度
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
//获取子控件Margin参数
MarginLayoutParams lp = null;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child
.getLayoutParams();
}else{
lp = new MarginLayoutParams(0,0);
}
//子控件占用宽高
int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//一般来说这里写计算子控件占你自定义ViewGroup宽高的逻辑
}
//当属性是MeasureSpec.EXACTLY时,那么它的高度就是确定的,
// 只有当是wrap_content时,根据内部控件的大小来确定它的大小时,大小是不确定的,属性是AT_MOST,此时,就需要我们自己计算它的应当的大小,并设置进去
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth
: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight
: height);
}
//要支持子控件的LayoutMargin 就要重写这个方法 返回一个MarginLayoutParams类
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
//自定义的Viewgroup中 自定义了内部类LayoutParams 则必须重写该方法 让子view有特殊的属性
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MarginLayoutParams(lp);
}
//new布局的时候没指定宽高 赋予默认宽高
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

布局

自定义ViewGroup的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int lineWidth = 0;
int lineHeight = 0;
int top=0,left=0;
for (int i=0; i<count;i++){
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
//一般计算childView的left,top,right,bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc =lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
//将left置为下一子控件的起始点
left+=childWidth;
}