📌 目录

自定义 View 为什么存在?

自定义控件的 4 大分类

方式一:继承 View(完全自绘)

方式二:继承 ViewGroup(自定义布局)

方式三:组合控件(XML + inflate)

方式四:继承已有控件扩展功能

View 绘制三大流程(Measure/Layout/Draw)

资源文件(attrs.xml,自定义属性)

四种方式对比总结(表格)

最佳实践与性能优化

⭐ 1. 自定义 View 为什么存在?

Android 原生控件有限,自定义 View 可以让我们做:

仪表盘

雷达图

折线图

特效控件

自定义动画

高级布局(FlowLayout、TagLayout)

底层都离不开:测量 + 绘制 + 布局。

⭐ 2. 自定义控件的 4 大分类

Android 自定义控件主要分为 4 类:

| 类型                     | 继承            | 是否自己画 | 是否自布局 | 是否包含 XML 子控件 | 使用场景                     |
|--------------------------|-----------------|------------|------------|-----------------------|------------------------------|
| ① 自定义绘制 View        | View            | ✔          | ❌          | ❌                     | 图形、动画、仪表盘等        |
| ② 自定义布局 ViewGroup   | ViewGroup       | ❌          | ✔          | ✔                     | 自定义复杂布局、流式布局    |
| ③ 组合控件(复合控件)   | FrameLayout 等   | 部分        | 部分        | ✔(inflate)           | 自定义输入框、搜索框、Card  |
| ④ 扩展已有控件           | TextView 等      | 可选        | ❌          | ❌                     | 扩展行为,如跑马灯、折叠文字 |

🟥 3. 方式一:继承 View(完全自绘控件)

适用场景:

绘制图形(仪表盘、波形图)

自定义动画

数据可视化控件

核心点:

重写 onMeasure() 手动测量尺寸

重写 onDraw() 绘制图像

使用 Canvas/Path/Paint

🔧 示例代码
class CircleView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.RED
    }

    override fun onMeasure(widthSpec: Int, heightSpec: Int) {
        val defaultSize = 200
        val w = resolveSize(defaultSize, widthSpec)
        val h = resolveSize(defaultSize, heightSpec)
        setMeasuredDimension(w, h)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint)
    }
}

🟦 4. 方式二:继承 ViewGroup(自定义布局控件)

适用场景:

流式布局 FlowLayout

九宫格

复杂排序布局

自定义 Banner、卡片堆叠布局

关键点:

onMeasure():测量每个子 View

onLayout():摆放子 View 位置

不负责绘制(不重写 onDraw)

🔧 示例代码示例(FlowLayout)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    var lineWidth = 0
    var totalHeight = paddingTop + paddingBottom
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)

    for (i in 0 until childCount) {
        val child = getChildAt(i)
        measureChild(child, widthMeasureSpec, heightMeasureSpec)

        if (lineWidth + child.measuredWidth > widthSize) {
            totalHeight += child.measuredHeight
            lineWidth = 0
        }
        lineWidth += child.measuredWidth
    }

    setMeasuredDimension(widthSize, totalHeight)
}

override fun onLayout(p0: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var x = paddingLeft
    var y = paddingTop
    val width = r - l

    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (x + child.measuredWidth > width) {
            x = paddingLeft
            y += child.measuredHeight
        }
        child.layout(x, y, x + child.measuredWidth, y + child.measuredHeight)
        x += child.measuredWidth
    }
}

🟩 5. 方式三:组合控件(XML + inflate)

最常见的自定义控件方式。

适用场景:

自定义 TitleBar

自定义输入框(带图标、清理按钮)

SearchView、自定义卡片控件

核心点:使用 LayoutInflater

🔧 示例代码
class SearchBar @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    init {
        LayoutInflater.from(context)
            .inflate(R.layout.view_search_bar, this)
    }
}


view_search_bar.xml:

<LinearLayout ... >
    <ImageView android:src="@drawable/ic_search"/>
    <EditText android:hint="搜索"/>
</LinearLayout>


优点:

复用已有控件,效率高

