一、开篇:为什么你需要深入理解三大流程?
作为Android开发者,你一定遇到过这些令人困惑的场景:
场景1: 你写了一个自定义View,在XML中设置了wrap_content,但显示的效果却和match_parent一样,内容被截断或显示不全。
场景2: 你的自定义ViewGroup在滑动时卡顿,用Profiler分析发现onMeasure被调用了数十次甚至上百次。
场景3: 你照着网上的例子实现了自定义View,但在某些手机上布局错乱,却找不到原因。
这些问题的根源,都是对Android View系统的三大核心流程------measure(测量)、layout(布局)、draw(绘制) 理解不够深入。今天,我将带你彻底解密这三大流程,让你成为自定义View的专家。
错误示例:90%的开发者都踩过这个坑
java
public class WrongView extends View {
public WrongView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 错误!直接设置固定尺寸,wrap_content将完全失效
setMeasuredDimension(200, 200);
}
}
这个看似简单的代码,却让无数开发者掉进坑里。为什么?读完本文,你将彻底明白。
二、宏观视角:从DecorView开始的绘制旅程
在深入细节之前,我们先从整体上理解Android UI系统是如何工作的。
2.1 绘制流程的起点:ViewRootImpl
每个Activity都包含一个Window,每个Window都关联着一个ViewRootImpl。它是连接WindowManager和View树的桥梁。
java
// 简化版的绘制流程核心代码
public final class ViewRootImpl {
public void performTraversals() {
// 1. 预测量阶段(可能多次)
if (layoutRequested) {
windowSizeMayChange = measureHierarchy(...);
}
// 2. 最终测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 3. 布局阶段
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
// 4. 绘制阶段
if (!cancelDraw) {
performDraw();
}
}
}
2.2 三大流程的关系图
text
┌─────────────────────────────────────────────────────┐
│ ViewRootImpl.performTraversals() │
├─────────────┬──────────────┬────────────────────────┤
│ performMeasure │ performLayout │ performDraw │
│ (测量) │ (布局) │ (绘制) │
└─────────────┴──────────────┴────────────────────────┘
↓ ↓ ↓
onMeasure() onLayout() onDraw()/dispatchDraw()
(确定尺寸) (确定位置) (绘制内容)
关键点理解:
- 测量决定大小:View需要多大空间
- 布局决定位置:View放在哪里
- 绘制决定内容:View长什么样
三、测量(measure)过程深度解析
3.1 MeasureSpec:测量规格的DNA
MeasureSpec是View测量过程中的核心概念,它是一个32位的int值,高2位表示测量模式,低30位表示测量尺寸。
java
// MeasureSpec的源码实现
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 0
public static final int EXACTLY = 1 << MODE_SHIFT; // 1073741824
public static final int AT_MOST = 2 << MODE_SHIFT; // 2147483648
// 打包模式和尺寸为MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
// 从MeasureSpec解包模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 从MeasureSpec解包尺寸
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
三种测量模式的实战含义
| 模式 | 二进制值 | 含义 | 典型场景 | 开发者需要做什么 |
|---|---|---|---|---|
| UNSPECIFIED | 00 | 父容器对子View无限制,子View想要多大就多大 | ScrollView、ListView等可滚动的容器 | 返回期望的真实尺寸 |
| EXACTLY | 01 | 精确尺寸,View必须是这个大小 | match_parent或具体数值(100dp) | 直接使用给定的尺寸 |
| AT_MOST | 10 | 最大尺寸限制,View不能超过这个大小 | wrap_content | 返回不超过限制的期望尺寸 |
3.2 MeasureSpec的传递:父View如何决定子View的大小
理解MeasureSpec的关键是明白它是如何从父View传递给子View的。看下面这个例子:
java
// ViewGroup中的关键方法:如何为子View生成MeasureSpec
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 计算子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 让子View测量自己
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec方法是理解MeasureSpec传递的核心:
java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 可用尺寸 = 父容器尺寸 - padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父容器是EXACTLY模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 子View指定了具体尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View是match_parent
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子View是wrap_content
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器是AT_MOST模式
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器是UNSPECIFIED模式
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这个方法的逻辑可以用下面的表格总结:
| 父容器模式 | 子View的LayoutParams | 子View的MeasureSpec |
|---|---|---|
| EXACTLY | 具体数值(100dp) | EXACTLY + 100dp |
| EXACTLY | match_parent | EXACTLY + 父容器可用尺寸 |
| EXACTLY | wrap_content | AT_MOST + 父容器可用尺寸 |
| AT_MOST | match_parent | AT_MOST + 父容器可用尺寸 |
| AT_MOST | wrap_content | AT_MOST + 父容器可用尺寸 |
| UNSPECIFIED | match_parent | UNSPECIFIED + 0 |
| UNSPECIFIED | wrap_content | UNSPECIFIED + 0 |
3.3 View的measure过程:从measure()到onMeasure()
让我们看看View的measure()方法到底做了什么:
java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 检查是否需要重新测量(性能优化)
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// 2. 清除"已测量"标志
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_CHANGED;
// 3. 回调onMeasure,让子类实现测量逻辑
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 4. 检查子类是否调用了setMeasuredDimension()
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_CHANGED) != PFLAG_MEASURED_DIMENSION_CHANGED) {
throw new IllegalStateException(
getClass().getName() + " did not call setMeasuredDimension()");
}
// 5. 设置"需要布局"标志
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 6. 保存当前的MeasureSpec
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
关键点:
- measure()是final方法,不能重写
- 测量有缓存机制,MeasureSpec不变时不会重新测量
- 必须在onMeasure中调用setMeasuredDimension(),否则会抛出异常
3.4 正确的onMeasure实现:处理所有测量模式
下面是一个正确处理所有测量模式的完整示例:
java
public class CorrectView extends View {
private Paint mPaint;
private String mText = "Hello Custom View";
public CorrectView(Context context) {
super(context);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(50);
mPaint.setColor(Color.BLACK);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 步骤1:解析MeasureSpec
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 步骤2:计算内容需要的尺寸(不包含padding)
int desiredWidth = calculateDesiredWidth();
int desiredHeight = calculateDesiredHeight();
// 步骤3:考虑padding
int widthWithPadding = desiredWidth + getPaddingLeft() + getPaddingRight();
int heightWithPadding = desiredHeight + getPaddingTop() + getPaddingBottom();
// 步骤4:根据测量模式确定最终尺寸
int finalWidth = resolveSize(widthWithPadding, widthMeasureSpec);
int finalHeight = resolveSize(heightWithPadding, heightMeasureSpec);
// 步骤5:必须调用!
setMeasuredDimension(finalWidth, finalHeight);
}
private int calculateDesiredWidth() {
// 计算文本宽度
return (int) mPaint.measureText(mText);
}
private int calculateDesiredHeight() {
// 计算文本高度
Paint.FontMetrics fm = mPaint.getFontMetrics();
return (int) (fm.bottom - fm.top);
}
// 通用的尺寸解析方法(Android源码中的实现)
public static int resolveSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制时要考虑padding
canvas.drawText(mText,
getPaddingLeft(),
getPaddingTop() - mPaint.ascent(),
mPaint);
}
}
3.5 ViewGroup的测量:测量所有子View
对于ViewGroup,测量过程更加复杂,因为需要测量所有子View:
java
public abstract class ViewGroup extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 测量所有子View
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 测量每个子View
measureChildWithMargins(child,
widthMeasureSpec, 0,
heightMeasureSpec, 0);
}
}
// 2. 根据子View的测量结果确定自己的尺寸
int width = calculateTotalWidth();
int height = calculateTotalHeight();
// 3. 考虑自身的padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
// 4. 处理测量模式限制
width = resolveSizeAndState(width, widthMeasureSpec, 0);
height = resolveSizeAndState(height, heightMeasureSpec, 0);
// 5. 设置最终尺寸
setMeasuredDimension(width, height);
}
// 这个方法非常重要!它考虑子View的margin
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 为子View创建MeasureSpec(考虑margin)
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
3.6 实战:实现一个简单的LinearLayout
让我们通过实现一个简化版的LinearLayout来加深理解:
java
public class SimpleLinearLayout extends ViewGroup {
private static final int VERTICAL = 0;
private static final int HORIZONTAL = 1;
private int orientation = VERTICAL;
public SimpleLinearLayout(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (orientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 可用宽度 = 父容器宽度 - padding
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
int totalHeight = getPaddingTop() + getPaddingBottom();
int maxChildWidth = 0;
// 第一遍:测量所有子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子View
measureChildWithMargins(child,
widthMeasureSpec, 0,
heightMeasureSpec, totalHeight);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 更新最大子View宽度
maxChildWidth = Math.max(maxChildWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 累加高度(包括margin)
totalHeight += child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin;
}
// 确定自己的宽度
int width;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = maxChildWidth + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize);
}
}
// 确定自己的高度
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = totalHeight;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
}
3.7 测量优化:避免不必要的测量
测量是性能敏感的操作,特别是在列表或滚动视图中。以下是一些优化技巧:
java
public class OptimizedView extends View {
private int mCachedWidthMeasureSpec = Integer.MIN_VALUE;
private int mCachedHeightMeasureSpec = Integer.MIN_VALUE;
private int mCachedMeasuredWidth;
private int mCachedMeasuredHeight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 如果MeasureSpec没有变化,使用缓存结果
if (widthMeasureSpec == mCachedWidthMeasureSpec &&
heightMeasureSpec == mCachedHeightMeasureSpec) {
setMeasuredDimension(mCachedMeasuredWidth, mCachedMeasuredHeight);
return;
}
// 重新计算尺寸
int width = calculateWidth(widthMeasureSpec);
int height = calculateHeight(heightMeasureSpec);
// 更新缓存
mCachedWidthMeasureSpec = widthMeasureSpec;
mCachedHeightMeasureSpec = heightMeasureSpec;
mCachedMeasuredWidth = width;
mCachedMeasuredHeight = height;
setMeasuredDimension(width, height);
}
@Override
public void requestLayout() {
// 清除缓存,强制重新测量
mCachedWidthMeasureSpec = Integer.MIN_VALUE;
mCachedHeightMeasureSpec = Integer.MIN_VALUE;
super.requestLayout();
}
}
四、布局(layout)过程深度解析
4.1 layout()方法:确定View的位置
测量完成后,接下来就是布局。layout()方法确定View在其父容器中的位置。
java
public void layout(int l, int t, int r, int b) {
// 记录旧的边界
int oldL = mLeft;
int oldT = mTop;
int oldR = mRight;
int oldB = mBottom;
// 判断布局是否改变
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用onLayout
onLayout(changed, l, t, r, b);
// 清除需要布局的标志
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 通知布局变化监听器
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
}
关键点:
- setFrame()方法实际设置View的四个边界值
- onLayout()在布局改变或需要布局时被调用
- 可以注册OnLayoutChangeListener监听布局变化
4.2 View的onLayout:单个View不需要布局子View
对于单个View(非ViewGroup),onLayout()是空实现,因为它没有子View需要布局:
java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 单个View不需要布局子View
}
4.3 ViewGroup的onLayout:布局所有子View
对于ViewGroup,必须实现onLayout()来布局所有子View:
java
public class SimpleLayout extends ViewGroup {
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取padding
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int currentLeft = paddingLeft;
int currentTop = paddingTop;
// 布局每个子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 获取子View的测量尺寸
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 布局子View
child.layout(currentLeft,
currentTop,
currentLeft + childWidth,
currentTop + childHeight);
// 更新下一个View的位置
currentLeft += childWidth;
}
}
}
4.4 支持margin的完整布局实现
实际开发中,我们需要考虑子View的margin:
java
public class MarginSupportLayout extends ViewGroup {
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int currentLeft = getPaddingLeft();
int currentTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 考虑margin计算子View的位置
int childLeft = currentLeft + lp.leftMargin;
int childTop = currentTop + lp.topMargin;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
// 布局子View
child.layout(childLeft, childTop, childRight, childBottom);
// 更新位置,考虑rightMargin
currentLeft = childRight + lp.rightMargin;
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
4.5 测量与布局的协作:优化性能
聪明的开发者会在测量阶段就计算好布局信息,这样在布局阶段就可以直接使用:
java
public class SmartLayout extends ViewGroup {
// 在测量阶段就计算好子View的位置
private int[] childPositions;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
childPositions = new int[childCount * 4]; // 每个子View存储left, top, right, bottom
int totalWidth = getPaddingLeft() + getPaddingRight();
int maxHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 计算并存储位置
childPositions[i*4] = totalWidth; // left
childPositions[i*4+1] = getPaddingTop(); // top
childPositions[i*4+2] = totalWidth + child.getMeasuredWidth(); // right
childPositions[i*4+3] = getPaddingTop() + child.getMeasuredHeight(); // bottom
totalWidth += child.getMeasuredWidth();
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
}
// 考虑padding设置自身尺寸
int height = maxHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(
resolveSize(totalWidth, widthMeasureSpec),
resolveSize(height, heightMeasureSpec)
);
}
@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);
if (child.getVisibility() != GONE) {
child.layout(
childPositions[i*4],
childPositions[i*4+1],
childPositions[i*4+2],
childPositions[i*4+3]
);
}
}
}
}
五、绘制(draw)过程深度解析
5.1 draw()方法的六个步骤
View的draw()方法实际上执行了六个步骤:
java
public void draw(Canvas canvas) {
// Step 1: 绘制背景
drawBackground(canvas);
// Step 2: 保存图层(如果需要特殊效果)
int saveCount = 0;
if (!dirtyOpaque) {
saveCount = canvas.saveLayer(...);
}
// Step 3: 绘制View自身内容
onDraw(canvas);
// Step 4: 绘制子View
dispatchDraw(canvas);
// Step 5: 绘制装饰(如滚动条、前景)
onDrawForeground(canvas);
// Step 6: 恢复图层
if (!dirtyOpaque) {
canvas.restoreToCount(saveCount);
}
}
5.2 onDraw():绘制View的内容
对于自定义View,最常重写的就是onDraw()方法:
java
public class CustomCircleView extends View {
private Paint mPaint;
private int mCircleColor = Color.RED;
private int mStrokeColor = Color.BLUE;
private int mStrokeWidth = 10;
public CustomCircleView(Context context) {
super(context);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = Math.min(centerX, centerY) - mStrokeWidth;
// 1. 绘制背景(如果需要)
canvas.drawColor(Color.WHITE);
// 2. 绘制圆形填充
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mCircleColor);
canvas.drawCircle(centerX, centerY, radius, mPaint);
// 3. 绘制圆形边框
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setColor(mStrokeColor);
canvas.drawCircle(centerX, centerY, radius, mPaint);
// 4. 绘制文字
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(40);
mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("Hello", centerX, centerY, mPaint);
}
}
5.3 dispatchDraw():绘制子View
对于ViewGroup,还需要绘制子View,这是通过dispatchDraw()实现的:
java
public abstract class ViewGroup extends View {
@Override
protected void dispatchDraw(Canvas canvas) {
// 按子View的绘制顺序进行绘制
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 绘制子View
drawChild(canvas, child, drawingTime);
}
}
}
}
5.4 绘制顺序的重要性
绘制顺序决定了哪些内容在上层,哪些在下层:
java
@Override
protected void onDraw(Canvas canvas) {
// 错误的绘制顺序示例
mPaint.setColor(Color.RED);
canvas.drawText("Hello", 100, 100, mPaint); // 文字先绘制
mPaint.setColor(Color.BLUE);
canvas.drawRect(50, 50, 150, 150, mPaint); // 矩形后绘制,会覆盖文字
// 正确的绘制顺序应该是:
// 1. 先绘制底层内容
canvas.drawRect(50, 50, 150, 150, mPaint);
// 2. 再绘制上层内容
canvas.drawText("Hello", 100, 100, mPaint);
}
5.5 避免过度绘制的技巧
过度绘制是Android UI性能的常见杀手。以下是一些优化技巧:
技巧1:使用clipRect限制绘制区域
java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 保存画布状态
canvas.save();
// 只绘制可见区域,避免绘制不可见的部分
canvas.clipRect(mVisibleRect);
// 执行复杂的绘制操作
drawComplexContent(canvas);
// 恢复画布状态
canvas.restore();
}
技巧2:快速拒绝不可见区域
java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 快速拒绝:如果View完全不可见
if (getWidth() == 0 || getHeight() == 0) {
return;
}
// 快速拒绝:如果脏区域与内容区域不相交
Rect dirtyRect = new Rect();
canvas.getClipBounds(dirtyRect);
if (!Rect.intersects(dirtyRect, mContentBounds)) {
return;
}
// 只绘制需要更新的部分
drawPartialContent(canvas, dirtyRect);
}
技巧3:使用Canvas的快速绘制方法
java
@Override
protected void onDraw(Canvas canvas) {
// 使用这些方法比单独绘制多个形状更快
canvas.drawRect(mRectArray, mPaint);
canvas.drawLines(mLineArray, mPaint);
canvas.drawPoints(mPointArray, mPaint);
// 批量绘制文本
canvas.drawText(mText, mStart, mEnd, x, y, mPaint);
}
六、综合实战:实现一个Tag流式布局
现在,让我们把学到的知识综合起来,实现一个完整的、生产可用的TagFlowLayout。
6.1 需求分析
- 标签自动换行
- 可设置水平和垂直间距
- 支持最大行数限制
- 支持标签点击效果
- 良好的性能表现
6.2 完整实现
java
/**
* 流式标签布局
* 支持自动换行、间距设置、最大行数限制
*/
public class TagFlowLayout extends ViewGroup {
private static final int DEFAULT_HORIZONTAL_SPACING = dp2px(8);
private static final int DEFAULT_VERTICAL_SPACING = dp2px(8);
private static final int DEFAULT_MAX_LINES = Integer.MAX_VALUE;
private int horizontalSpacing = DEFAULT_HORIZONTAL_SPACING;
private int verticalSpacing = DEFAULT_VERTICAL_SPACING;
private int maxLines = DEFAULT_MAX_LINES;
// 存储每行的View和行高
private final List<List<View>> lines = new ArrayList<>();
private final List<Integer> lineHeights = new ArrayList<>();
private final List<Integer> lineWidths = new ArrayList<>();
public TagFlowLayout(Context context) {
this(context, null);
}
public TagFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout,
defStyleAttr, 0);
try {
horizontalSpacing = ta.getDimensionPixelSize(
R.styleable.TagFlowLayout_horizontalSpacing, DEFAULT_HORIZONTAL_SPACING);
verticalSpacing = ta.getDimensionPixelSize(
R.styleable.TagFlowLayout_verticalSpacing, DEFAULT_VERTICAL_SPACING);
maxLines = ta.getInt(R.styleable.TagFlowLayout_maxLines, DEFAULT_MAX_LINES);
} finally {
ta.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 清空之前的数据
lines.clear();
lineHeights.clear();
lineWidths.clear();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 计算可用宽度
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
if (availableWidth <= 0) {
availableWidth = widthSize;
}
// 当前行的数据
List<View> currentLine = new ArrayList<>();
int currentLineWidth = 0;
int currentLineHeight = 0;
int totalHeight = 0;
int lineCount = 0;
// 遍历所有子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子View
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 判断是否需要换行
boolean needNewLine = !currentLine.isEmpty() &&
currentLineWidth + childWidth + horizontalSpacing > availableWidth;
if (needNewLine) {
// 保存当前行
saveCurrentLine(currentLine, currentLineWidth, currentLineHeight);
lineCount++;
// 检查是否超过最大行数
if (lineCount >= maxLines) {
break;
}
// 开始新的一行
currentLine.clear();
currentLineWidth = 0;
currentLineHeight = 0;
}
// 添加子View到当前行
currentLine.add(child);
if (currentLine.isEmpty()) {
currentLineWidth = childWidth;
} else {
currentLineWidth += childWidth + horizontalSpacing;
}
currentLineHeight = Math.max(currentLineHeight, childHeight);
}
// 保存最后一行
if (!currentLine.isEmpty() && lineCount < maxLines) {
saveCurrentLine(currentLine, currentLineWidth, currentLineHeight);
lineCount++;
}
// 计算总高度
totalHeight = calculateTotalHeight();
// 确定最终尺寸
int measuredWidth = resolveSize(widthSize, widthMeasureSpec);
int measuredHeight = resolveSize(totalHeight, heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
private void saveCurrentLine(List<View> line, int width, int height) {
lines.add(new ArrayList<>(line));
lineWidths.add(width);
lineHeights.add(height);
}
private int calculateTotalHeight() {
if (lineHeights.isEmpty()) {
return getPaddingTop() + getPaddingBottom();
}
int totalHeight = getPaddingTop() + getPaddingBottom();
for (int i = 0; i < lineHeights.size(); i++) {
totalHeight += lineHeights.get(i);
if (i > 0) {
totalHeight += verticalSpacing;
}
}
return totalHeight;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftStart = getPaddingLeft();
int topStart = getPaddingTop();
// 遍历每一行
for (int i = 0; i < lines.size(); i++) {
List<View> line = lines.get(i);
int lineHeight = lineHeights.get(i);
int lineWidth = lineWidths.get(i);
// 计算行内对齐(居左、居中、居右)
int currentLeft = calculateLineStartLeft(leftStart, lineWidth);
// 布局当前行的每个View
for (View child : line) {
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childLeft = currentLeft + lp.leftMargin;
int childTop = topStart + lp.topMargin;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
currentLeft = childRight + lp.rightMargin + horizontalSpacing;
}
// 更新下一行的起始top
topStart += lineHeight + verticalSpacing;
}
}
private int calculateLineStartLeft(int leftStart, int lineWidth) {
// 可以根据需要实现不同的对齐方式
// 这里实现居左对齐
return leftStart;
// 居中对齐的实现:
// return leftStart + (getWidth() - getPaddingLeft() - getPaddingRight() - lineWidth) / 2;
// 居右对齐的实现:
// return getWidth() - getPaddingRight() - lineWidth;
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
// 添加标签的方法
public void addTag(String text) {
TextView tagView = new TextView(getContext());
tagView.setText(text);
tagView.setBackgroundResource(R.drawable.tag_background);
tagView.setPadding(dp2px(8), dp2px(4), dp2px(8), dp2px(4));
tagView.setOnClickListener(v -> {
// 处理标签点击
onTagClick(text);
});
addView(tagView);
}
private void onTagClick(String tag) {
// 标签点击回调
if (listener != null) {
listener.onTagClick(tag);
}
}
public interface OnTagClickListener {
void onTagClick(String tag);
}
private OnTagClickListener listener;
public void setOnTagClickListener(OnTagClickListener listener) {
this.listener = listener;
}
private static int dp2px(float dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp,
Resources.getSystem().getDisplayMetrics());
}
}
6.3 属性定义(attrs.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TagFlowLayout">
<attr name="horizontalSpacing" format="dimension" />
<attr name="verticalSpacing" format="dimension" />
<attr name="maxLines" format="integer" />
</declare-styleable>
</resources>
6.4 使用示例
xml
<com.example.view.TagFlowLayout
android:id="@+id/tag_flow_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
app:horizontalSpacing="8dp"
app:verticalSpacing="8dp"
app:maxLines="3" />
java
// 在Activity或Fragment中使用
TagFlowLayout tagLayout = findViewById(R.id.tag_flow_layout);
tagLayout.setOnTagClickListener(tag -> {
Toast.makeText(this, "点击了: " + tag, Toast.LENGTH_SHORT).show();
});
// 添加标签
String[] tags = {"Android", "Java", "Kotlin", "Flutter", "React Native",
"iOS", "Python", "机器学习", "人工智能"};
for (String tag : tags) {
tagLayout.addTag(tag);
}
七、性能优化最佳实践
7.1 测量优化策略
java
public class PerformanceView extends View {
// 缓存测量结果
private int cachedWidth;
private int cachedHeight;
private int cachedWidthMeasureSpec = Integer.MIN_VALUE;
private int cachedHeightMeasureSpec = Integer.MIN_VALUE;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 使用缓存避免重复测量
if (widthMeasureSpec == cachedWidthMeasureSpec &&
heightMeasureSpec == cachedHeightMeasureSpec) {
setMeasuredDimension(cachedWidth, cachedHeight);
return;
}
// 实际测量逻辑
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
// 更新缓存
cachedWidth = width;
cachedHeight = height;
cachedWidthMeasureSpec = widthMeasureSpec;
cachedHeightMeasureSpec = heightMeasureSpec;
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 尺寸变化时清除缓存
cachedWidthMeasureSpec = Integer.MIN_VALUE;
cachedHeightMeasureSpec = Integer.MIN_VALUE;
}
}
7.2 绘制优化策略
java
public class OptimizedDrawView extends View {
private Bitmap cacheBitmap;
private Canvas cacheCanvas;
private boolean cacheDirty = true;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
// 创建缓存Bitmap
cacheBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(cacheBitmap);
cacheDirty = true;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (cacheDirty) {
// 只在需要时重新绘制到缓存
drawToCache(cacheCanvas);
cacheDirty = false;
}
// 绘制缓存Bitmap
canvas.drawBitmap(cacheBitmap, 0, 0, null);
}
private void drawToCache(Canvas canvas) {
// 复杂的绘制操作
long start = System.currentTimeMillis();
// ... 绘制逻辑
long duration = System.currentTimeMillis() - start;
Log.d("OptimizedDrawView", "缓存绘制耗时: " + duration + "ms");
}
public void invalidateCache() {
cacheDirty = true;
invalidate();
}
}
7.3 使用硬件加速
java
public class HardwareAcceleratedView extends View {
public HardwareAcceleratedView(Context context) {
super(context);
// 启用硬件加速
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_HARDWARE, null);
}
}
@Override
protected void onDraw(Canvas canvas) {
// 硬件加速支持的绘制操作(性能更好)
canvas.drawRect(0, 0, 100, 100, paint);
canvas.drawCircle(150, 50, 50, paint);
// 硬件加速不支持的绘制操作(性能较差)
// canvas.drawPath(complexPath, paint); // 复杂Path
// canvas.clipPath(path); // 非矩形裁剪
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 释放资源
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_NONE, null);
}
}
}
八、调试技巧与工具
8.1 添加调试信息
java
public class DebugView extends View {
private static final boolean DEBUG = BuildConfig.DEBUG;
private int measureCount = 0;
private int layoutCount = 0;
private int drawCount = 0;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (DEBUG) {
measureCount++;
Log.d("DebugView", String.format(
"onMeasure #%d: mode=%s, size=%d",
measureCount,
modeToString(MeasureSpec.getMode(widthMeasureSpec)),
MeasureSpec.getSize(widthMeasureSpec)));
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (DEBUG) {
layoutCount++;
Log.d("DebugView", String.format(
"onLayout #%d: [%d, %d, %d, %d] changed=%b",
layoutCount, l, t, r, b, changed));
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (DEBUG) {
drawCount++;
Log.d("DebugView", "onDraw #" + drawCount);
// 绘制调试信息
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setTextSize(24);
canvas.drawText("Measure: " + measureCount, 10, 30, paint);
canvas.drawText("Layout: " + layoutCount, 10, 60, paint);
canvas.drawText("Draw: " + drawCount, 10, 90, paint);
}
}
private String modeToString(int mode) {
switch (mode) {
case MeasureSpec.UNSPECIFIED: return "UNSPECIFIED";
case MeasureSpec.EXACTLY: return "EXACTLY";
case MeasureSpec.AT_MOST: return "AT_MOST";
default: return "UNKNOWN";
}
}
}
8.2 使用Android Studio的调试工具
- Layout Inspector:查看View的层次结构和属性
text
Tools → Layout Inspector
- GPU渲染模式分析:检查绘制性能
text
开发者选项 → GPU渲染模式分析 → 在屏幕上显示为条形图
- Profile GPU Rendering:更详细的渲染分析
text
Run → Profile 'app' → CPU/GPU
8.3 过度绘制检测
在开发者选项中开启"调试GPU过度绘制",颜色含义:
- 无颜色:没有过度绘制(理想状态)
- 蓝色:1次过度绘制(可以接受)
- 绿色:2次过度绘制(勉强接受)
- 粉色:3次过度绘制(需要优化)
- 红色:4次或更多过度绘制(必须优化)
九、总结与要点回顾
9.1 三大流程的核心要点总结
| 流程 | 关键方法 | 主要职责 | 注意事项 |
|---|---|---|---|
| 测量 | onMeasure() | 确定View的尺寸 | 必须调用setMeasuredDimension() |
| 布局 | onLayout() | 确定View的位置 | 必须调用子View的layout() |
| 绘制 | onDraw() | 绘制View的内容 | 避免过度绘制,优化性能 |
9.2 常见问题解决方案
问题1:wrap_content不生效
原因:直接设置了固定尺寸,没有根据内容计算尺寸 解决:在onMeasure中计算内容尺寸,根据测量模式调整
问题2:测量次数过多
原因:没有正确使用测量缓存 解决:缓存测量结果,MeasureSpec不变时直接使用缓存
问题3:布局错乱
原因:没有正确处理padding和margin 解决:在测量和布局时都要考虑padding和margin
问题4:绘制性能差
原因:过度绘制或复杂的绘制操作 解决:使用clipRect限制绘制区域,启用硬件加速
9.3 性能优化检查清单
- 是否正确处理了wrap_content?
- 是否考虑了padding和margin?
- 是否使用了测量缓存?
- 是否避免了不必要的测量?
- 是否使用clipRect限制绘制区域?
- 是否启用了硬件加速?
- 是否避免了过度绘制?
- 是否在onDraw中创建了新对象?
十、扩展学习
10.1 推荐阅读
-
Android源码
- View.java - 查看measure、layout、draw的实现
- ViewGroup.java - 学习ViewGroup的测量和布局逻辑
- FrameLayout.java、LinearLayout.java - 学习系统布局的实现
-
官方文档
-
开源项目
10.2 下一步学习方向
-
事件分发机制
- 理解onInterceptTouchEvent和onTouchEvent
- 学习多点触控和手势识别
-
属性动画
- 掌握ValueAnimator和ObjectAnimator
- 学习自定义插值器和估值器
-
高级绘制技术
- 学习Path的高级用法
- 掌握Shader和Xfermode
- 了解SurfaceView和TextureView
10.3 思考题
- 问题:如果一个View同时设置了layout_width="wrap_content"和minWidth属性,测量过程如何处理?
- 问题:如何实现一个支持权重(weight)的自定义LinearLayout?
- 问题:在RecyclerView中使用自定义View时,测量有哪些特别需要注意的地方?
- 挑战:实现一个自定义ViewGroup,要求子View可以重叠,且后添加的View显示在上层。
实践建议:
- 从简单的自定义View开始,逐步增加复杂度
- 使用View的post方法延迟操作,避免性能问题
- 为自定义View编写单元测试
- 在不同尺寸和密度的设备上测试你的View
记住:理解原理比记住API更重要。当你真正理解了Android View系统的工作原理,你就能创造出既美观又高性能的自定义View。
如果你在实现过程中遇到问题,或者有更好的实现方案,欢迎在评论区交流讨论。让我们一起进步,成为Android UI开发专家!