以下是 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. 性能优化技巧
- 避免过度绘制:
- 使用
canvas.clipRect()
限制绘制区域。 - 在
onDraw
中避免创建新对象(如Paint
)。
- 使用
- 启用硬件加速:xml
<application android:hardwareAccelerated="true">
- 使用
View.isInEditMode
:kotlinif (!isInEditMode) { // 仅运行时执行的代码(避免预览崩溃) }
- 合理使用缓存:
- 对静态内容使用
Bitmap
缓存。 - 对频繁变化的内容使用
SurfaceView
。
- 对静态内容使用
总结
关键点 | 说明 |
---|---|
自定义属性 | 通过 attrs.xml 定义,在构造函数中解析 |
测量与布局 | 重写 onMeasure 和 onLayout (ViewGroup) |
绘制流程 | 在 onDraw 中使用 Canvas 和 Paint 进行绘制 |
性能优化 | 避免过度绘制、启用硬件加速、合理使用缓存 |
触摸事件处理 | 重写 onTouchEvent 或使用 GestureDetector |
通过自定义控件,可以灵活实现复杂的 UI 需求,但需注意代码的可维护性和性能表现。