易扩展、易维护

高度可定制化

🟨 6. 方式四:继承已有控件(行为扩展型)

适用场景:

扩展 EditText(限制输入)

扩展 TextView(自定义跑马灯)

扩展 ImageView(实现圆角图片)

示例:

class RoundImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {

    private val path = Path()

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        path.reset()
        path.addRoundRect(
            0f, 0f, w.toFloat(), h.toFloat(),
            20f, 20f,
            Path.Direction.CW
        )
    }

    override fun onDraw(canvas: Canvas) {
        canvas.save()
        canvas.clipPath(path)
        super.onDraw(canvas)
        canvas.restore()
    }
}

🎨 7. View 绘制三大流程(适用于所有方式)

核心流程图:

Measure → Layout → Draw

① onMeasure:测量大小

决定 View 的宽高(wrap_content 逻辑写这里)

② onLayout:对子 View 摆放位置

只存在于 ViewGroup

③ onDraw:绘制内容

纯 View 重绘的核心

🧩 8. 自定义属性(attrs.xml)

几乎所有自定义控件都需要支持 XML 属性。

attrs.xml:

<declare-styleable name="CircleView">
    <attr name="circleColor" format="color"/>
</declare-styleable>


使用:

<com.xxx.CircleView
    app:circleColor="@color/red"/>


读取:

val typed = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
paint.color = typed.getColor(R.styleable.CircleView_circleColor, Color.BLACK)
typed.recycle()

📊 9. 四种方式对比总结
类型	                自绘	子 View	自布局	难度	性能
纯 View(绘制型)	✔	❌	❌	⭐⭐⭐⭐	⭐⭐⭐⭐⭐
ViewGroup(布局型)	❌	✔	✔	⭐⭐⭐⭐⭐	⭐⭐⭐
组合控件(XML inflate)	部分	✔	部分	⭐⭐	⭐⭐⭐⭐
扩展已有控件	部分	❌	❌	⭐	⭐⭐⭐⭐⭐
🧠 10. 性能优化建议

不要在 onDraw() 里创建对象

使用 postInvalidate() 更新 UI(子线程)

使用硬件加速(默认开启)

谨慎使用 saveLayer()(会创建离屏缓冲)

减少过深的 View 层级(组合控件时注意)

📌 最终总结

Android 自定义控件的核心是:

绘制(View) + 布局(ViewGroup) + 组合复用(inflate) + 控件行为扩展

四种方式对应不同场景,理解:

onMeasure

onLayout

onDraw

attrs.xml

即可轻松实现任意定制 UI。



📌 目录

View 绘制体系概述

自定义 View 的本质是什么?

Measure 测量流程解析

Layout 布局流程

Draw 绘制流程

Canvas & Paint 底层原理

自定义 View 的完整模板

常见问题与性能优化

总结

🔥 1. View 绘制体系概述(整体流程图)

Android UI 渲染是 从 ViewRootImpl → DecorView → 各层级 View 一层一层递归传递的。

下面是完整流程图(你可以作为博客插图):

                ┌───────────────────┐
                │ ViewRootImpl       │
                └─────────┬─────────┘
                          │
            ┌─────────────▼──────────────┐
            │ performTraversals()         │
            └─────────────┬──────────────┘
         Measure → Layout → Draw(核心三步骤)

          ┌────────────┐   ┌────────────┐   ┌────────────┐
          │ measure()   │   │ layout()   │   │ draw()     │
          └──────┬─────┘   └──────┬─────┘   └──────┬─────┘
                 │                │                │
                 ▼                ▼                ▼
           onMeasure()      onLayout()         onDraw()

⭐ 2. 自定义 View 的本质是什么?

一句话总结:

自定义 View = 手动实现 Measure + 绘制逻辑 + 事件逻辑。

Android 框架提供了一个基础绘制管线,开发者只需要实现以下部分:

测量:决定 View 的大小 (onMeasure)

绘制:决定 View 怎么画 (onDraw)

布局:决定子 View 的位置(自定义 ViewGroup 才需要)

