自定义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:

  1. 自定义View的分类和绘制流程
  2. 测量过程和MeasureSpec
  3. 绘制过程和Paint、Canvas使用
  4. 自定义属性的定义和使用
  5. 触摸事件处理
  6. 自定义ViewGroup

自定义View是Android高级开发的核心技能,掌握它可以实现各种复杂的UI效果。

最后更新:2026-03-26