Android 自定义 View 与绘制性能
Android 自定义 View 与绘制性能
自定义 View 让界面可以突破标准控件的限制,但也把绘制性能的责任交给了开发者。onMeasure、onLayout、onDraw 的调用频率很高,任何不必要的对象创建、布局读取和复杂计算都可能放大成滚动掉帧。
一、测量模式必须正确处理
onMeasure 中必须处理 EXACTLY、AT_MOST 和 UNSPECIFIED 三种模式,否则控件在不同父布局中会异常。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = resolveSize(desiredWidth, widthMeasureSpec)
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
}
不要在 onMeasure 中创建对象或执行耗时计算。需要缓存的数据应在尺寸变化时计算一次。
二、绘制对象提前创建
onDraw 可能每帧执行,Paint、Path 和 Shader 等对象应提前创建,只在属性变化时更新。
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 4f
}
在 onDraw 中新建对象会增加内存分配压力,进而引发更频繁的垃圾回收,造成间歇性卡顿。
三、不要在绘制阶段读取布局
onDraw 中调用 width、height 是安全的,但不要触发重新测量或布局。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
backgroundBounds.set(0f, 0f, w.toFloat(), h.toFloat())
}
尺寸相关的 Path、RectF 和渐变应在 onSizeChanged 中更新,绘制时只使用缓存值。
四、动画时只重绘变化区域
小面积动画不需要让整个 View 重绘。可以用脏区域减少绘制成本。
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(centerX, centerY, radius, paint)
}
如果使用 invalidate(left, top, right, bottom),必须准确计算变化区域。区域计算比全量重绘更复杂时,不应强行使用。
五、使用硬件加速友好的绘制 API
自定义 View 默认运行在硬件加速环境,应优先使用 Canvas、Shader 和 Matrix 等加速友好的能力。
canvas.save()
canvas.rotate(angle, centerX, centerY)
canvas.drawRect(bounds, paint)
canvas.restore()
避免在绘制中使用大量位图逐像素操作。像素级处理应放到后台线程,完成后再更新位图并触发重绘。
六、自定义属性支持主题
自定义属性应支持通过主题或样式覆盖,避免使用者只能写死颜色和尺寸。
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressRing)
try {
ringColor = typedArray.getColor(R.styleable.ProgressRing_ringColor, defaultRingColor)
ringWidth = typedArray.getDimension(R.styleable.ProgressRing_ringWidth, defaultRingWidth)
} finally {
typedArray.recycle()
}
TypedArray 必须及时回收。深色模式下需要使用语义化颜色资源,而不是固定色值。
七、触摸反馈不要依赖连续重绘
按压态变化应优先使用 Drawable 状态或属性动画,避免在 onTouchEvent 中无差别调用 invalidate。
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isPressed = true
invalidate()
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isPressed = false
invalidate()
performClick()
return true
}
}
return super.onTouchEvent(event)
}
自定义触摸必须调用 performClick,否则无法正确暴露给无障碍服务。
八、不要过度自定义
系统控件已经处理了无障碍、输入法、文本选择和触控反馈。能用 TextView、Button、RecyclerView 承载的界面,不应为了视觉效果重写整套控件。
class ProgressRing @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)
自定义 View 的职责要单一。把布局、业务判断和数据处理都写进 View,会让后续维护变得困难。
九、状态更新触发正确流程
属性变化后应触发对应的流程:只影响绘制时调用 invalidate,影响尺寸时调用 requestLayout。
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 1f)
invalidate()
}
不要每次都同时调用 requestLayout 和 invalidate。不必要的测量布局会显著增加列表滚动成本。
十、用工具验证性能
自定义 View 完成后应通过真实数据验证性能。
- 滚动过程中是否有掉帧。
- 绘制阶段是否创建对象。
- 动画是否持续触发重绘。
- 触摸是否正确支持无障碍。
- 不同屏幕密度下是否清晰。
总结
自定义 View 的性能原则是把工作前移。测量阶段处理约束,尺寸变化阶段更新缓存,绘制阶段只消费结果。绘制对象提前创建,状态更新触发正确流程,动画控制重绘范围。优先复用系统控件,确需自定义时保持职责单一,并用真实场景验证帧率。
- 点赞
- 收藏
- 关注作者
评论(0)