编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义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 内存优化
        • 九、面试高频问题
        • 十、总结
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
      • 组件化与路由设计
      • 插件化与热修复
      • NDK开发实践
      • WebView核心设计
      • ADB常见使用操作
    • 智能硬件

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • Apps
  • Android提升进阶
  • 专栏博客
杨充
2026-04-14
目录

自定义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的步骤如下:

  1. 创建View:继承View或已有控件,重写构造方法
  2. 处理测量:重写onMeasure,确定控件宽高
  3. 绘制内容:重写onDraw,使用Canvas和Paint绑定内容
  4. 用户交互:处理触摸事件和手势
  5. 性能优化:确保流畅运行

# 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);
    }
}
1
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>
1
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是共享资源
}
1
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;
    }
}
1
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); // 图片
}
1
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+默认启用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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直接应用新的变换
  → 整个过程不需要主线程参与!
1
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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 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));
// 颜色矩阵变换(如灰度化、亮度调整)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

复杂手势可使用GestureDetector处理轻点、快速滑动等。动画效果使用属性动画框架(Property Animation)实现平滑的开始和结束效果。

# 2.9 优化建议

  1. 避免在onDraw()中执行不必要的代码
  2. 不要在onDraw()中创建对象(避免GC导致掉帧)
  3. 减少不必要的invalidate()调用
  4. 确保动画始终保持60fps
  5. 使用硬件加速友好的API(避免不支持硬件加速的操作)

# 三、自定义ViewGroup

# 3.1 自定义ViewGroup步骤

自定义ViewGroup一般是利用现有组件根据特定布局方式组成新组件,继承自ViewGroup或各种Layout:

  1. 创建ViewGroup:继承ViewGroup,重写构造方法
  2. 测量View:重写onMeasure,测量自身和所有子控件
  3. 布局View:重写onLayout,确定子控件位置
  4. 绘制View:按需重写onDraw
  5. 事件分发处理:按需重写事件分发方法

# 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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 四、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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 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;
    }
}
1
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()
→ 继续向上...
1
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
1
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 → 反映实际显示尺寸
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 六、事件分发处理

自定义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);
}
1
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);
}
1
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实现圆形
1
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时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 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级别的缓存更高效)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 八、自定义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,比每帧重绘快得多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 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 → 严重卡顿
1
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中清理资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 九、面试高频问题

问题: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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
上次更新: 2026/06/10, 11:13:41
Surface渲染原理
WMS窗口管理

← Surface渲染原理 WMS窗口管理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式