深入浅出安卓自定义ViewGroup
一、ViewGroup是什么?——安卓界面的乐高大师
想象ViewGroup就像乐高积木的底板:
- 基本功能:可以固定其他积木(子View)
- 扩展能力:能自定义积木排列规则
- 事件处理:决定哪些积木响应触摸
自定义ViewGroup就是自己设计一块特殊底板,实现独特的布局效果。
二、自定义ViewGroup三大核心方法
1. onMeasure() —— 量尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 测量所有子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
// 2. 计算自己的尺寸
int width = calculateTotalWidth();
int height = calculateTotalHeight();
// 3. 保存测量结果
setMeasuredDimension(width, height);
}
关键点:
MeasureSpec包含模式和尺寸(EXACTLY/AT_MOST/UNSPECIFIED)- 必须先测量子View再确定自身尺寸
- 最终必须调用
setMeasuredDimension()
2. onLayout() —— 摆位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 计算当前子View的四个边界位置
int left = calculateChildLeft(i);
int top = calculateChildTop(i);
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
// 布局子View
child.layout(left, top, right, bottom);
}
}
记忆口诀:
- 左上是起点(left, top)
- 右下=左上+宽高(right=left+width, bottom=top+height)
- 必须调用每个子View的
layout()
3. onDraw() —— 画装饰(可选)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // 默认会绘制背景
// 自定义绘制内容
canvas.drawCircle(...);
canvas.drawText(...);
}
注意:
- 如果只是布局容器,可以不重写onDraw
- 需要自定义绘制时才重写
三、自定义ViewGroup实战——实现流式布局
1. 设计目标
- 从左到右排列子View
- 行满自动换行
- 支持行居中/左对齐/右对齐
2. 完整实现代码
public class FlowLayout extends ViewGroup {
private int mHorizontalSpacing = 10; // 水平间距
private int mVerticalSpacing = 10; // 垂直间距
private int mGravity = Gravity.LEFT; // 对齐方式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int totalWidth = 0;
int totalHeight = 0;
int lineWidth = 0;
int lineHeight = 0;
// 测量所有子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
measureChild(child, widthMeasureSpec, heightMeasureSpec);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 判断是否需要换行
if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
totalWidth = Math.max(totalWidth, lineWidth);
totalHeight += lineHeight + mVerticalSpacing;
lineWidth = 0;
lineHeight = 0;
}
lineWidth += childWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
}
// 加上最后一行
totalWidth = Math.max(totalWidth, lineWidth);
totalHeight += lineHeight;
// 考虑padding
totalWidth += getPaddingLeft() + getPaddingRight();
totalHeight += getPaddingTop() + getPaddingBottom();
// 处理AT_MOST模式
widthSize = widthMode == MeasureSpec.AT_MOST ?
Math.min(widthSize, totalWidth) : totalWidth;
setMeasuredDimension(widthSize, totalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = getWidth();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int x = paddingLeft;
int y = paddingTop;
int lineHeight = 0;
// 记录每行的View列表
List<View> lineViews = new ArrayList<>();
int lineWidth = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 需要换行
if (x + childWidth > width - getPaddingRight()) {
layoutLine(lineViews, lineWidth, lineHeight, y);
y += lineHeight + mVerticalSpacing;
x = paddingLeft;
lineViews.clear();
lineWidth = 0;
lineHeight = 0;
}
lineViews.add(child);
lineWidth += childWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
x += childWidth + mHorizontalSpacing;
}
// 布局最后一行
if (!lineViews.isEmpty()) {
layoutLine(lineViews, lineWidth, lineHeight, y);
}
}
// 布局单行View
private void layoutLine(List<View> views, int lineWidth, int lineHeight, int y) {
int parentWidth = getWidth();
int paddingLeft = getPaddingLeft();
// 计算起始x坐标(根据gravity)
int x = paddingLeft;
if (mGravity == Gravity.CENTER_HORIZONTAL) {
x = (parentWidth - lineWidth) / 2;
} else if (mGravity == Gravity.RIGHT) {
x = parentWidth - lineWidth - getPaddingRight();
}
// 布局该行所有View
for (View child : views) {
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(
x,
y,
x + childWidth,
y + childHeight
);
x += childWidth + mHorizontalSpacing;
}
}
}
四、自定义ViewGroup进阶技巧
1. 处理触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 判断是否需要拦截事件
return shouldIntercept ? true : super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理自己的触摸逻辑
return true; // 表示消费事件
}
2. 支持XML属性
<!-- res/values/attrs.xml -->
<declare-styleable name="FlowLayout">
<attr name="horizontal_spacing" format="dimension" />
<attr name="vertical_spacing" format="dimension" />
<attr name="gravity">
<flag name="left" value="0x03" />
<flag name="center_horizontal" value="0x01" />
<flag name="right" value="0x05" />
</attr>
</declare-styleable>
// 在构造方法中读取
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mHorizontalSpacing = ta.getDimensionPixelSize(
R.styleable.FlowLayout_horizontal_spacing, 10);
mGravity = ta.getInt(R.styleable.FlowLayout_gravity, Gravity.LEFT);
ta.recycle();
3. 性能优化
// 1. 减少onMeasure/onLayout调用
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
// 2. 使用View的绘制缓存
setLayerType(LAYER_TYPE_HARDWARE, null);
// 3. 避免过度绘制
setWillNotDraw(true); // 如果没有自定义绘制
五、常见问题解决方案
1. 子View显示不全
原因:onMeasure计算尺寸错误或onLayout位置计算错误 排查:
- 检查子View是否都调用了measureChild()
- 检查layout()的right/bottom是否=left+measuredWidth/top+measuredHeight
2. 布局性能差
优化方案:
- 减少不必要的measure/layout调用
- 对于固定尺寸的子View,使用setMeasuredDimension()直接设置
- 重写
getChildDrawingOrder()优化绘制顺序
3. 触摸事件不响应
检查点:
- 是否错误拦截了事件(onInterceptTouchEvent)
- 子View的clickable属性是否为true
- 是否在onTouchEvent中返回了true
六、自定义ViewGroup vs 自定义View
| 特性 | 自定义ViewGroup | 自定义View |
|---|---|---|
| 核心方法 | onMeasure/onLayout | onMeasure/onDraw |
| 复杂度 | 高(需处理子View) | 低 |
| 适用场景 | 自定义布局规则 | 自定义绘制效果 |
| 性能考量 | 测量/布局性能 | 绘制性能 |
七、总结
自定义ViewGroup三板斧:
- 量尺寸:onMeasure()里测量所有子View
- 摆位置:onLayout()里给每个子View安排座位
- 管事件:决定是否拦截触摸事件
进阶技巧:
- 支持XML属性让布局更灵活
- 优化测量布局性能
- 处理复杂的触摸交互
记住:好的自定义ViewGroup应该像优秀的导演——知道每个演员(子View)该站哪里,该什么时候上场!