自定义View #
一、自定义View概述 #
当系统提供的控件无法满足需求时,可以通过自定义View来实现特定的UI效果。自定义View是Android高级开发必备技能。
1.1 自定义View的分类 #
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 继承View | 完全自定义 | 特殊绘制效果 |
| 继承ViewGroup | 自定义布局 | 特殊布局排列 |
| 继承现有控件 | 扩展功能 | 增强现有控件 |
1.2 View的绘制流程 #
text
measure() -> layout() -> draw()
│ │ │
▼ ▼ ▼
onMeasure() onLayout() onDraw()
二、测量(Measure) #
2.1 MeasureSpec #
MeasureSpec是一个32位整数,包含测量模式和测量大小:
text
| SpecMode (高2位) | SpecSize (低30位) |
| 模式 | 说明 | 对应布局参数 |
|---|---|---|
| EXACTLY | 精确大小 | match_parent、具体数值 |
| AT_MOST | 最大限制 | wrap_content |
| UNSPECIFIED | 无限制 | 父容器不限制 |
2.2 onMeasure #
kotlin
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 100f
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)
var width = 0
var height = 0
when (widthMode) {
MeasureSpec.EXACTLY -> {
width = widthSize
}
MeasureSpec.AT_MOST -> {
width = (radius * 2 + paddingLeft + paddingRight).toInt()
width = min(width, widthSize)
}
MeasureSpec.UNSPECIFIED -> {
width = (radius * 2 + paddingLeft + paddingRight).toInt()
}
}
when (heightMode) {
MeasureSpec.EXACTLY -> {
height = heightSize
}
MeasureSpec.AT_MOST -> {
height = (radius * 2 + paddingTop + paddingBottom).toInt()
height = min(height, heightSize)
}
MeasureSpec.UNSPECIFIED -> {
height = (radius * 2 + paddingTop + paddingBottom).toInt()
}
}
setMeasuredDimension(width, height)
}
}
2.3 工具方法 #
kotlin
fun measureWidth(measureSpec: Int, desiredSize: Int): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
MeasureSpec.EXACTLY -> size
MeasureSpec.AT_MOST -> min(desiredSize, size)
else -> desiredSize
}
}
三、绘制(Draw) #
3.1 onDraw #
kotlin
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var radius = 100f
private var color = Color.RED
init {
paint.color = color
paint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
canvas.drawCircle(centerX, centerY, radius, paint)
}
}
3.2 Paint常用属性 #
kotlin
val paint = Paint().apply {
// 抗锯齿
isAntiAlias = true
// 颜色
color = Color.RED
// 样式
style = Paint.Style.FILL // FILL, STROKE, FILL_AND_STROKE
// 线宽
strokeWidth = 5f
// 文字大小
textSize = 40f
// 文字对齐
textAlign = Paint.Align.CENTER
// 阴影
setShadowLayer(10f, 5f, 5f, Color.GRAY)
// 渐变
shader = LinearGradient(0f, 0f, 100f, 100f, Color.RED, Color.BLUE, Shader.TileMode.CLAMP)
// 路径效果
pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
}
3.3 Canvas常用方法 #
kotlin
// 绘制颜色
canvas.drawColor(Color.WHITE)
// 绘制点
canvas.drawPoint(x, y, paint)
// 绘制线
canvas.drawLine(startX, startY, stopX, stopY, paint)
// 绘制矩形
canvas.drawRect(left, top, right, bottom, paint)
canvas.drawRect(rect, paint)
// 绘制圆
canvas.drawCircle(cx, cy, radius, paint)
// 绘制椭圆
canvas.drawOval(left, top, right, bottom, paint)
// 绘制圆角矩形
canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint)
// 绘制弧
canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint)
// 绘制路径
canvas.drawPath(path, paint)
// 绘制文字
canvas.drawText(text, x, y, paint)
// 绘制Bitmap
canvas.drawBitmap(bitmap, left, top, paint)
// 保存和恢复
canvas.save()
canvas.rotate(45f)
// 绘制操作
canvas.restore()
// 平移
canvas.translate(dx, dy)
// 缩放
canvas.scale(sx, sy)
// 旋转
canvas.rotate(degrees)
3.4 Path的使用 #
kotlin
val path = Path()
// 移动到起点
path.moveTo(x, y)
// 直线
path.lineTo(x, y)
// 二次贝塞尔曲线
path.quadTo(x1, y1, x2, y2)
// 三次贝塞尔曲线
path.cubicTo(x1, y1, x2, y2, x3, y3)
// 弧
path.arcTo(oval, startAngle, sweepAngle)
// 圆
path.addCircle(cx, cy, radius, Path.Direction.CW)
// 矩形
path.addRect(left, top, right, bottom, Path.Direction.CW)
// 闭合路径
path.close()
canvas.drawPath(path, paint)
四、自定义属性 #
4.1 定义属性 #
xml
<!-- res/values/attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="radius" format="dimension" />
<attr name="circleColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="strokeColor" format="color" />
</declare-styleable>
</resources>
4.2 获取属性 #
kotlin
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 100f
private var circleColor = Color.RED
private var strokeWidth = 0f
private var strokeColor = Color.TRANSPARENT
init {
attrs?.let {
val typedArray = context.obtainStyledAttributes(
it,
R.styleable.CircleView,
defStyleAttr,
0
)
radius = typedArray.getDimension(
R.styleable.CircleView_radius,
100f
)
circleColor = typedArray.getColor(
R.styleable.CircleView_circleColor,
Color.RED
)
strokeWidth = typedArray.getDimension(
R.styleable.CircleView_strokeWidth,
0f
)
strokeColor = typedArray.getColor(
R.styleable.CircleView_strokeColor,
Color.TRANSPARENT
)
typedArray.recycle()
}
}
}
4.3 使用自定义属性 #
xml
<com.example.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:radius="50dp"
app:circleColor="#FF5722"
app:strokeWidth="3dp"
app:strokeColor="#000000" />
五、处理触摸事件 #
5.1 onTouchEvent #
kotlin
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 手指按下
return true
}
MotionEvent.ACTION_MOVE -> {
// 手指移动
}
MotionEvent.ACTION_UP -> {
// 手指抬起
}
MotionEvent.ACTION_CANCEL -> {
// 取消
}
}
return super.onTouchEvent(event)
}
}
5.2 点击检测 #
kotlin
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var touchX = 0f
private var touchY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
touchX = event.x
touchY = event.y
return true
}
MotionEvent.ACTION_UP -> {
if (isClick(event.x, event.y)) {
performClick()
}
}
}
return super.onTouchEvent(event)
}
private fun isClick(x: Float, y: Float): Boolean {
val dx = x - touchX
val dy = y - touchY
return dx * dx + dy * dy < 100 // 10像素内视为点击
}
override fun performClick(): Boolean {
super.performClick()
// 处理点击
return true
}
}
六、自定义ViewGroup #
6.1 onMeasure #
kotlin
class FlowLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
var width = 0
var height = 0
var lineWidth = 0
var lineHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
if (lineWidth + childWidth > widthSize) {
width = max(width, lineWidth)
height += lineHeight
lineWidth = childWidth
lineHeight = childHeight
} else {
lineWidth += childWidth
lineHeight = max(lineHeight, childHeight)
}
}
width = max(width, lineWidth)
height += lineHeight
setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) widthSize else width,
if (heightMode == MeasureSpec.EXACTLY) MeasureSpec.getSize(heightMeasureSpec) else height
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var left = 0
var top = 0
var lineHeight = 0
val width = r - l
for (i in 0 until childCount) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
if (left + childWidth > width) {
left = 0
top += lineHeight
lineHeight = 0
}
child.layout(left, top, left + childWidth, top + childHeight)
left += childWidth
lineHeight = max(lineHeight, childHeight)
}
}
}
七、总结 #
本章详细介绍了自定义View:
- 自定义View的分类和绘制流程
- 测量过程和MeasureSpec
- 绘制过程和Paint、Canvas使用
- 自定义属性的定义和使用
- 触摸事件处理
- 自定义ViewGroup
自定义View是Android高级开发的核心技能,掌握它可以实现各种复杂的UI效果。
最后更新:2026-03-26