创建一个自定义 View,本质上是实现一个“测量 → 布局 → 绘制”的完整生命周期。理解并掌握这三个核心回调方法是定制任何复杂视图的基石。
核心思想:父 View 向子 View 传递约束,子 View 根据约束计算自己的尺寸,最终由父 View 决定其在屏幕上的具体位置。
onMeasure()
- 测量尺寸onMeasure()
决定了 View “有多大”。它的核心职责是根据父容器传递的测量规格(MeasureSpec
),计算并设定 View 自身的宽度和高度。
核心任务:必须在方法结束前调用
setMeasuredDimension(width, height)
来保存测量结果,否则会触发IllegalStateException
。
MeasureSpec
是一个 32 位的 int
值,它通过位运算将两种完全不同的信息——模式 (Mode) 和 尺寸 (Size)——打包在一起,以提升效率。
高 2 位:测量模式 (Mode)
MeasureSpec.getMode(spec)
获取。低 30 位:测量尺寸 (Size)
MeasureSpec.getSize(spec)
获取。模式名称 | 常量 | 对应布局属性 | 含义 |
---|---|---|---|
EXACTLY |
MeasureSpec.EXACTLY |
match_parent / 明确尺寸 |
精确模式:父视图已为子视图指定了确切大小,子视图应遵从该尺寸 |
AT_MOST |
MeasureSpec.AT_MOST |
wrap_content |
最大模式:子视图大小不能超过父视图指定值,通常根据内容自适应 |
UNSPECIFIED |
MeasureSpec.UNSPECIFIED |
ScrollView 等特殊容器 | 无约束模式:父视图对子视图没有尺寸限制,子视图可任意设定自身尺寸 |
View.MeasureSpec.makeMeasureSpec(size, mode)
View.MeasureSpec.getMode(spec)
和 View.MeasureSpec.getSize(spec)
onLayout()
- 确定位置onLayout()
决定了 View “在哪里”。其职责是确定所有子 View 的最终位置和大小。
注意:此方法主要在自定义
ViewGroup
时重写。普通 View 不需要重写。
在 onLayout()
中,ViewGroup
需遍历子 View,并调用每个子 View 的 layout(left, top, right, bottom)
方法来设置位置。
onDraw()
- 绘制内容onDraw()
决定了 View “长什么样”。职责是使用 Canvas
和 Paint
将 View 的内容绘制到屏幕上。
Canvas (画布)
提供绘图 API,如 drawRect()
、drawText()
、drawBitmap()
、drawPath()
等。
Paint (画笔)
定义绘图样式,如颜色、线宽、抗锯齿、填充模式、字体大小等。
性能提示:避免在
onDraw()
中创建新对象,尤其是Paint
。应在构造函数或初始化代码块中初始化。
invalidate()
vs requestLayout()
方法 | 触发流程 | 适用场景 |
---|---|---|
invalidate() |
触发 onDraw() |
内容改变但尺寸/位置不变 |
requestLayout() |
触发 onMeasure() → onLayout() → onDraw() |
尺寸或结构发生变化 |
为使自定义 View 更加灵活可配置,可以通过 XML 声明自定义属性。
res/values/attrs.xml
中定义属性集<resources>
<declare-styleable name="MyCustomView">
<attr name="titleText" format="string" />
<attr name="titleColor" format="color" />
<attr name="titleSize" format="dimension" />
</declare-styleable>
</resources>
<com.example.MyCustomView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:titleText="Hello, Custom View!"
app:titleColor="@color/purple_500"
app:titleSize="16sp" />
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import androidx.core.content.withStyledAttributes
class MyCustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var titleText: String? = "Default Title"
private var titleColor: Int = Color.BLACK
init {
context.withStyledAttributes(attrs, R.styleable.MyCustomView) {
titleText = getString(R.styleable.MyCustomView_titleText)
titleColor = getColor(R.styleable.MyCustomView_titleColor, Color.BLACK)
}
// 可在此初始化 Paint 等资源
}
// 后续重写 onMeasure, onDraw 等方法
}
最佳实践:使用 Kotlin 的
use
块或withStyledAttributes
,确保TypedArray.recycle()
被安全调用,避免内存泄漏。