Skip to content

以下是 Android 中 自定义控件 的详细解析,包含核心概念、实现步骤和最佳实践:


1. 自定义控件的作用

  • 扩展原生控件功能:实现原生控件无法满足的 UI 效果(如圆形进度条)。
  • 代码复用:将复杂 UI 逻辑封装成独立组件,提高开发效率。
  • 性能优化:通过精细控制绘制逻辑,减少不必要的布局嵌套。

2. 自定义控件的类型

类型说明
继承 View完全自定义绘制逻辑(如绘制图表)
继承 ViewGroup自定义布局容器(如流式布局)
组合现有控件通过组合多个控件实现复杂功能(如带清除按钮的输入框)

3. 实现自定义控件(以继承 View 为例)

步骤 1:创建自定义 View 类

kotlin
class CircleProgressView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 初始化代码
    init {
        // 解析自定义属性(见步骤2)
        parseAttributes(context, attrs)
    }

    // 测量、绘制、触摸事件处理(见后续步骤)
}

步骤 2:定义自定义属性

res/values/attrs.xml 中添加:

xml
<resources>
    <declare-styleable name="CircleProgressView">
        <attr name="progressColor" format="color" />
        <attr name="progressWidth" format="dimension" />
        <attr name="maxProgress" format="integer" />
    </declare-styleable>
</resources>

解析属性:

kotlin
private var progressColor = Color.RED
private var progressWidth = 10.dpToPx() // 扩展函数转换 dp 到 px
private var maxProgress = 100

private fun parseAttributes(context: Context, attrs: AttributeSet?) {
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView)
    progressColor = typedArray.getColor(R.styleable.CircleProgressView_progressColor, Color.RED)
    progressWidth = typedArray.getDimension(R.styleable.CircleProgressView_progressWidth, 10.dpToPx())
    maxProgress = typedArray.getInt(R.styleable.CircleProgressView_maxProgress, 100)
    typedArray.recycle()
}

// dp 转 px 扩展函数
fun Int.dpToPx(): Float = TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP, 
    this.toFloat(), 
    resources.displayMetrics
)

步骤 3:重写 onMeasure(测量尺寸)

kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val minWidth = 100.dpToPx().toInt()
    val minHeight = 100.dpToPx().toInt()

    val width = resolveSize(minWidth, widthMeasureSpec)
    val height = resolveSize(minHeight, heightMeasureSpec)
    
    setMeasuredDimension(width, height)
}

步骤 4:重写 onDraw(绘制内容)

kotlin
private var progress = 0f

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // 绘制背景圆
    paint.color = Color.LTGRAY
    paint.strokeWidth = progressWidth
    paint.style = Paint.Style.STROKE
    val radius = (width / 2 - progressWidth).coerceAtLeast(0f)
    canvas.drawCircle(width / 2f, height / 2f, radius, paint)

    // 绘制进度圆弧
    paint.color = progressColor
    val sweepAngle = 360 * (progress / maxProgress)
    canvas.drawArc(
        progressWidth, 
        progressWidth, 
        width - progressWidth, 
        height - progressWidth,
        -90f, 
        sweepAngle, 
        false, 
        paint
    )
}

// 设置进度并刷新
fun setProgress(progress: Float) {
    this.progress = progress.coerceIn(0f, maxProgress.toFloat())
    invalidate() // 触发重绘
}

步骤 5:处理触摸事件(可选)

kotlin
override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            // 处理按下事件
            return true
        }
        MotionEvent.ACTION_MOVE -> {
            // 处理滑动事件
            return true
        }
    }
    return super.onTouchEvent(event)
}

4. 使用自定义控件

在 XML 布局中引用

xml
<com.example.ui.CircleProgressView
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:progressColor="@color/blue"
    app:progressWidth="4dp"
    app:maxProgress="200" />

在代码中控制

kotlin
val progressView = findViewById<CircleProgressView>(R.id.progress_view)
progressView.setProgress(75f)

5. 自定义 ViewGroup 示例(流式布局)

kotlin
class FlowLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    private val horizontalSpacing = 8.dpToPx().toInt()
    private val verticalSpacing = 8.dpToPx().toInt()

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 测量子 View 并计算总高度
        // ...(具体实现略)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // 排列子 View
        var x = paddingLeft
        var y = paddingTop
        var rowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility == GONE) continue

            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight

            if (x + childWidth > width - paddingRight) {
                x = paddingLeft
                y += rowHeight + verticalSpacing
                rowHeight = 0
            }

            child.layout(x, y, x + childWidth, y + childHeight)
            x += childWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, childHeight)
        }
    }
}

6. 性能优化技巧

  1. 避免过度绘制
    • 使用 canvas.clipRect() 限制绘制区域。
    • onDraw 中避免创建新对象(如 Paint)。
  2. 启用硬件加速
    xml
    <application android:hardwareAccelerated="true">
  3. 使用 View.isInEditMode
    kotlin
    if (!isInEditMode) {
        // 仅运行时执行的代码(避免预览崩溃)
    }
  4. 合理使用缓存
    • 对静态内容使用 Bitmap 缓存。
    • 对频繁变化的内容使用 SurfaceView

总结

关键点说明
自定义属性通过 attrs.xml 定义,在构造函数中解析
测量与布局重写 onMeasureonLayout(ViewGroup)
绘制流程onDraw 中使用 CanvasPaint 进行绘制
性能优化避免过度绘制、启用硬件加速、合理使用缓存
触摸事件处理重写 onTouchEvent 或使用 GestureDetector

通过自定义控件,可以灵活实现复杂的 UI 需求,但需注意代码的可维护性和性能表现。