🎯 3. Measure 测量流程
✔ 测量的目标:

计算 View 的:

measuredWidth

measuredHeight

这两个值由 测量模式 (MeasureSpec) 决定:

Mode	含义
EXACTLY	精确大小(match_parent 或固定值)
AT_MOST	最大不能超过父容器(wrap_content)
UNSPECIFIED	不限制(滚动容器会用)
✔ 必须重写 onMeasure(wrap_content 的关键)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)

    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    val desiredWidth = 200
    val desiredHeight = 200

    val width = when(widthMode) {
        MeasureSpec.EXACTLY -> widthSize
        MeasureSpec.AT_MOST -> desiredWidth.coerceAtMost(widthSize)
        else -> desiredWidth
    }

    val height = when(heightMode) {
        MeasureSpec.EXACTLY -> heightSize
        MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(heightSize)
        else -> desiredHeight
    }

    setMeasuredDimension(width, height)
}


⚠ 如果你不重写 onMeasure,wrap_content 会失效!

🎯 4. Layout 布局流程(仅 ViewGroup 需要)

作用:

确定子 View 的位置(left、top、right、bottom)

流程:

layout()
  └── onLayout()


例子(自定义简单线性布局):

override fun onLayout(p0: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var childTop = paddingTop

    for (i in 0 until childCount) {
        val child = getChildAt(i)
        val childHeight = child.measuredHeight
        child.layout(paddingLeft, childTop, r - paddingRight, childTop + childHeight)
        childTop += childHeight
    }
}

🎯 5. Draw 绘制流程(核心)

draw() 的内部流程如下:

draw()
 ├── drawBackground()
 ├── onDraw()           ← 开发者核心绘制逻辑
 ├── dispatchDraw()     ← 绘制子 View(ViewGroup)
 └── onDrawForeground()


你的自定义内容都写在 onDraw():

override fun onDraw(canvas: Canvas) {
    paint.color = Color.RED
    canvas.drawCircle(width / 2f, height / 2f, 100f, paint)
}

🎨 6. Canvas & Paint 底层绘制原理
✔ Canvas 是 绘图指令的集合

本质是向 Surface 发送 GPU 绘制指令,例如:

drawLine

drawCircle

drawPath

clipRect

rotate

Canvas 内部使用 GPU(Skia 图形库)。

✔ Paint 是画笔

Paint 决定线条风格:

color

strokeWidth

style (FILL / STROKE)

shader(渐变、BitmapShader)

antiAlias

例如:

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.BLUE
    strokeWidth = 4f
    style = Paint.Style.STROKE
}

🧩 7. 自定义 View 完整模板(可直接复制)
class CircleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.RED
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val defaultSize = 300
        val width = resolveSize(defaultSize, widthMeasureSpec)
        val height = resolveSize(defaultSize, heightMeasureSpec)
        setMeasuredDimension(width, height)
    }

    override fun onDraw(canvas: Canvas) {
        val radius = min(width, height) / 2f
        canvas.drawCircle(width / 2f, height / 2f, radius, paint)
    }
}


这是一份标准自定义 View 模板。

⚙️ 8. 性能优化 & 常见问题
1. 避免在 onDraw 创建对象

❌ 不要 new Paint / Path
✔ 在构造函数创建

2. 使用 invalidate() vs postInvalidate()

invalidate():UI 线程

postInvalidate():非 UI 线程

3. 避免使用过多的 saveLayer()

它会创建离屏缓冲,非常耗性能。

4. onMeasure 尽量使用 resolveSize()
5. 大量动画建议用:ValueAnimator + invalidate()

不要直接在 onDraw 做运算。

📌 9. 总结

自定义 View 是 Android UI 开发的核心能力,理解其底层流程是高级开发者必备技能。

View 绘制三大流程:Measure、Layout、Draw

测量模式 MeasureSpec

Canvas / Paint 底层原理

自定义 View 模板

性能优化策略

掌握这些,就可以绘制任何 UI:

仪表盘

雷达图

动态波形

自定义图标

特效控件

富交互图形
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