自定义 ViewGroup

基本概念

ViewGroup 继承 View ,但是用来作为一个容器,装载各种 View 以及对它们做 UI 布局,比如高、宽、对齐方式等等,布局文件中凡是以 layout_ 开头的属性,都是传递给 ViewGroup 来解析和使用的。ViewGroup 主要是计算子 View 的测量高宽并决定他们的位置。 重写 LayoutParams 可以自定义子 View 的特定参数,比如 weight 等。

框架和层级结构

ViewViewGroup 的绘制流程框架:

0030_view_draw_flow_framework.png

层级结构如下:

0030_view_draw_flow.png

重要 API

  • onMeasure
    测量自己的高宽;测量所有子 View 的高宽
  • onLayout
    抽象函数,必须重写。自定义子 View 的排列规则

自定义 ViewGroup 步骤

自定义布局属性及 LayoutParams

同样在 res/values/attr.xml 文件中定义 ViewGroup 的属性及样式。

1
2
3
4
5
6
7
8
9
<attr name="custom_orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>

<declare-styleable name="CustomLayout">
<attr name="custom_orientation"/>
<attr name="custom_margin" format="integer"/>
</declare-styleable>

在布局文件使用时,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<com.***.view.CustomLayout
android:id="@+id/view_event_custom_layout"
android:layout_width="300dp"
android:layout_height="300dp"
// 自定义Layout的属性1
app:custom_orientation="vertical" >

<TextView
android:id="@+id/view_event_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_event_textview"
// 自定义Layout的属性2
app:custom_margin="@dimen/custom_margin_text"/>
</com.***.view.CustomLayout>

获取自定义布局属性

ViewGroup 或者自定义 LayoutParams 的构造方法中获取自定义属性值。

1
2
3
4
5
6
7
8
9
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CustomLayout);
// 注意这里的 styleable 的字符串拼接
// 获取layout自定义属性orientation
mOrientation = a.getInt(R.styleable.CustomLayout_custom_orientation,
HORIZONTAL);
}

重写 LayoutParams 相关方法

自定义类 LayoutParams 继承 ViewGroup.LayoutParams,并定义布局所需的几个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class LayoutParams extends ViewGroup.LayoutParams{
// layout 时需要使用的几个变量
public int left = 0;
public int top = 0;
public int right = 0;
public int bottom = 0;

// custom layout property
public int custom_margin = 0;

// 构造方法
public LayoutParams(Context c, AttributeSet attrs){
super(c, attrs);
// 获取layout自定义属性margin
final TypedArray a = c.obtainStyledAttributes(attrs,
R.styleable.CustomLayout);
custom_margin = a.getDimensionPixelSize(
R.styleable.CustomLayout_custom_margin, 0);
}
...
}

如果自定义了 LayoutParams ,必须重写下面四个方法,确保能做类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayout.LayoutParams(getContext(), attrs);
}

@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new CustomLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}

@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof CustomLayout.LayoutParams;
}

@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new CustomLayout.LayoutParams(p);
}

注意:如果没有重写这四个方法,会导致子 View 获取的 LayoutParams 转换为自定义时抛出异常:
CustomLayout.LayoutParams lp = (CustomLayout.LayoutParams) childView.getLayoutParams();
转换失败异常 Log 打印如下:
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to com.***.view.CustomLayout$LayoutParams

重写 onMeasure

实现两个功能:

  • 计算子 View 的测量高宽
  • 计算自身测量高宽
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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//measureChildren(widthMeasureSpec, heightMeasureSpec);

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int measureWidth = 0, measureHeight = 0;

if(widthMode != MeasureSpec.AT_MOST){
measureWidth = widthSize;
}

if(heightMode != MeasureSpec.AT_MOST){
measureHeight = heightSize;
}

int totalLeft = 0, totalTop = 0;
int count = getChildCount();
for(int i = 0; i < count; i++){
View childView = getChildAt(i);
// 1. 计算子 View 的测量高宽
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
int measureChildWidth = childView.getMeasuredWidth();
int measureChildHeight = childView.getMeasuredHeight();
LayoutParams lp = (LayoutParams) childView.getLayoutParams();
// 方向为垂直是的计算方式
if(mOrientation == VERTICAL) {
lp.left = 0;
lp.top = totalTop + lp.custom_margin;
lp.right = measureChildWidth;
lp.bottom = lp.top + measureChildHeight;
totalTop = lp.bottom;

// 如果 ViewGroup 高宽设置的是 wrap_content ,需要计算实际大小
if(widthMode == MeasureSpec.AT_MOST){
measureWidth = Math.max(measureWidth, measureChildWidth);
}
if(heightMode == MeasureSpec.AT_MOST){
measureHeight = lp.bottom;
}
}
...
}

// 2. 设置 ViewGroup 自身的测量高宽
setMeasuredDimension(measureWidth, measureHeight);
}

重写 onLayout

计算子 View 的具体布局位置

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for(int i = 0; i < count; i++){
View childView = getChildAt(i);
CustomLayout.LayoutParams lp = (CustomLayout.LayoutParams)
childView.getLayoutParams();
// 指定子 View 具体的布局位置,这些位置是在 onMeasure 中计算的
childView.layout(lp.left, lp.top, lp.right, lp.bottom);
}
}

自定义 ViewGroup 中,至少需要重写 onMeasureonLayout

总结

  • onMeasure 主要计算 wrap_content 模式下的测量高宽,包含自己和所有的子 View
  • onLayout 主要计算子 View 布局的具体位置
  • onDraw 绘制自己,展现需要显示的内容

自定义 ViewGroup 主要计算自身和子 View 的测量高宽,以及子 View 布局的具体位置。
自定义 View 主要计算自身的测量高宽,以及绘制自己。

问题

Log 跟踪中发现 onLayoutonMeasure 会被调用执行两次

目标

  • 自定义 ViewGroup 常见流程
  • 必须重写 onLayout 及需要实现那些功能
  • 是否处理事件分发流程

参考文档

  1. http://www.jianshu.com/p/3d2c49315d68
  2. http://blog.csdn.net/lmj623565791/article/details/38339817/
  3. http://www.jianshu.com/p/138b98095778
0%