Android View 绘制机制完整详解
View绘制机制概述
什么是View绘制机制?
View绘制机制是Android系统将View从XML布局文件或代码中创建,经过测量(Measure)、布局(Layout)、绘制(Draw)三个阶段,最终显示在屏幕上的完整流程。
为什么需要View绘制机制?
- 确定View的尺寸:通过测量阶段确定View需要多大的空间
- 确定View的位置:通过布局阶段确定View在父容器中的位置
- 绘制View的内容:通过绘制阶段将View的内容绘制到屏幕上
- 支持动态更新:当View内容变化时,可以重新绘制
View绘制的三个阶段
View绘制流程:
↓
【1. Measure(测量)】- 回答"多大?"
↓ 确定View的宽高
↓ 调用onMeasure()
↓ 必须调用setMeasuredDimension()
【2. Layout(布局)】- 回答"在哪?"
↓ 确定View的位置
↓ 调用onLayout()
↓ 设置left, top, right, bottom
【3. Draw(绘制)】- 回答"画什么?"
↓ 绘制View的内容
↓ 调用onDraw()
↓ 使用Canvas绘制
【显示在屏幕上】✅
记忆口诀: "多大在哪画什么" - Measure确定多大,Layout确定在哪,Draw画什么内容
View绘制的设计思想
- 责任链模式:从根View开始,逐层向下传递测量和布局信息
- 自顶向下测量,自底向上布局:先测量子View,再确定父View的尺寸
- 缓存机制:避免重复测量和绘制,提高性能
View绘制的三个核心方法
方法列表
| 方法 | 作用 | 调用时机 | 返回值 |
|---|---|---|---|
onMeasure() | 测量View的宽高 | measure阶段 | void(通过setMeasuredDimension设置) |
onLayout() | 布局View的位置 | layout阶段 | void |
onDraw() | 绘制View的内容 | draw阶段 | void |
方法调用顺序
【完整调用链】
ViewRootImpl.performTraversals()
↓
┌─────────────────────────────────────┐
│ 【1. Measure阶段】- 测量尺寸 │
└─────────────────────────────────────┘
↓
View.measure(widthMeasureSpec, heightMeasureSpec)
↓
View.onMeasure(widthMeasureSpec, heightMeasureSpec)
↓ 必须调用 setMeasuredDimension(width, height)
↓
┌─────────────────────────────────────┐
│ 【2. Layout阶段】- 布局位置 │
└─────────────────────────────────────┘
↓
View.layout(left, top, right, bottom)
↓
View.onLayout(changed, l, t, r, b)
↓ 设置View的四个边界
↓
┌─────────────────────────────────────┐
│ 【3. Draw阶段】- 绘制内容 │
└─────────────────────────────────────┘
↓
View.draw(canvas)
↓
View.onDraw(canvas)
↓ 使用Canvas绘制内容
↓
【显示在屏幕上】✅
关键记忆点:
- Measure:先测量,确定尺寸
- Layout:再布局,确定位置
- Draw:最后绘制,显示内容
- 顺序固定:Measure → Layout → Draw,不能颠倒
三个方法的关系
- onMeasure():必须先执行,确定View的尺寸
- onLayout():在onMeasure()之后执行,确定View的位置
- onDraw():在onLayout()之后执行,绘制View的内容
View绘制流程详解
完整绘制流程
【1. 触发绘制】
↓
ViewRootImpl.performTraversals()
↓
【2. Measure阶段】
↓
performMeasure() → measure() → onMeasure()
↓
【3. Layout阶段】
↓
performLayout() → layout() → onLayout()
↓
【4. Draw阶段】
↓
performDraw() → draw() → onDraw()
↓
【5. 显示在屏幕上】
Measure阶段详解
流程:
ViewRootImpl.performMeasure()
↓
DecorView.measure()
↓
ViewGroup.measure()
↓
ViewGroup.onMeasure()
↓
遍历子View,调用子View.measure()
↓
子View.onMeasure()
↓
setMeasuredDimension()设置测量尺寸
关键点:
- 自顶向下测量:从根View开始,逐层向下
- 父View先测量子View,再根据子View确定自己的尺寸
- 通过MeasureSpec传递测量约束
Layout阶段详解
流程:
ViewRootImpl.performLayout()
↓
DecorView.layout()
↓
ViewGroup.layout()
↓
ViewGroup.onLayout()
↓
遍历子View,调用子View.layout()
↓
子View.onLayout()
↓
确定View的四个边界(left, top, right, bottom)
关键点:
- 自顶向下布局:从根View开始,逐层向下
- 父View确定子View的位置
- 通过layout()方法设置View的四个边界
Draw阶段详解
流程:
ViewRootImpl.performDraw()
↓
View.draw()
↓
【绘制背景】
drawBackground()
↓
【绘制内容】
onDraw()
↓
【绘制子View】
dispatchDraw()
↓
【绘制前景、滚动条等】
onDrawForeground()
关键点:
- 自顶向下绘制:从根View开始,逐层向下
- 先绘制背景,再绘制内容,最后绘制前景
- ViewGroup需要绘制子View(dispatchDraw)
MeasureSpec详解
什么是MeasureSpec?
MeasureSpec是View测量时的约束条件,包含两个信息:
- 测量模式(Mode):如何测量
- 测量尺寸(Size):测量的大小
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; // 未指定
public static final int EXACTLY = 1 << MODE_SHIFT; // 精确值
public static final int AT_MOST = 2 << MODE_SHIFT; // 最大不超过
// 高2位表示Mode,低30位表示Size
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
三种测量模式
1. EXACTLY(精确模式)✅
含义: View的尺寸已经确定,必须使用指定的尺寸
记忆口诀: "精确值,必须用"
使用场景:
- 指定了具体数值:
android:layout_width="100dp" - 指定了match_parent:父View已经确定了尺寸
代码示例:
int widthSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
// View的宽度必须是100,不能改变
处理方式:
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize; // 直接使用,不能改变
}
2. AT_MOST(最大模式)⚠️
含义: View的尺寸不能超过指定的尺寸,但可以小于
记忆口诀: "最大不超过,可以更小"
使用场景:
- 指定了wrap_content:View可以根据内容确定尺寸,但不能超过父View
代码示例:
int widthSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.AT_MOST);
// View的宽度不能超过200,但可以小于200(如150、100等)
处理方式:
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(calculateWidth(), widthSize); // 取较小值
}
3. UNSPECIFIED(未指定模式)🔓
含义: View的尺寸没有限制,可以任意大小
记忆口诀: "无限制,任意大"
使用场景:
- ScrollView中的子View:可以超出屏幕
- 自定义View的特殊情况
代码示例:
int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// View的宽度没有限制,可以任意大小
处理方式:
if (widthMode == MeasureSpec.UNSPECIFIED) {
width = calculateWidth(); // 使用计算值,没有限制
}
📊 三种模式对比表
| 模式 | 含义 | 使用场景 | 处理方式 | 记忆口诀 |
|---|---|---|---|---|
| EXACTLY | 精确值,必须用 | 具体数值、match_parent | width = widthSize | "精确值,必须用" |
| AT_MOST | 最大不超过,可以更小 | wrap_content | width = Math.min(计算值, widthSize) | "最大不超过,可以更小" |
| UNSPECIFIED | 无限制,任意大 | ScrollView子View | width = 计算值 | "无限制,任意大" |
MeasureSpec的传递
父View的MeasureSpec
↓
根据子View的layout_width/layout_height
↓
生成子View的MeasureSpec
↓
传递给子View.measure()
总结与记忆表格
| 子View的 LayoutParams | 父View的 MeasureSpec | 生成的子View MeasureSpec | 通俗解释 |
|---|---|---|---|
| match_parent | EXACTLY (父有精确尺寸) | EXACTLY (子也有精确尺寸) | “父子同心,一样大。” |
| match_parent | AT_MOST (父有上限) | AT_MOST (子也有上限) | “爸爸自己也在受限,你跟着我受限。” |
| wrap_content | EXACTLY (父有精确尺寸) | AT_MOST (子不能超过父) | “你想多小都行,但别超过我。” |
| wrap-content | AT_MOST (父有上限) | AT_MOST (子不能超过父) | “咱爷俩都有上限,都别超。” |
| 具体数值 (e.g., 100dp) | 任意模式 | EXACTLY (子有精确尺寸) | “你就按我说的这个尺寸来,别管我。” |
核心思想:
- 约束是向下传递的。父View从它的父View那里获得约束,再根据自己的规则“翻译”后传递给子View。
match_parent的子View,其约束取决于父View的状态。wrap_content的子View,总是收到一个有上限(AT_MOST)的约束。- 具体数值 是最强的“要求”,会直接转换为精确约束。
onMeasure()详解
onMeasure()的作用
onMeasure()用于测量View的宽高,必须在此方法中调用setMeasuredDimension()设置测量结果。
onMeasure()的调用时机
- View首次添加到View树时
- 调用
requestLayout()时 - 父View的尺寸发生变化时
- 子View的尺寸发生变化时
onMeasure()的实现
View的onMeasure()实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
protected int getDefaultSize(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:
case MeasureSpec.EXACTLY:
result = specSize; // 使用MeasureSpec指定的尺寸
break;
}
return result;
}
自定义View的onMeasure()实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
// 根据测量模式计算宽度
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize; // 精确值,直接使用
break;
case MeasureSpec.AT_MOST:
width = Math.min(calculateWidth(), widthSize); // 不能超过指定值
break;
case MeasureSpec.UNSPECIFIED:
width = calculateWidth(); // 没有限制,使用计算值
break;
}
// 根据测量模式计算高度
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
height = Math.min(calculateHeight(), heightSize);
break;
case MeasureSpec.UNSPECIFIED:
height = calculateHeight();
break;
}
// 必须调用setMeasuredDimension设置测量结果
setMeasuredDimension(width, height);
}
private int calculateWidth() {
// 根据View的内容计算宽度
return 200; // 示例
}
private int calculateHeight() {
// 根据View的内容计算高度
return 200; // 示例
}
onMeasure()的注意事项
- 必须调用setMeasuredDimension():否则会抛出异常
- 测量结果应该考虑padding:
width = contentWidth + paddingLeft + paddingRight - 不要修改MeasureSpec:只能读取,不能修改
- 避免无限递归:不要在onMeasure()中调用requestLayout()
onLayout()详解
onLayout()的作用
onLayout()用于布局View的位置,确定View在父容器中的位置(left, top, right, bottom)。
onLayout()的调用时机
- View首次添加到View树时
- 调用
requestLayout()时 - 父View的尺寸或位置发生变化时
onLayout()的实现
View的onLayout()实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// View的onLayout()是空实现
// ViewGroup需要重写此方法来布局子View
}
ViewGroup的onLayout()实现
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 遍历所有子View,调用子View的layout()方法
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 计算子View的位置
int childLeft = calculateChildLeft(i);
int childTop = calculateChildTop(i);
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
// 布局子View
child.layout(childLeft, childTop, childRight, childBottom);
}
}
}
layout()方法
public void layout(int l, int t, int r, int b) {
// 设置View的四个边界
setFrame(l, t, r, b);
// 调用onLayout()
onLayout(changed, l, t, r, b);
}
onLayout()的注意事项
- 使用getMeasuredWidth()和getMeasuredHeight():不要使用getWidth()和getHeight()
- 考虑padding:子View的位置应该考虑父View的padding
- 处理GONE的View:不需要布局GONE的View
- 避免无限递归:不要在onLayout()中调用requestLayout()
onDraw()详解
onDraw()的作用
onDraw()用于绘制View的内容,通过Canvas绘制各种图形、文字、图片等。
onDraw()的调用时机
- View首次显示时
- 调用
invalidate()时 - View的内容发生变化时
- 动画执行时
onDraw()的实现
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制背景(系统自动绘制)
// drawBackground(canvas);
// 绘制内容
canvas.drawColor(Color.WHITE); // 绘制背景色
canvas.drawCircle(100, 100, 50, paint); // 绘制圆形
canvas.drawText("Hello", 50, 50, paint); // 绘制文字
// 绘制子View(ViewGroup需要)
// dispatchDraw(canvas);
// 绘制前景、滚动条等(系统自动绘制)
// onDrawForeground(canvas);
}
View.draw()的完整流程
public void draw(Canvas canvas) {
// 1. 绘制背景
drawBackground(canvas);
// 2. 保存图层(如果需要)
if (!dirtyOpaque) {
drawShadow(canvas);
}
// 3. 绘制内容
onDraw(canvas);
// 4. 绘制子View(ViewGroup)
dispatchDraw(canvas);
// 5. 绘制前景、滚动条等
onDrawForeground(canvas);
}
onDraw()的注意事项
- 不要创建新对象:避免在onDraw()中创建Paint、Path等对象
- 使用缓存:复用Paint、Path等对象
- 避免过度绘制:不要绘制不必要的区域
- 使用硬件加速:利用GPU加速绘制
View绘制相关方法
invalidate()和requestLayout()
invalidate()
作用: 标记View需要重绘,触发onDraw()
使用场景:
- View的内容发生变化
- 需要更新显示
特点:
- 只触发draw阶段
- 不触发measure和layout阶段
- 会向上传播到ViewRootImpl
代码示例:
// 修改View的内容后,调用invalidate()重绘
textView.setText("新文本");
textView.invalidate(); // 触发onDraw()
requestLayout()
作用: 请求重新布局,触发measure和layout阶段
使用场景:
- View的尺寸发生变化
- View的位置需要调整
特点:
- 触发measure和layout阶段
- 也会触发draw阶段
- 会向上传播到ViewRootImpl
代码示例:
// 修改View的尺寸后,调用requestLayout()重新布局
view.setLayoutParams(new LayoutParams(200, 200));
view.requestLayout(); // 触发measure()和layout()
区别对比
| 方法 | 触发阶段 | 使用场景 | 性能影响 |
|---|---|---|---|
invalidate() | 只触发draw | 内容变化 | 较小 |
requestLayout() | 触发measure、layout、draw | 尺寸/位置变化 | 较大 |
getWidth()和getMeasuredWidth()
getWidth()
作用: 获取View的实际宽度(布局后的宽度)
计算方式: getWidth() = right - left
特点:
- 在layout阶段之后才有值
- 表示View在屏幕上的实际宽度
getMeasuredWidth()
作用: 获取View的测量宽度(测量后的宽度)
计算方式: getMeasuredWidth() = measuredWidth
特点:
- 在measure阶段之后就有值
- 表示View测量后的宽度
区别对比
| 方法 | 获取时机 | 值来源 | 使用场景 |
|---|---|---|---|
getWidth() | layout之后 | right - left | 布局时使用 |
getMeasuredWidth() | measure之后 | measuredWidth | 测量时使用 |
注意: 在onLayout()中应该使用getMeasuredWidth(),而不是getWidth()
自定义View绘制
自定义View的步骤
- 继承View或ViewGroup
- 重写onMeasure():测量View的尺寸
- 重写onLayout()(ViewGroup需要):布局子View
- 重写onDraw():绘制View的内容
- 处理触摸事件(可选):重写onTouchEvent()
自定义View示例
public class CustomView extends View {
private Paint paint;
public CustomView(Context context) {
super(context);
init();
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// 初始化Paint,避免在onDraw()中创建
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 200; // 默认宽度
int height = 200; // 默认高度
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制圆形
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = Math.min(getWidth(), getHeight()) / 2 - 20;
canvas.drawCircle(centerX, centerY, radius, paint);
}
}
自定义View的注意事项
- 不要在onDraw()中创建对象:应该提前创建并复用
- 正确处理padding:在onMeasure()和onDraw()中考虑padding
- 支持wrap_content:在onMeasure()中处理AT_MOST模式
- 避免过度绘制:只绘制必要的区域
- 使用硬件加速:在AndroidManifest.xml中启用
自定义ViewGroup绘制
自定义ViewGroup的步骤
- 继承ViewGroup
- 重写onMeasure():测量ViewGroup和子View的尺寸
- 重写onLayout():布局子View的位置
- 处理触摸事件(可选):重写onInterceptTouchEvent()和onTouchEvent()
自定义ViewGroup示例
public class CustomViewGroup extends ViewGroup {
public CustomViewGroup(Context context) {
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 测量所有子View
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 为子View生成MeasureSpec
int childWidthSpec = getChildMeasureSpec(
widthMeasureSpec,
getPaddingLeft() + getPaddingRight(),
child.getLayoutParams().width
);
int childHeightSpec = getChildMeasureSpec(
heightMeasureSpec,
getPaddingTop() + getPaddingBottom(),
child.getLayoutParams().height
);
// 测量子View
child.measure(childWidthSpec, childHeightSpec);
}
}
// 2. 根据子View确定自己的尺寸
int width = 0;
int height = 0;
// 计算所有子View的最大宽度和高度
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
width = Math.max(width, child.getMeasuredWidth());
height += child.getMeasuredHeight();
}
}
// 考虑padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
// 3. 根据MeasureSpec调整尺寸
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int currentTop = getPaddingTop();
// 布局所有子View
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childLeft = getPaddingLeft();
int childTop = currentTop;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
currentTop = childBottom;
}
}
}
}
自定义ViewGroup的注意事项
- 先测量子View,再确定自己的尺寸:在onMeasure()中先测量所有子View
- 使用getChildMeasureSpec():为子View生成正确的MeasureSpec
- 考虑padding和margin:在测量和布局时都要考虑
- 处理GONE的View:不需要测量和布局GONE的View
Canvas绘制详解
Canvas的绘制操作
1. 绘制基本图形
// 绘制点
canvas.drawPoint(x, y, paint);
// 绘制线
canvas.drawLine(startX, startY, endX, endY, paint);
// 绘制矩形
canvas.drawRect(left, top, right, bottom, paint);
// 绘制圆角矩形
canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint);
// 绘制圆形
canvas.drawCircle(centerX, centerY, radius, paint);
// 绘制椭圆
canvas.drawOval(left, top, right, bottom, paint);
// 绘制弧形
canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
2. 绘制文字
// 绘制文字
canvas.drawText(text, x, y, paint);
// 绘制多行文字
canvas.drawText(text, start, end, x, y, paint);
// 沿路径绘制文字
canvas.drawTextOnPath(text, path, hOffset, vOffset, paint);
3. 绘制图片
// 绘制Bitmap
canvas.drawBitmap(bitmap, left, top, paint);
// 绘制Bitmap(指定区域)
canvas.drawBitmap(bitmap, src, dst, paint);
// 绘制Drawable
drawable.draw(canvas);
4. 绘制路径
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 100);
path.quadTo(150, 50, 200, 100);
path.close();
canvas.drawPath(path, paint);
Paint的使用
Paint paint = new Paint();
paint.setAntiAlias(true); // 抗锯齿
paint.setColor(Color.BLACK); // 颜色
paint.setStyle(Paint.Style.FILL); // 填充模式
paint.setStrokeWidth(5); // 线条宽度
paint.setTextSize(30); // 文字大小
paint.setTypeface(Typeface.DEFAULT_BOLD); // 字体
Canvas的变换操作
// 平移
canvas.translate(dx, dy);
// 旋转
canvas.rotate(degrees);
canvas.rotate(degrees, px, py);
// 缩放
canvas.scale(sx, sy);
canvas.scale(sx, sy, px, py);
// 错切
canvas.skew(sx, sy);
// 保存和恢复
canvas.save();
// 进行变换
canvas.restore();
绘制优化
过度绘制(Overdraw)
什么是过度绘制?
过度绘制是指同一个像素被绘制了多次,造成性能浪费。
如何检测过度绘制?
- 开启开发者选项中的"显示过度绘制"
- 颜色含义:
- 蓝色:绘制1次(正常)
- 绿色:绘制2次(可接受)
- 浅红色:绘制3次(需要优化)
- 深红色:绘制4次以上(必须优化)
如何减少过度绘制?
- 移除不必要的背景
// 如果View的背景和父View相同,可以移除
view.setBackground(null);
- 使用clipRect()裁剪绘制区域
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
- 使用ViewStub延迟加载
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/heavy_layout" />
- 使用merge标签减少布局层级
<merge>
<TextView ... />
<ImageView ... />
</merge>
绘制性能优化
1. 避免在onDraw()中创建对象
// ❌ 错误:在onDraw()中创建对象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每次绘制都创建新对象
canvas.drawCircle(100, 100, 50, paint);
}
// ✅ 正确:提前创建并复用
private Paint paint = new Paint();
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(100, 100, 50, paint); // 复用对象
}
2. 使用硬件加速
<!-- AndroidManifest.xml -->
<application
android:hardwareAccelerated="true">
...
</application>
3. 使用缓存
// 使用Bitmap缓存绘制结果
private Bitmap cacheBitmap;
private Canvas cacheCanvas;
private void initCache() {
cacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(cacheBitmap);
}
@Override
protected void onDraw(Canvas canvas) {
if (cacheBitmap != null) {
// 先绘制到缓存
drawToCache(cacheCanvas);
// 再绘制到屏幕
canvas.drawBitmap(cacheBitmap, 0, 0, null);
}
}
4. 减少绘制区域
// 只绘制变化的区域
@Override
protected void onDraw(Canvas canvas) {
canvas.clipRect(dirtyRect); // 裁剪到脏区域
// 绘制内容
}
5. 使用invalidate()的带参数版本
// 只重绘指定区域
invalidate(left, top, right, bottom);
常见问题与解决方案
1. View不显示
原因:
- onMeasure()没有调用setMeasuredDimension()
- View的尺寸为0
- View被其他View遮挡
解决方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 确保调用了setMeasuredDimension()
setMeasuredDimension(width, height);
}
2. wrap_content不生效
原因:
- onMeasure()中没有处理AT_MOST模式
解决方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (widthMode == MeasureSpec.AT_MOST) {
width = calculateContentWidth(); // 计算内容宽度
width = Math.min(width, widthSize); // 不能超过父View
} else if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
}
setMeasuredDimension(width, height);
}
3. padding不生效
原因:
- onMeasure()和onDraw()中没有考虑padding
解决方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = contentWidth + getPaddingLeft() + getPaddingRight();
int height = contentHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int bottom = getHeight() - getPaddingBottom();
// 在考虑padding的区域内绘制
}
4. 内存泄漏
原因:
- Paint、Path等对象持有Context引用
- Bitmap没有及时回收
解决方案:
// 使用Application Context
private Context appContext;
public CustomView(Context context) {
super(context);
appContext = context.getApplicationContext();
}
// 及时回收Bitmap
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
快速查询表
MeasureSpec快速查询
| layout_width/layout_height | 父View的Mode | 子View的Mode | 子View的Size |
|---|---|---|---|
100dp (具体数值) | 任意 | EXACTLY | 100dp |
match_parent | EXACTLY | EXACTLY | 父View的Size |
match_parent | AT_MOST | AT_MOST | 父View的Size |
wrap_content | EXACTLY | AT_MOST | 父View的Size |
wrap_content | AT_MOST | AT_MOST | 父View的Size |
三个方法快速查询
| 方法 | 作用 | 必须做什么 | 使用什么方法获取尺寸 |
|---|---|---|---|
onMeasure() | 测量尺寸 | 调用setMeasuredDimension() | 使用MeasureSpec.getSize() |
onLayout() | 布局位置 | 调用child.layout() | 使用getMeasuredWidth() |
onDraw() | 绘制内容 | 使用Canvas绘制 | 使用getWidth()和getHeight() |
常见方法对比
| 方法 | 获取时机 | 值来源 | 使用场景 |
|---|---|---|---|
getWidth() | layout之后 | right - left | onDraw()中使用 |
getMeasuredWidth() | measure之后 | measuredWidth | onLayout()中使用 |
getSuggestedMinimumWidth() | 任何时候 | 背景Drawable或minWidth | onMeasure()中计算最小尺寸 |
记忆口诀
三个阶段的记忆
"多大在哪画什么"
- Measure(多大):确定View的尺寸
- Layout(在哪):确定View的位置
- Draw(画什么):绘制View的内容
三种模式的记忆
"精确最大无限制"
- EXACTLY(精确):精确值,必须用
- AT_MOST(最大):最大不超过,可以更小
- UNSPECIFIED(无限制):无限制,任意大
三个方法的记忆
"测量布局绘制"
- onMeasure():测量尺寸,必须调用setMeasuredDimension()
- onLayout():布局位置,设置left/top/right/bottom
- onDraw():绘制内容,使用Canvas
MeasureSpec传递规则记忆
"具体数值精确,match_parent跟随,wrap_content最大"
- 具体数值 → EXACTLY
- match_parent → 跟随父View的Mode
- wrap_content → AT_MOST
常见错误对比
❌ 错误1:onMeasure()中忘记调用setMeasuredDimension()
// ❌ 错误:忘记调用setMeasuredDimension()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 200;
int height = 200;
// 忘记调用setMeasuredDimension(),会抛出异常!
}
// ✅ 正确:必须调用setMeasuredDimension()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 200;
int height = 200;
setMeasuredDimension(width, height); // ✅ 必须调用
}
❌ 错误2:wrap_content不生效
// ❌ 错误:没有处理AT_MOST模式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 200; // 固定值,wrap_content不生效
int height = 200;
setMeasuredDimension(width, height);
}
// ✅ 正确:处理AT_MOST模式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (widthMode == MeasureSpec.AT_MOST) {
width = calculateContentWidth(); // 计算内容宽度
width = Math.min(width, widthSize); // 不能超过父View
} else if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
}
setMeasuredDimension(width, height);
}
❌ 错误3:在onDraw()中创建对象
// ❌ 错误:在onDraw()中创建对象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // ❌ 每次绘制都创建新对象
canvas.drawCircle(100, 100, 50, paint);
}
// ✅ 正确:提前创建并复用
private Paint paint = new Paint(); // ✅ 提前创建
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(100, 100, 50, paint); // ✅ 复用对象
}
❌ 错误4:padding不生效
// ❌ 错误:没有考虑padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = contentWidth; // ❌ 没有考虑padding
int height = contentHeight;
setMeasuredDimension(width, height);
}
// ✅ 正确:考虑padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = contentWidth + getPaddingLeft() + getPaddingRight(); // ✅ 考虑padding
int height = contentHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
}
❌ 错误5:在onLayout()中使用getWidth()
// ❌ 错误:在onLayout()中使用getWidth()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childWidth = child.getWidth(); // ❌ getWidth()此时可能为0
child.layout(0, 0, childWidth, childHeight);
}
// ✅ 正确:使用getMeasuredWidth()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childWidth = child.getMeasuredWidth(); // ✅ 使用测量宽度
child.layout(0, 0, childWidth, childHeight);
}
实战案例
案例1:自定义圆形View
public class CircleView extends View {
private Paint paint;
private int radius = 50;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// ✅ 提前创建Paint,不在onDraw()中创建
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
// ✅ 处理三种测量模式
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
// ✅ 支持wrap_content
width = Math.min(radius * 2 + getPaddingLeft() + getPaddingRight(), widthSize);
} else {
width = radius * 2 + getPaddingLeft() + getPaddingRight();
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(radius * 2 + getPaddingTop() + getPaddingBottom(), heightSize);
} else {
height = radius * 2 + getPaddingTop() + getPaddingBottom();
}
// ✅ 必须调用setMeasuredDimension()
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ✅ 考虑padding
int centerX = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int centerY = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
int actualRadius = Math.min(
(getWidth() - getPaddingLeft() - getPaddingRight()) / 2,
(getHeight() - getPaddingTop() - getPaddingBottom()) / 2
);
// ✅ 复用Paint对象
canvas.drawCircle(centerX, centerY, actualRadius, paint);
}
}
案例2:自定义垂直布局ViewGroup
public class VerticalLayout extends ViewGroup {
public VerticalLayout(Context context) {
super(context);
}
public VerticalLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
int totalHeight = 0;
int maxWidth = 0;
// ✅ 1. 先测量所有子View
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// ✅ 为子View生成MeasureSpec
int childWidthSpec = getChildMeasureSpec(
widthMeasureSpec,
getPaddingLeft() + getPaddingRight(),
child.getLayoutParams().width
);
int childHeightSpec = getChildMeasureSpec(
heightMeasureSpec,
getPaddingTop() + getPaddingBottom(),
child.getLayoutParams().height
);
child.measure(childWidthSpec, childHeightSpec);
// 累计高度和最大宽度
totalHeight += child.getMeasuredHeight();
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
}
}
// ✅ 2. 根据子View确定自己的尺寸
int width = maxWidth + getPaddingLeft() + getPaddingRight();
int height = totalHeight + getPaddingTop() + getPaddingBottom();
// ✅ 3. 根据MeasureSpec调整尺寸
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int currentTop = getPaddingTop();
// ✅ 布局所有子View
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// ✅ 使用getMeasuredWidth(),不是getWidth()
int childLeft = getPaddingLeft();
int childTop = currentTop;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
currentTop = childBottom;
}
}
}
}
最佳实践
1. 测量阶段
- ✅ 正确处理三种测量模式(EXACTLY、AT_MOST、UNSPECIFIED)
- ✅ 支持wrap_content
- ✅ 考虑padding
- ✅ 必须调用setMeasuredDimension()
2. 布局阶段
- ✅ 使用getMeasuredWidth()和getMeasuredHeight()
- ✅ 考虑padding和margin
- ✅ 处理GONE的View
3. 绘制阶段
- ✅ 不在onDraw()中创建对象
- ✅ 复用Paint、Path等对象
- ✅ 避免过度绘制
- ✅ 使用硬件加速
4. 性能优化
- ✅ 减少布局层级
- ✅ 使用ViewStub延迟加载
- ✅ 使用merge标签
- ✅ 合理使用缓存
学习路径建议
🎯 学习路径(从易到难)
阶段1:基础理解(1-2天)
- ✅ 理解View绘制的三个阶段(Measure、Layout、Draw)
- ✅ 理解三个核心方法的作用(onMeasure、onLayout、onDraw)
- ✅ 理解MeasureSpec的三种模式
学习资源:
- 阅读本文档的"View绘制机制概述"和"View绘制的三个核心方法"章节
- 理解"多大在哪画什么"的记忆口诀
阶段2:深入理解(2-3天)
- ✅ 深入理解onMeasure()的实现
- ✅ 深入理解onLayout()的实现
- ✅ 深入理解onDraw()的实现
- ✅ 理解MeasureSpec的传递规则
学习资源:
- 阅读本文档的"onMeasure()详解"、"onLayout()详解"、"onDraw()详解"章节
- 阅读"MeasureSpec详解"章节
- 理解"精确最大无限制"的记忆口诀
阶段3:实践应用(3-5天)
- ✅ 实现简单的自定义View(如圆形View)
- ✅ 实现简单的自定义ViewGroup(如垂直布局)
- ✅ 处理wrap_content和padding
- ✅ 优化绘制性能
学习资源:
- 阅读本文档的"自定义View绘制"和"自定义ViewGroup绘制"章节
- 参考"实战案例"章节
- 参考"常见错误对比"章节,避免常见错误
阶段4:高级优化(2-3天)
- ✅ 理解绘制优化原理
- ✅ 掌握过度绘制的检测和优化
- ✅ 掌握性能优化技巧
- ✅ 掌握调试技巧
学习资源:
- 阅读本文档的"绘制优化"章节
- 阅读"最佳实践"章节
- 使用Android Studio的调试工具
阶段5:面试准备(1-2天)
- ✅ 复习所有面试题
- ✅ 理解常见问题的答案
- ✅ 准备实际项目经验
学习资源:
- 阅读本文档的"面试题大全"章节
- 复习"快速查询表"和"记忆口诀"
📚 推荐学习顺序
第1天:基础理解
├─ View绘制机制概述
├─ View绘制的三个核心方法
└─ View绘制流程详解
第2-3天:深入理解
├─ MeasureSpec详解
├─ onMeasure()详解
├─ onLayout()详解
└─ onDraw()详解
第4-5天:实践应用
├─ 自定义View绘制
├─ 自定义ViewGroup绘制
└─ 实战案例
第6-7天:高级优化
├─ 绘制优化
├─ 常见问题与解决方案
└─ 最佳实践
第8天:面试准备
└─ 面试题大全
💡 学习技巧
- 理解记忆口诀:使用"多大在哪画什么"等口诀快速记忆
- 对比学习:使用"常见错误对比"章节,理解错误和正确的区别
- 实践为主:多写代码,参考"实战案例"章节
- 快速查询:使用"快速查询表"快速查找知识点
- 面试准备:重点复习"面试题大全"章节
API索引
View绘制核心API
Measure相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
measure(int widthMeasureSpec, int heightMeasureSpec) | View | 测量View的尺寸 | 由系统调用,一般不需要手动调用 |
onMeasure(int widthMeasureSpec, int heightMeasureSpec) | View | 测量View的尺寸(需重写) | 自定义View时必须重写 |
setMeasuredDimension(int measuredWidth, int measuredHeight) | View | 设置测量结果 | onMeasure()中必须调用 |
getMeasuredWidth() | View | 获取测量宽度 | onLayout()中使用 |
getMeasuredHeight() | View | 获取测量高度 | onLayout()中使用 |
getSuggestedMinimumWidth() | View | 获取建议的最小宽度 | onMeasure()中计算最小尺寸 |
getSuggestedMinimumHeight() | View | 获取建议的最小高度 | onMeasure()中计算最小高度 |
Layout相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
layout(int l, int t, int r, int b) | View | 布局View的位置 | 由系统调用,ViewGroup中调用子View的layout() |
onLayout(boolean changed, int l, int t, int r, int b) | View | 布局View的位置(需重写) | ViewGroup必须重写 |
getWidth() | View | 获取实际宽度 | onDraw()中使用 |
getHeight() | View | 获取实际高度 | onDraw()中使用 |
getLeft() | View | 获取左边界 | 获取View位置 |
getTop() | View | 获取上边界 | 获取View位置 |
getRight() | View | 获取右边界 | 获取View位置 |
getBottom() | View | 获取下边界 | 获取View位置 |
Draw相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
draw(Canvas canvas) | View | 绘制View(系统调用) | 由系统调用,包含完整绘制流程 |
onDraw(Canvas canvas) | View | 绘制View内容(需重写) | 自定义View时重写 |
dispatchDraw(Canvas canvas) | ViewGroup | 绘制子View | ViewGroup绘制子View |
invalidate() | View | 标记需要重绘 | View内容变化时调用 |
invalidate(int l, int t, int r, int b) | View | 标记指定区域需要重绘 | 只重绘指定区域 |
postInvalidate() | View | 在非UI线程中标记重绘 | 非UI线程中调用 |
postInvalidate(int l, int t, int r, int b) | View | 在非UI线程中标记指定区域重绘 | 非UI线程中调用 |
MeasureSpec相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
MeasureSpec.makeMeasureSpec(int size, int mode) | MeasureSpec | 创建MeasureSpec | 手动创建MeasureSpec |
MeasureSpec.getMode(int measureSpec) | MeasureSpec | 获取测量模式 | 从MeasureSpec中提取Mode |
MeasureSpec.getSize(int measureSpec) | MeasureSpec | 获取测量尺寸 | 从MeasureSpec中提取Size |
getChildMeasureSpec(int spec, int padding, int childDimension) | ViewGroup | 为子View生成MeasureSpec | ViewGroup中为子View生成MeasureSpec |
resolveSize(int size, int measureSpec) | View | 根据MeasureSpec解析尺寸 | onMeasure()中调整尺寸 |
ViewGroup相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
getChildCount() | ViewGroup | 获取子View数量 | 遍历子View |
getChildAt(int index) | ViewGroup | 获取指定位置的子View | 访问子View |
addView(View child) | ViewGroup | 添加子View | 动态添加子View |
removeView(View view) | ViewGroup | 移除子View | 动态移除子View |
removeAllViews() | ViewGroup | 移除所有子View | 清空所有子View |
Padding相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
getPaddingLeft() | View | 获取左内边距 | onMeasure()和onDraw()中使用 |
getPaddingTop() | View | 获取上内边距 | onMeasure()和onDraw()中使用 |
getPaddingRight() | View | 获取右内边距 | onMeasure()和onDraw()中使用 |
getPaddingBottom() | View | 获取下内边距 | onMeasure()和onDraw()中使用 |
setPadding(int left, int top, int right, int bottom) | View | 设置内边距 | 设置View的内边距 |
布局相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
requestLayout() | View | 请求重新布局 | View尺寸或位置变化时调用 |
forceLayout() | View | 强制重新布局 | 强制View重新布局 |
isLayoutRequested() | View | 检查是否需要布局 | 检查View是否需要布局 |
Canvas绘制API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
drawColor(int color) | Canvas | 绘制颜色 | 绘制背景色 |
drawCircle(float cx, float cy, float radius, Paint paint) | Canvas | 绘制圆形 | 绘制圆形 |
drawRect(float left, float top, float right, float bottom, Paint paint) | Canvas | 绘制矩形 | 绘制矩形 |
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) | Canvas | 绘制线条 | 绘制线条 |
drawText(String text, float x, float y, Paint paint) | Canvas | 绘制文字 | 绘制文字 |
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) | Canvas | 绘制图片 | 绘制图片 |
drawPath(Path path, Paint paint) | Canvas | 绘制路径 | 绘制复杂路径 |
clipRect(float left, float top, float right, float bottom) | Canvas | 裁剪绘制区域 | 减少过度绘制 |
save() | Canvas | 保存Canvas状态 | 保存变换状态 |
restore() | Canvas | 恢复Canvas状态 | 恢复变换状态 |
translate(float dx, float dy) | Canvas | 平移 | 变换Canvas |
rotate(float degrees) | Canvas | 旋转 | 变换Canvas |
scale(float sx, float sy) | Canvas | 缩放 | 变换Canvas |
Paint相关API
| API | 所属类 | 作用 | 使用场景 |
|---|---|---|---|
setColor(int color) | Paint | 设置颜色 | 设置绘制颜色 |
setStyle(Paint.Style style) | Paint | 设置样式 | 设置填充或描边 |
setStrokeWidth(float width) | Paint | 设置线条宽度 | 设置描边宽度 |
setTextSize(float textSize) | Paint | 设置文字大小 | 设置文字大小 |
setAntiAlias(boolean aa) | Paint | 设置抗锯齿 | 启用抗锯齿 |
setTypeface(Typeface typeface) | Paint | 设置字体 | 设置文字字体 |
方法对比表
getWidth() vs getMeasuredWidth()
| 方法 | 获取时机 | 值来源 | 使用场景 | 注意事项 |
|---|---|---|---|---|
getWidth() | layout之后 | right - left | onDraw()中使用 | 在onLayout()中可能为0 |
getMeasuredWidth() | measure之后 | measuredWidth | onLayout()中使用 | 测量后的宽度,可能不等于实际宽度 |
使用建议:
- ✅ onLayout()中使用
getMeasuredWidth() - ✅ onDraw()中使用
getWidth() - ❌ 不要在onLayout()中使用
getWidth()
invalidate() vs requestLayout()
| 方法 | 触发阶段 | 使用场景 | 性能影响 | 调用时机 |
|---|---|---|---|---|
invalidate() | 只触发draw | View内容变化 | 较小 | 内容变化时 |
requestLayout() | 触发measure、layout、draw | View尺寸/位置变化 | 较大 | 尺寸/位置变化时 |
使用建议:
- ✅ 内容变化时使用
invalidate() - ✅ 尺寸/位置变化时使用
requestLayout() - ❌ 不要在不必要时调用
requestLayout()
onMeasure() vs onLayout() vs onDraw()
| 方法 | 作用 | 必须做什么 | 使用什么获取尺寸 | 调用顺序 |
|---|---|---|---|---|
onMeasure() | 测量尺寸 | 调用setMeasuredDimension() | 使用MeasureSpec.getSize() | 第1个 |
onLayout() | 布局位置 | 调用child.layout() | 使用getMeasuredWidth() | 第2个 |
onDraw() | 绘制内容 | 使用Canvas绘制 | 使用getWidth()和getHeight() | 第3个 |
MeasureSpec三种模式对比
| 模式 | 含义 | 使用场景 | 处理方式 | 记忆口诀 |
|---|---|---|---|---|
| EXACTLY | 精确值,必须用 | 具体数值、match_parent | width = widthSize | "精确值,必须用" |
| AT_MOST | 最大不超过,可以更小 | wrap_content | width = Math.min(计算值, widthSize) | "最大不超过,可以更小" |
| UNSPECIFIED | 无限制,任意大 | ScrollView子View | width = 计算值 | "无限制,任意大" |
layout_width/layout_height与MeasureSpec的对应关系
| layout_width/layout_height | 父View的Mode | 子View的Mode | 子View的Size |
|---|---|---|---|
100dp (具体数值) | 任意 | EXACTLY | 100dp |
match_parent | EXACTLY | EXACTLY | 父View的Size |
match_parent | AT_MOST | AT_MOST | 父View的Size |
wrap_content | EXACTLY | AT_MOST | 父View的Size |
wrap_content | AT_MOST | AT_MOST | 父View的Size |
面试题大全
一、基础概念(10题)
1. View的绘制流程是什么?
答案: View的绘制流程分为三个阶段:
- Measure(测量):确定View的宽高
- Layout(布局):确定View的位置
- Draw(绘制):绘制View的内容
详细流程:
ViewRootImpl.performTraversals()
↓
performMeasure() → measure() → onMeasure()
↓
performLayout() → layout() → onLayout()
↓
performDraw() → draw() → onDraw()
2. onMeasure()、onLayout()、onDraw()的作用是什么?
答案:
- onMeasure():测量View的宽高,必须调用setMeasuredDimension()设置测量结果
- onLayout():布局View的位置,确定View在父容器中的位置(left, top, right, bottom)
- onDraw():绘制View的内容,通过Canvas绘制各种图形、文字、图片等
3. View的MeasureSpec是什么?
答案: MeasureSpec是View测量时的约束条件,包含两个信息:
- 测量模式(Mode):EXACTLY(精确值)、AT_MOST(最大不超过)、UNSPECIFIED(未指定)
- 测量尺寸(Size):测量的大小
MeasureSpec用一个int值表示,高2位表示Mode,低30位表示Size。
4. View的测量模式有哪些?
答案: 三种测量模式:
- EXACTLY(精确模式):View的尺寸已经确定,必须使用指定的尺寸
- AT_MOST(最大模式):View的尺寸不能超过指定的尺寸,但可以小于
- UNSPECIFIED(未指定模式):View的尺寸没有限制,可以任意大小
5. View的布局流程是什么?
答案: 布局流程:
- ViewRootImpl调用performLayout()
- 从根View开始,调用View.layout()
- View.layout()调用onLayout()
- ViewGroup在onLayout()中遍历子View,调用子View.layout()
- 确定View的四个边界(left, top, right, bottom)
6. invalidate()和requestLayout()的区别是什么?
答案:
- invalidate():只触发draw阶段,用于View内容变化时重绘
- requestLayout():触发measure、layout、draw三个阶段,用于View尺寸或位置变化时重新布局
对比:
| 方法 | 触发阶段 | 使用场景 | 性能影响 |
|---|---|---|---|
| invalidate() | 只触发draw | 内容变化 | 较小 |
| requestLayout() | 触发measure、layout、draw | 尺寸/位置变化 | 较大 |
7. getWidth()和getMeasuredWidth()的区别是什么?
答案:
- getWidth():获取View的实际宽度(布局后的宽度),计算方式:
right - left,在layout阶段之后才有值 - getMeasuredWidth():获取View的测量宽度(测量后的宽度),在measure阶段之后就有值
注意: 在onLayout()中应该使用getMeasuredWidth(),而不是getWidth()
8. View的绘制顺序是什么?
答案: View.draw()的绘制顺序:
- 绘制背景(drawBackground)
- 绘制内容(onDraw)
- 绘制子View(dispatchDraw,ViewGroup才有)
- 绘制前景、滚动条等(onDrawForeground)
9. ViewGroup的绘制流程和View有什么区别?
答案: ViewGroup继承自View,绘制流程基本相同,但有以下区别:
- onMeasure():ViewGroup需要先测量所有子View,再根据子View确定自己的尺寸
- onLayout():ViewGroup需要布局所有子View的位置
- dispatchDraw():ViewGroup需要绘制子View(View的dispatchDraw()是空实现)
10. 为什么View的onLayout()是空实现?
答案: 因为View是叶子节点,没有子View,所以不需要布局子View。ViewGroup需要重写onLayout()来布局子View。
二、MeasureSpec详解(8题)
11. MeasureSpec的三种模式分别对应什么场景?
答案:
- EXACTLY:
- 指定了具体数值:
android:layout_width="100dp" - 指定了match_parent:父View已经确定了尺寸
- 指定了具体数值:
- AT_MOST:
- 指定了wrap_content:View可以根据内容确定尺寸,但不能超过父View
- UNSPECIFIED:
- ScrollView中的子View:可以超出屏幕
- 自定义View的特殊情况
12. 父View的MeasureSpec如何传递给子View?
答案: 根据子View的layout_width/layout_height和父View的MeasureSpec,生成子View的MeasureSpec:
match_parent+ 父View的EXACTLY → 子View的EXACTLYmatch_parent+ 父View的AT_MOST → 子View的AT_MOSTwrap_content+ 父View的EXACTLY → 子View的AT_MOSTwrap_content+ 父View的AT_MOST → 子View的AT_MOST- 具体数值 → 子View的EXACTLY
13. 如何从MeasureSpec中获取Mode和Size?
答案:
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
14. 如何创建MeasureSpec?
答案:
int measureSpec = MeasureSpec.makeMeasureSpec(size, mode);
15. ViewGroup如何为子View生成MeasureSpec?
答案:
使用getChildMeasureSpec()方法:
int childWidthSpec = ViewGroup.getChildMeasureSpec(
parentWidthMeasureSpec,
padding,
childLayoutParams.width
);
16. wrap_content为什么需要处理AT_MOST模式?
答案: 因为wrap_content时,父View会传递AT_MOST模式的MeasureSpec,表示View的尺寸不能超过父View,但可以根据内容确定。如果不处理AT_MOST模式,wrap_content就不会生效。
17. 自定义View如何支持wrap_content?
答案: 在onMeasure()中处理AT_MOST模式:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (widthMode == MeasureSpec.AT_MOST) {
width = calculateContentWidth(); // 计算内容宽度
width = Math.min(width, widthSize); // 不能超过父View
} else if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
}
setMeasuredDimension(width, height);
18. UNSPECIFIED模式的使用场景是什么?
答案:
- ScrollView中的子View:可以超出屏幕,没有尺寸限制
- 自定义View的特殊情况:需要无限大的尺寸
- 测量View的最大尺寸:用于确定View的最大可能尺寸
三、onMeasure()详解(10题)
19. onMeasure()的调用时机是什么?
答案:
- View首次添加到ViewTree时
- 调用requestLayout()时
- 父View的尺寸发生变化时
- 子View的尺寸发生变化时
20. onMeasure()中必须做什么?
答案:
必须调用setMeasuredDimension(width, height)设置测量结果,否则会抛出异常。
21. 自定义View的onMeasure()如何实现?
答案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
// 根据测量模式计算宽度
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
width = Math.min(calculateWidth(), widthSize);
break;
case MeasureSpec.UNSPECIFIED:
width = calculateWidth();
break;
}
// 根据测量模式计算高度
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
height = Math.min(calculateHeight(), heightSize);
break;
case MeasureSpec.UNSPECIFIED:
height = calculateHeight();
break;
}
setMeasuredDimension(width, height);
}
22. onMeasure()中如何考虑padding?
答案:
int width = contentWidth + getPaddingLeft() + getPaddingRight();
int height = contentHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
23. 为什么不能在onMeasure()中调用requestLayout()?
答案: 会导致无限递归:
- onMeasure()中调用requestLayout()
- requestLayout()触发measure()
- measure()又调用onMeasure()
- 形成无限循环
24. View的默认onMeasure()实现是什么?
答案:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
25. getSuggestedMinimumWidth()的作用是什么?
答案: 返回View建议的最小宽度,考虑背景Drawable的最小宽度和minWidth属性。
26. ViewGroup的onMeasure()如何实现?
答案:
- 遍历所有子View,调用子View.measure()测量子View
- 根据子View的尺寸确定自己的尺寸
- 调用setMeasuredDimension()设置自己的尺寸
27. 如何测量GONE的View?
答案: 不需要测量GONE的View,在onMeasure()中跳过:
if (child.getVisibility() != GONE) {
child.measure(childWidthSpec, childHeightSpec);
}
28. 自定义ViewGroup的onMeasure()如何实现?
答案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 测量所有子View
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childWidthSpec = getChildMeasureSpec(...);
int childHeightSpec = getChildMeasureSpec(...);
child.measure(childWidthSpec, childHeightSpec);
}
}
// 2. 根据子View确定自己的尺寸
int width = calculateWidthFromChildren();
int height = calculateHeightFromChildren();
// 3. 根据MeasureSpec调整尺寸
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
四、onLayout()详解(8题)
29. onLayout()的调用时机是什么?
答案:
- View首次添加到ViewTree时
- 调用requestLayout()时
- 父View的尺寸或位置发生变化时
30. onLayout()的参数含义是什么?
答案:
protected void onLayout(boolean changed, int l, int t, int r, int b)
changed:View的尺寸或位置是否发生变化l, t, r, b:View的四个边界(left, top, right, bottom)
31. 自定义View的onLayout()如何实现?
答案: View的onLayout()是空实现,因为View是叶子节点,没有子View。自定义View一般不需要重写onLayout()。
32. 自定义ViewGroup的onLayout()如何实现?
答案:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 计算子View的位置
int childLeft = calculateChildLeft(i);
int childTop = calculateChildTop(i);
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
// 布局子View
child.layout(childLeft, childTop, childRight, childBottom);
}
}
}
33. layout()方法的作用是什么?
答案: 设置View的四个边界(left, top, right, bottom),并调用onLayout()。
34. 在onLayout()中应该使用getWidth()还是getMeasuredWidth()?
答案:
应该使用getMeasuredWidth(),因为:
- getMeasuredWidth()在measure阶段之后就有值
- getWidth()在layout阶段之后才有值
- onLayout()在layout阶段执行,此时getWidth()可能还没有值
35. onLayout()中如何考虑padding?
答案:
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
int childRight = getWidth() - getPaddingRight();
int childBottom = getHeight() - getPaddingBottom();
36. 如何布局GONE的View?
答案: 不需要布局GONE的View,在onLayout()中跳过。
五、onDraw()详解(10题)
37. onDraw()的调用时机是什么?
答案:
- View首次显示时
- 调用invalidate()时
- View的内容发生变化时
- 动画执行时
38. 自定义View的onDraw()如何实现?
答案:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制内容
canvas.drawColor(Color.WHITE);
canvas.drawCircle(100, 100, 50, paint);
canvas.drawText("Hello", 50, 50, paint);
}
39. View.draw()的完整流程是什么?
答案:
public void draw(Canvas canvas) {
// 1. 绘制背景
drawBackground(canvas);
// 2. 绘制内容
onDraw(canvas);
// 3. 绘制子View(ViewGroup)
dispatchDraw(canvas);
// 4. 绘制前景、滚动条等
onDrawForeground(canvas);
}
40. 为什么不能在onDraw()中创建对象?
答案: 因为onDraw()会被频繁调用,在onDraw()中创建对象会导致:
- 频繁创建对象,增加GC压力
- 影响绘制性能
- 可能导致内存泄漏
正确做法: 提前创建对象并复用。
41. Canvas的绘制操作有哪些?
答案:
- 绘制基本图形:drawPoint、drawLine、drawRect、drawCircle等
- 绘制文字:drawText
- 绘制图片:drawBitmap
- 绘制路径:drawPath
- 变换操作:translate、rotate、scale、skew
42. Paint的使用注意事项是什么?
答案:
- 提前创建Paint对象,不要在onDraw()中创建
- 复用Paint对象
- 合理设置Paint的属性(颜色、样式、抗锯齿等)
43. Canvas的save()和restore()的作用是什么?
答案:
save():保存当前Canvas的状态(变换矩阵、裁剪区域等)restore():恢复之前保存的Canvas状态
用于在变换Canvas后恢复原始状态。
44. 如何实现自定义View的动画效果?
答案:
- 使用ValueAnimator或ObjectAnimator
- 在动画回调中更新View的状态
- 调用invalidate()重绘
- 在onDraw()中根据状态绘制
45. ViewGroup如何绘制子View?
答案:
ViewGroup的draw()方法会调用dispatchDraw(),dispatchDraw()会遍历所有子View,调用子View的draw()方法。
46. 如何优化onDraw()的性能?
答案:
- 不在onDraw()中创建对象
- 复用Paint、Path等对象
- 使用clipRect()裁剪绘制区域
- 使用硬件加速
- 减少不必要的绘制
六、绘制优化(10题)
47. 过度绘制(Overdraw)是什么?
答案: 过度绘制是指同一个像素被绘制了多次,造成性能浪费。
48. 如何检测过度绘制?
答案:
- 开启开发者选项中的"显示过度绘制"
- 颜色含义:
- 蓝色:绘制1次(正常)
- 绿色:绘制2次(可接受)
- 浅红色:绘制3次(需要优化)
- 深红色:绘制4次以上(必须优化)
49. 如何减少过度绘制?
答案:
- 移除不必要的背景
- 使用clipRect()裁剪绘制区域
- 使用ViewStub延迟加载
- 使用merge标签减少布局层级
- 使用invalidate()的带参数版本
50. 自定义View的绘制优化有哪些?
答案:
- 不在onDraw()中创建对象
- 复用Paint、Path等对象
- 使用硬件加速
- 使用缓存(Bitmap缓存)
- 减少绘制区域(clipRect)
- 避免不必要的绘制
51. 图片的绘制优化有哪些?
答案:
- 使用合适的图片格式(WebP、PNG、JPG)
- 压缩图片大小
- 使用图片缓存(LruCache)
- 使用缩略图
- 异步加载图片
52. 动画的绘制优化有哪些?
答案:
- 使用硬件加速
- 使用属性动画代替补间动画
- 减少动画的复杂度
- 使用ViewPropertyAnimator
- 避免在动画中创建对象
53. 绘制优化的最佳实践有哪些?
答案:
- 减少布局层级
- 使用ViewStub延迟加载
- 使用merge标签
- 合理使用缓存
- 使用硬件加速
- 避免过度绘制
- 优化onDraw()方法
54. 绘制优化的监控如何实现?
答案:
- 使用Systrace监控绘制性能
- 使用GPU渲染模式分析
- 使用Layout Inspector分析布局
- 使用过度绘制检测工具
- 使用性能分析工具(Android Profiler)
55. 硬件加速的作用是什么?
答案: 硬件加速利用GPU进行绘制,提高绘制性能,减少CPU负担。
56. 如何启用硬件加速?
答案:
<!-- AndroidManifest.xml -->
<application
android:hardwareAccelerated="true">
...
</application>
七、自定义View实践(10题)
57. 自定义View的方式有哪些?
答案:
- 继承View:实现完全自定义的View
- 继承ViewGroup:实现自定义的布局容器
- 组合现有View:通过组合多个View实现
- 继承现有View:扩展现有View的功能
58. 自定义View的步骤是什么?
答案:
- 继承View或ViewGroup
- 重写onMeasure():测量View的尺寸
- 重写onLayout()(ViewGroup需要):布局子View
- 重写onDraw():绘制View的内容
- 处理触摸事件(可选):重写onTouchEvent()
59. 自定义View的性能优化有哪些?
答案:
- 不在onDraw()中创建对象
- 复用Paint、Path等对象
- 使用硬件加速
- 使用缓存
- 减少绘制区域
- 避免过度绘制
60. 自定义View的内存泄漏如何避免?
答案:
- 使用Application Context
- 及时回收Bitmap
- 移除Handler的Callback
- 避免持有Activity的引用
- 使用WeakReference
61. 自定义View的触摸事件如何处理?
答案: 重写onTouchEvent()方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
return true;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
return true;
case MotionEvent.ACTION_UP:
// 处理抬起事件
return true;
}
return super.onTouchEvent(event);
}
62. 自定义View的动画如何实现?
答案:
- 使用ValueAnimator或ObjectAnimator
- 在动画回调中更新View的状态
- 调用invalidate()重绘
- 在onDraw()中根据状态绘制
63. 自定义View的最佳实践有哪些?
答案:
- 正确处理三种测量模式
- 支持wrap_content
- 考虑padding
- 不在onDraw()中创建对象
- 使用硬件加速
- 避免过度绘制
- 处理内存泄漏
64. 自定义View的常见问题有哪些?
答案:
- View不显示
- wrap_content不生效
- padding不生效
- 内存泄漏
- 性能问题
- 触摸事件不响应
65. 自定义View的测试如何进行?
答案:
- 单元测试:测试View的逻辑
- UI测试:测试View的显示
- 性能测试:测试View的性能
- 兼容性测试:测试不同版本的兼容性
66. 自定义ViewGroup的onMeasure()如何实现?
答案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 测量所有子View
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childWidthSpec = getChildMeasureSpec(...);
int childHeightSpec = getChildMeasureSpec(...);
child.measure(childWidthSpec, childHeightSpec);
}
}
// 2. 根据子View确定自己的尺寸
int width = calculateWidthFromChildren();
int height = calculateHeightFromChildren();
// 3. 设置测量结果
setMeasuredDimension(width, height);
}