自定义 View 的绘制逻辑和触摸事件处理,通常在 Demo 里运行正常,但嵌入复杂列表、ViewPager 或嵌套滚动容器后,就会出现滑动冲突、绘制卡顿、状态丢失等线上问题。本文从一个可复现的自定义进度条场景出发,拆解测量、布局、绘制、事件分发和状态保存的完整链路,给出可验证的实现策略。
自定义 View 要回答三个问题:
还有两个隐藏问题:
很多卡顿和冲突,源于把"绘制逻辑"和"状态更新"混在一起,或者在 onDraw 里做了超出绘制范围的事情。
父容器通过 MeasureSpec 告诉你可用空间和约束:
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)
val desiredWidth = (defaultWidth * resources.displayMetrics.density).toInt()
val desiredHeight = (defaultHeight * resources.displayMetrics.density).toInt()
val finalWidth = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
val finalHeight = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(finalWidth, finalHeight)
}不调用 `setMeasuredDimension()` 会崩溃。wrap_content 时如果直接返回父容器给的最大尺寸,View 会填满整个可用空间,看起来和 match_parent 一样。
onDraw 会被频繁调用(滚动、动画、焦点变化),所以它必须快,并且不应该有副作用。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// ❌ 错误:在 onDraw 里创建对象
// val paint = Paint()
// ✅ 正确:Paint 在构造函数或 init 块里创建
paint.color = progressColor
paint.strokeWidth = barHeight
val progressWidth = width * (progress / 100f)
canvas.drawLine(0f, height / 2f, progressWidth, height / 2f, paint)
}常见错误:
Paint、Path、Rect 等绘制工具应该在初始化时创建并复用。如果需要根据尺寸调整,放在 onSizeChanged 里。
更新进度后需要重新绘制:
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 100f)
invalidate() // 主线程调用
}
// 后台线程更新时用 postInvalidate()
fun updateFromBackground(newProgress: Float) {
progress = newProgress
postInvalidate()
}invalidate 会触发整个 View 重绘。如果只有局部区域变化,可以用 `invalidate(left, top, right, bottom)` 减少绘制范围,但要确保坐标计算正确。
当自定义 View 嵌套在 ScrollView、ViewPager 或 RecyclerView 里,用户滑动时可能被父容器拦截,导致自定义 View 收不到事件。
**事件分发流程**:
1. **dispatchTouchEvent**:事件入口,决定是自己处理还是传给子 View
2. **onInterceptTouchEvent**:ViewGroup 专用,决定是否拦截事件
3. **onTouchEvent**:实际处理事件
**场景 1:水平滑动进度条嵌套在垂直 RecyclerView**
用户水平滑动进度条时,RecyclerView 可能误判为垂直滚动并拦截事件。解决方式:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
lastX = event.x
return true
}
MotionEvent.ACTION_MOVE -> {
val delta = event.x - lastX
progress += (delta / width) * 100
lastX = event.x
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
parent.requestDisallowInterceptTouchEvent(false)
return true
}
}
return super.onTouchEvent(event)
}关键是 `requestDisallowInterceptTouchEvent(true)`,告诉父容器"这个事件序列我要独占,你别拦截"。
**场景 2:自定义 ViewGroup 需要拦截子 View 的滑动**
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return when (event.action) {
MotionEvent.ACTION_MOVE -> {
val dx = abs(event.x - downX)
val dy = abs(event.y - downY)
// 水平滑动距离大于垂直,拦截
dx > dy && dx > touchSlop
}
else -> false
}
}一旦 onInterceptTouchEvent 返回 true,后续事件会直接到达自己的 onTouchEvent,子 View 会收到 ACTION_CANCEL。
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val savedState = SavedState(superState)
savedState.progress = progress
return savedState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
progress = state.progress
} else {
super.onRestoreInstanceState(state)
}
}
private class SavedState : BaseSavedState {
var progress: Float = 0f
constructor(superState: Parcelable?) : super(superState)
constructor(source: Parcel) : super(source) {
progress = source.readFloat()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeFloat(progress)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int) = arrayOfNulls<SavedState>(size)
}
}
}如果不实现状态保存,屏幕旋转后进度会重置为初始值。
**开启硬件加速**(API 14+ 默认开启):
init {
setLayerType(LAYER_TYPE_HARDWARE, null)
}但硬件加速不支持某些绘制操作(如 drawPicture),此时需要降级:
setLayerType(LAYER_TYPE_SOFTWARE, null)**减少过度绘制**:
**避免在 onDraw 里做重计算**:
至少覆盖:
用 Robolectric 可以测量、布局和部分绘制逻辑,但触摸事件和硬件加速相关的行为需要真机或模拟器验证。
自定义 View 的稳定性不是单纯的绘制技巧,而是"测量模式、事件分发、状态保存、性能优化和可测试性"的组合工程。先保证测量逻辑能适配各种父容器,再保证触摸事件不被误拦截或误消费,最后用状态保存和硬件加速让 View 在配置变更和高频刷新下仍然稳定,问题才能从偶发卡顿变成可验证的工程行为。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。