安卓自定义View的4种方式
·
📌 目录
自定义 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:
仪表盘
雷达图
动态波形
自定义图标
特效控件
富交互图形更多推荐


所有评论(0)