自定义View设计
# 10.自定义View设计
# 目录介绍
- 一、概述
- 二、自定义View
- 2.1 自定义View步骤
- 2.2 创建View
- 2.3 测量View(Measure)
- 2.4 绘制View(Draw)
- 2.5 Canvas的底层原理
- 2.6 Canvas变换操作
- 2.7 Paint高级特性
- 2.8 用户交互
- 2.9 优化建议
- 三、自定义ViewGroup
- 3.1 自定义ViewGroup步骤
- 3.2 测量子控件
- 3.3 LayoutParams
- 3.4 onLayout实现
- 四、FlowLayout实践案例
- 4.1 完整onMeasure实现
- 4.2 完整onLayout实现
- 五、关键设计思想
- 5.1 测量的递归思想
- 5.2 布局的坐标系统
- 5.3 getMeasuredWidth与getWidth的区别
- 六、事件分发处理
- 6.1 外部拦截法
- 6.2 内部拦截法
- 七、硬件加速与自定义View
- 7.1 硬件加速的影响
- 7.2 View Layer类型
- 7.3 invalidate与postInvalidate
- 八、自定义View的性能优化
- 8.1 绘制性能优化
- 8.2 测量与布局优化
- 8.3 内存优化
- 九、面试高频问题
- 十、总结
# 一、概述
自定义控件是Android开发中的重要技能,掌握自定义View和ViewGroup的设计方法,能够实现标准控件无法满足的复杂UI需求。本文系统介绍自定义View和ViewGroup的完整步骤、核心原理以及实践案例。
# 二、自定义View
# 2.1 自定义View步骤
根据Android官方指引,自定义View的步骤如下:
- 创建View:继承View或已有控件,重写构造方法
- 处理测量:重写onMeasure,确定控件宽高
- 绘制内容:重写onDraw,使用Canvas和Paint绑定内容
- 用户交互:处理触摸事件和手势
- 性能优化:确保流畅运行
# 2.2 创建View
继承与构造方法:
public class CustomView extends View {
// 代码中new对象时调用
public CustomView(Context context) {
this(context, null);
}
// 在XML布局中使用时调用
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// 使用样式时调用
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(attrs);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
自定义属性:
在res/values/attrs.xml中声明:
<declare-styleable name="CustomView">
<attr name="custom_color" format="color"/>
<attr name="custom_size" format="dimension"/>
<attr name="custom_text" format="string"/>
</declare-styleable>
2
3
4
5
在构造方法中获取:
private void initView(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);
int color = ta.getColor(R.styleable.CustomView_custom_color, Color.BLACK);
float size = ta.getDimension(R.styleable.CustomView_custom_size, 16f);
String text = ta.getString(R.styleable.CustomView_custom_text);
ta.recycle(); // 必须回收!TypedArray是共享资源
}
2
3
4
5
6
7
# 2.3 测量View(Measure)
onMeasure方法确定控件的宽高,关键是处理wrap_content情况:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = measureDimension(widthMeasureSpec, getDesiredWidth());
int measureHeight = measureDimension(heightMeasureSpec, getDesiredHeight());
setMeasuredDimension(measureWidth, measureHeight);
}
private int measureDimension(int measureSpec, int desiredSize) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY: // match_parent或指定dp
return size;
case MeasureSpec.AT_MOST: // wrap_content
return Math.min(desiredSize, size);
default: // UNSPECIFIED
return desiredSize;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:计算完宽高后必须调用setMeasuredDimension(),否则会抛出运行时异常。
onSizeChanged()方法在View第一次被指定大小或大小发生变化时调用,适合计算与size相关的值。
# 2.4 绘制View(Draw)
onDraw方法中使用Canvas和Paint进行绘制:
- Canvas:决定画什么(矩形、圆、文本、图片等)
- Paint:决定怎么画(颜色、样式、粗细等)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Canvas常用绑制操作
canvas.drawRect(rect, paint); // 矩形
canvas.drawRoundRect(rectF, rx, ry, paint); // 圆角矩形
canvas.drawCircle(cx, cy, radius, paint); // 圆
canvas.drawOval(rectF, paint); // 椭圆
canvas.drawArc(rectF, startAngle, sweepAngle, useCenter, paint); // 圆弧
canvas.drawText(text, x, y, paint); // 文本
canvas.drawBitmap(bitmap, x, y, paint); // 图片
}
2
3
4
5
6
7
8
9
10
11
12
重要:Paint对象的初始化应在构造方法或init方法中完成,不要在onDraw中创建对象,因为onDraw会被频繁调用。
# 2.5 Canvas的底层原理
Canvas的两种实现模式:
1. 软件渲染Canvas(Software Canvas)
Canvas → Skia库 → Bitmap(CPU内存)
流程:
View.draw(canvas)
→ Canvas.drawXxx()
→ Skia引擎执行光栅化
→ 写入底层Bitmap的像素缓冲区
→ 将Bitmap提交到Surface的BufferQueue
特点:
- 所有绘制操作在CPU上执行
- 绘制结果直接写入像素缓冲区
- 每次invalidate重绘整个View
- Android 3.0之前的默认模式
2. 硬件加速Canvas(DisplayListCanvas / RecordingCanvas)
Canvas → DisplayList(记录绘制命令)→ RenderThread → OpenGL/Vulkan → GPU
流程:
View.draw(canvas)
→ RecordingCanvas.drawXxx()
→ 记录绘制操作到RenderNode的DisplayList中(不执行实际绘制)
→ 主线程工作完成,sync DisplayList到RenderThread
→ RenderThread遍历DisplayList
→ 转换为OpenGL/Vulkan命令
→ GPU执行实际渲染
特点:
- 主线程只记录命令,不执行渲染
- GPU并行处理绘制命令
- 可以只重绘变化的RenderNode(局部更新)
- Android 3.0+默认启用
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
RenderNode与DisplayList的关系:
每个View对应一个RenderNode:
View
└── mRenderNode
└── DisplayList(绘制命令列表)
├── drawRect(0, 0, 100, 50, paint1)
├── drawText("Hello", 10, 30, paint2)
├── drawBitmap(bmp, 0, 0, paint3)
└── drawRenderNode(childRenderNode) ← 子View的RenderNode
当invalidate()被调用时:
├── 软件渲染:重绘该View及其所有子View
└── 硬件加速:只重建该View的DisplayList
→ 子View的DisplayList可以复用(如果没有变化)
→ 大幅减少重绘工作量
RenderNode还支持属性动画的优化:
View.setAlpha() / setTranslationX() / setRotation()
→ 不需要重建DisplayList
→ 只修改RenderNode的变换矩阵
→ RenderThread直接应用新的变换
→ 整个过程不需要主线程参与!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.6 Canvas变换操作
// Canvas的变换操作在自定义View中非常常用
@Override
protected void onDraw(Canvas canvas) {
// save/restore用于保存和恢复Canvas状态
canvas.save();
// 平移:将坐标原点移动到(100, 100)
canvas.translate(100, 100);
// 旋转:以当前原点为中心旋转45度
canvas.rotate(45);
// 缩放:x和y方向各缩放0.5倍
canvas.scale(0.5f, 0.5f);
// 在变换后的坐标系中绘制
canvas.drawRect(0, 0, 200, 100, paint);
canvas.restore(); // 恢复到save前的状态
// Canvas变换的底层原理:
// 实际是修改了Canvas内部的变换矩阵(Matrix)
// 所有后续的绘制坐标都会经过矩阵变换
// save/restore是将矩阵入栈/出栈
}
// 裁剪操作
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
// 裁剪画布:之后的绘制只在裁剪区域内可见
canvas.clipRect(50, 50, 250, 250);
// 圆形裁剪
Path clipPath = new Path();
clipPath.addCircle(150, 150, 100, Path.Direction.CW);
canvas.clipPath(clipPath);
// 绘制操作(只有裁剪区域内的部分可见)
canvas.drawBitmap(bitmap, 0, 0, null);
canvas.restore();
}
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
# 2.7 Paint高级特性
// Paint不仅是颜色和粗细,还有许多高级特性
// 1. Shader(着色器)
// 线性渐变
LinearGradient gradient = new LinearGradient(
0, 0, width, 0,
Color.RED, Color.BLUE,
Shader.TileMode.CLAMP);
paint.setShader(gradient);
canvas.drawRect(rect, paint);
// 2. Xfermode(混合模式)
// 实现圆形头像等效果
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// DST_IN: 保留目标与源重叠的部分
// SRC_IN: 保留源与目标重叠的部分
// 3. PathEffect(路径效果)
paint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
// 虚线效果
// 4. MaskFilter(遮罩滤镜)
paint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
// 模糊效果
// 5. ColorFilter(颜色滤镜)
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
// 颜色矩阵变换(如灰度化、亮度调整)
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
# 2.8 用户交互
处理触摸事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
return true;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
break;
case MotionEvent.ACTION_UP:
// 处理抬起事件
break;
}
return super.onTouchEvent(event);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复杂手势可使用GestureDetector处理轻点、快速滑动等。动画效果使用属性动画框架(Property Animation)实现平滑的开始和结束效果。
# 2.9 优化建议
- 避免在onDraw()中执行不必要的代码
- 不要在onDraw()中创建对象(避免GC导致掉帧)
- 减少不必要的invalidate()调用
- 确保动画始终保持60fps
- 使用硬件加速友好的API(避免不支持硬件加速的操作)
# 三、自定义ViewGroup
# 3.1 自定义ViewGroup步骤
自定义ViewGroup一般是利用现有组件根据特定布局方式组成新组件,继承自ViewGroup或各种Layout:
- 创建ViewGroup:继承ViewGroup,重写构造方法
- 测量View:重写onMeasure,测量自身和所有子控件
- 布局View:重写onLayout,确定子控件位置
- 绘制View:按需重写onDraw
- 事件分发处理:按需重写事件分发方法
# 3.2 测量子控件
ViewGroup提供了三个测量子控件的方法:
| 方法 | 说明 | 是否考虑margin |
|---|---|---|
| measureChildren | 遍历测量所有子控件 | 否 |
| measureChild | 测量单个子控件 | 否 |
| measureChildWithMargins | 测量单个子控件 | 是 |
measureChild和measureChildWithMargins的区别:
- measureChild计算宽度时只考虑padding
- measureChildWithMargins还考虑了margin值
- 例如屏幕宽1080,子控件match_parent + marginLeft=100:measureChild得到1080,measureChildWithMargins得到980
三个方法都调用getChildMeasureSpec()来生成子控件的MeasureSpec,这个方法根据父控件的MeasureSpec和子控件的LayoutParams综合计算。
# 3.3 LayoutParams
ViewGroup中定义了两个重要的内部类:
ViewGroup.LayoutParams:基础布局参数(width、height)ViewGroup.MarginLayoutParams:继承LayoutParams,增加了margin支持
为什么LayoutParams定义在ViewGroup中?因为ViewGroup是所有容器的基类,有义务提供布局属性类来控制子控件的布局参数。
View中有mLayoutParams变量保存布局属性,这些属性在父控件摆放子控件时使用。
# 3.4 onLayout实现
onLayout是ViewGroup的核心方法,负责确定每个子控件的位置。参数l、t、r、b是以父ViewGroup左上角为原点的坐标值。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 以FlowLayout为例
int childLeft = 0, childTop = 0;
int usedWidth = 0;
int layoutWidth = getMeasuredWidth();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 如果当前行放不下,换行
if (layoutWidth - usedWidth < childWidth) {
childLeft = 0;
usedWidth = 0;
childTop += childHeight;
}
// 布局子控件
child.layout(childLeft, childTop,
childLeft + childWidth, childTop + childHeight);
childLeft += childWidth;
usedWidth += childWidth;
}
}
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
# 四、FlowLayout实践案例
FlowLayout(流式布局)是经典的自定义ViewGroup案例,子控件从左到右排列,一行放不下自动换行。
# 4.1 完整onMeasure实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int usedWidth = 0;
int totalHeight = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
// 先测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
int remaining = widthSize - usedWidth;
// 一行放不下,换行
if (childView.getMeasuredWidth() > remaining) {
usedWidth = 0;
totalHeight += lineHeight;
}
usedWidth += childView.getMeasuredWidth();
lineHeight = childView.getMeasuredHeight();
}
// 加上最后一行的高度
totalHeight += lineHeight;
// wrap_content时使用计算的高度
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = totalHeight;
}
setMeasuredDimension(widthSize, heightSize);
}
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
# 4.2 完整onLayout实现
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = 0, childLeft = 0;
int usedWidth = 0;
int layoutWidth = getMeasuredWidth();
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
// 换行
if (layoutWidth - usedWidth < childWidth) {
childLeft = 0;
usedWidth = 0;
childTop += childHeight;
}
childView.layout(childLeft, childTop,
childLeft + childWidth, childTop + childHeight);
childLeft += childWidth;
usedWidth += childWidth;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 五、关键设计思想
# 5.1 测量的递归思想
View树测量流程(递归):
递流程(自顶向下):
ViewGroup.onMeasure()
→ measureChild()/measureChildWithMargins()
→ getChildMeasureSpec() // 生成子控件MeasureSpec
→ child.measure()
→ child.onMeasure() // 如果child是ViewGroup,继续递
归流程(自底向上):
叶子View.setMeasuredDimension() // 测量完成
→ 父ViewGroup聚合子控件尺寸
→ 父ViewGroup.setMeasuredDimension()
→ 继续向上...
2
3
4
5
6
7
8
9
10
11
12
13
14
核心理解:子控件的测量结果由父控件和其自身共同决定,父控件通过MeasureSpec传递布局约束。
# 5.2 布局的坐标系统
布局坐标系:
以父ViewGroup左上角为原点
→ 向右为x正方向
→ 向下为y正方向
child.layout(left, top, right, bottom)
→ left/top:子控件左上角相对于父控件的坐标
→ right/bottom:子控件右下角相对于父控件的坐标
→ width = right - left
→ height = bottom - top
2
3
4
5
6
7
8
9
10
11
重要:坐标是相对于父控件的相对位置,不是屏幕坐标系的绝对位置,保证了控件树结构的内部自治性。
# 5.3 getMeasuredWidth与getWidth的区别
| 方法 | 含义 | 可用时机 |
|---|---|---|
| getMeasuredWidth() | 测量宽度 | measure之后 |
| getWidth() | 实际宽度(right-left) | layout之后 |
通常两者相等,但在layout()中可以设置不同的值。
详细对比和底层原理:
getMeasuredWidth():
→ 返回mMeasuredWidth & MEASURED_SIZE_MASK
→ 在setMeasuredDimension()中设置
→ 表示View自身测量后"期望"的宽度
→ 在onMeasure()完成后即可获取
→ 可能被多次调用(ViewGroup可能多次测量子View)
getWidth():
→ 返回mRight - mLeft
→ 在layout() → setFrame(l, t, r, b)中设置
→ 表示View在父布局中"实际分配"的宽度
→ 在onLayout()完成后才有效
两者不同的场景:
// 在onLayout中故意设置不同的位置
child.layout(0, 0, child.getMeasuredWidth() + 100, child.getMeasuredHeight());
// 此时 getWidth() = getMeasuredWidth() + 100
// getMeasuredWidth()不变,getWidth()被layout修改
// 实际开发中这种情况很少见
// 99%的情况下两者相等
在自定义View中的最佳实践:
onMeasure()中使用getMeasuredWidth/Height → 因为layout还没执行
onLayout()中使用getMeasuredWidth/Height → 分配子View位置
onDraw()中使用getWidth/Height → 因为layout已完成,更准确
onClick等交互中使用getWidth/Height → 反映实际显示尺寸
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
# 六、事件分发处理
自定义ViewGroup可能需要处理事件分发,特别是存在滑动冲突时:
# 6.1 外部拦截法
在ViewGroup中重写onInterceptTouchEvent:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false; // 不拦截DOWN
case MotionEvent.ACTION_MOVE:
// 根据业务判断是否拦截
return needIntercept(ev);
case MotionEvent.ACTION_UP:
return false; // 不拦截UP
}
return super.onInterceptTouchEvent(ev);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 6.2 内部拦截法
在子View中重写dispatchTouchEvent:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentNeedEvent(event)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(event);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 七、硬件加速与自定义View
# 7.1 硬件加速的影响
硬件加速对自定义View的影响:
Android 3.0+默认启用硬件加速:
Application级别:android:hardwareAccelerated="true"
Activity级别:android:hardwareAccelerated="true/false"
Window级别:getWindow().setFlags(FLAG_HARDWARE_ACCELERATED, FLAG_HARDWARE_ACCELERATED)
View级别:view.setLayerType(LAYER_TYPE_SOFTWARE, null)
硬件加速不支持的Canvas操作(会回退到软件渲染或显示异常):
├── Canvas.drawPicture()
├── Canvas.drawVertices()
├── Paint.setLinearText()
├── Paint.setMaskFilter() (部分)
├── Camera(3D旋转)的一些操作
└── Canvas.clipPath()在某些旧设备上
当使用不支持的API时:
方案1:关闭View级别的硬件加速
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
方案2:使用替代API
如用PorterDuffXfermode代替clipPath实现圆形
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 7.2 View Layer类型
View的三种Layer类型:
1. LAYER_TYPE_NONE(默认)
→ 不使用离屏缓冲
→ 每次invalidate重新执行onDraw
2. LAYER_TYPE_HARDWARE
→ 在GPU上创建一个FBO(Frame Buffer Object)作为离屏纹理
→ 第一次绘制结果缓存到GPU纹理中
→ 后续如果只有属性变化(alpha/translation/rotation/scale)
→ 不重新绘制,只改变纹理的变换矩阵
→ 动画极快(纯GPU操作)
→ invalidate时才重新绘制纹理内容
适用场景:
├── 属性动画期间临时开启(动画开始setLayerType(HARDWARE),结束setLayerType(NONE))
├── 复杂View的alpha动画
└── View.animate().alpha(0.5f).withLayer()会自动管理
3. LAYER_TYPE_SOFTWARE
→ 创建一个Bitmap作为离屏缓冲
→ 所有绘制在CPU上完成到Bitmap中
→ 支持所有Canvas操作(包括硬件加速不支持的)
→ 性能最差
适用场景:
└── 使用硬件加速不支持的Canvas API时
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
# 7.3 invalidate与postInvalidate
重绘机制的底层原理:
invalidate()调用链:
View.invalidate()
→ View.invalidateInternal(l, t, r, b)
→ 设置View的PFLAG_DIRTY标记
→ parent.invalidateChild(this, damage)
→ 逐层向上传递dirty区域
→ 到达ViewRootImpl
→ ViewRootImpl.invalidate()
→ 向主线程Handler发送DO_TRAVERSAL消息
→ Choreographer.postCallback(CALLBACK_TRAVERSAL, mTraversalRunnable, null)
→ 在下一个VSYNC时执行performTraversals()
→ 遍历View树,重绘dirty的View
invalidate vs requestLayout vs postInvalidate:
┌───────────────────┬──────────────┬───────────────────────┐
│ 方法 │ 触发流程 │ 适用场景 │
├───────────────────┼──────────────┼───────────────────────┤
│ invalidate() │ 只重绘(draw) │ 外观变化(颜色/内容) │
│ requestLayout() │ 测量+布局+绘制│ 尺寸/位置变化 │
│ postInvalidate() │ 同invalidate │ 在子线程触发重绘 │
│ forceLayout() │ 标记需要测量 │ 强制下次测量(不立即触发)│
└───────────────────┴──────────────┴───────────────────────┘
性能建议:
能用invalidate就不用requestLayout
requestLayout会导致整个View树重新measure和layout
invalidate只会重绘dirty区域
invalidate(Rect)可以指定dirty区域,进一步减少重绘范围
(但在硬件加速下,这个优化通常被忽略,因为DisplayList级别的缓存更高效)
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
# 八、自定义View的性能优化
# 8.1 绘制性能优化
onDraw性能优化清单:
1. 不在onDraw中创建对象
// 差:每帧创建Paint
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每帧GC压力!
paint.setColor(Color.RED);
canvas.drawCircle(x, y, r, paint);
}
// 好:在init中创建,onDraw复用
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawCircle(x, y, r, mPaint);
}
2. 避免过度绘制
// 使用clipRect限制绘制区域
canvas.clipRect(visibleRect);
// 只绘制可见区域内的元素
for (Item item : items) {
if (item.bounds.intersect(visibleRect)) {
item.draw(canvas);
}
}
3. 使用硬件加速友好的API
// 避免:频繁调用Canvas.saveLayer()(创建离屏FBO,开销大)
// 替代:使用View.setLayerType或RenderNode
4. 减少Path计算
// 复杂Path在onSizeChanged中计算一次
// onDraw只做drawPath
5. 使用drawBitmap代替复杂绘制
// 如果是静态复杂图案,预渲染到Bitmap
// onDraw直接drawBitmap,比每帧重绘快得多
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
# 8.2 测量与布局优化
measure/layout性能优化:
1. 避免多次测量
ViewGroup的onMeasure可能被多次调用
避免在onMeasure中做耗时操作
缓存计算结果
2. 避免requestLayout连锁反应
一个View调用requestLayout
→ 整个View树都会重新measure和layout
→ 如果在onLayout中修改子View属性导致再次requestLayout
→ 会触发无限循环(系统有保护,但性能很差)
3. 正确使用GONE
View.GONE的子控件不参与测量和布局
比INVISIBLE更省性能
4. 避免在动画中requestLayout
属性动画(translationX/Y, alpha, rotation)不触发requestLayout
但如果在动画中改变LayoutParams → 每帧都requestLayout → 严重卡顿
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 8.3 内存优化
自定义View内存优化:
1. Bitmap管理
// 及时回收不再使用的Bitmap
// 使用inBitmap复用内存
// 按View大小加载合适尺寸的Bitmap
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
mBitmap = null;
}
}
2. 动画资源释放
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 停止动画
if (mAnimator != null) {
mAnimator.cancel();
}
// 移除Handler消息
mHandler.removeCallbacksAndMessages(null);
}
3. 避免内存泄漏
// 自定义View不要持有Activity的强引用
// 使用WeakReference包装外部回调
// 在onDetachedFromWindow中清理资源
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
# 九、面试高频问题
问题:MeasureSpec是什么?三种模式分别对应什么?
- MeasureSpec是一个32位int,高2位是模式,低30位是大小
- EXACTLY:父View指定了精确大小(match_parent或具体dp值)
- AT_MOST:父View指定了最大大小(wrap_content)
- UNSPECIFIED:无限制(如ScrollView中的子View)
- 子View的MeasureSpec由父View的MeasureSpec和子View的LayoutParams共同决定
问题:invalidate和requestLayout的区别?
- invalidate只触发draw过程,不会measure和layout
- requestLayout触发measure+layout+draw
- invalidate用于内容/颜色变化,requestLayout用于大小/位置变化
- invalidate可以在子线程通过postInvalidate调用
问题:硬件加速开启后onDraw中的Canvas是什么?
- 是RecordingCanvas(也叫DisplayListCanvas)
- 不会立即执行绘制,而是记录绘制命令到DisplayList
- DisplayList由RenderThread在GPU上执行
- 某些Canvas API在硬件加速下不支持(如drawPicture)
问题:自定义View如何做到60fps?
- onDraw中不创建对象(避免GC)
- 使用clipRect减少绘制范围
- 属性动画期间使用LAYER_TYPE_HARDWARE
- 复杂静态内容预渲染到Bitmap
- 避免在动画中requestLayout
- 使用invalidate(dirty)指定脏区域
问题:View.post(Runnable)为什么能获取到View的宽高?
- View.post将Runnable添加到Handler的消息队列中
- 在View未attach时,存储在RunQueue中
- 当View attach到Window后,将RunQueue中的消息转发到Handler
- 此时measure和layout已经完成,可以获取到正确的宽高
- 本质是利用了消息队列的顺序性:layout消息在前,post的消息在后
# 十、总结
自定义View/ViewGroup知识图谱:
自定义View
├── 创建 → 继承View、自定义属性、TypedArray
├── 测量 → onMeasure、MeasureSpec、setMeasuredDimension
├── 绘制 → onDraw、Canvas、Paint
│ ├── Canvas变换 → translate/rotate/scale/clipRect
│ ├── Paint高级 → Shader/Xfermode/PathEffect
│ └── 底层原理 → Software Canvas vs DisplayList Canvas
├── 交互 → onTouchEvent、GestureDetector
└── 优化 → 避免onDraw创建对象、减少invalidate、硬件加速
自定义ViewGroup
├── 测量 → measureChild/measureChildWithMargins
├── 布局 → onLayout、相对坐标系
├── LayoutParams → 布局参数类
├── 事件分发 → onInterceptTouchEvent
└── 实践 → FlowLayout、瀑布流等
硬件加速
├── DisplayList → 记录绘制命令,RenderThread执行
├── RenderNode → View对应的GPU渲染单元
├── Layer类型 → NONE/HARDWARE/SOFTWARE
└── 动画优化 → 属性动画不需要重建DisplayList
核心设计思想
├── 递归测量 → 自顶向下传递MeasureSpec,自底向上汇报结果
├── 相对坐标 → 保证View树的内部自治性
├── invalidate → 只重绘dirty区域/RenderNode
└── 子控件测量 = 父控件约束 + 自身LayoutParams
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