如何在自定义视图中更新油漆着色器?

How to update paint shader in a custom view?

我在一个自定义视图中工作,该视图根据触摸位置绘制一个圆圈。后来我想根据位置更改圆的大小和颜色,但现在我很担心,因为在 onDraw 方法中,当我创建 RadialGradient 并将其添加到 paint.shader、Android Studio 显示时我一个 DrawAllocation 警告。我想知道是否有办法避免这个警告或者是否有更好的方法来实现这个,事实是我想用动态值绘制渐变,在这种情况下,我唯一需要的动态值是中心的 RadialGradient,但稍后我会绘制更多具有更多渐变的圆圈,我不想出现性能问题。希望你能帮助我

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

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var touch = PointF(0.0f, 0.0f)
    private val size = SIZE_DP * resources.displayMetrics.density
    private var centerX = 0f
    private var centerY = 0f

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        touch = PointF(event.x, event.y)
        return true
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX = w * 0.5f
        centerY = h * 0.5f
        touch = PointF(centerX, centerY)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Here is where the warning shows
        // I would like to have only one RadialGradient object and only change the 
        // center coordinates here something like gradient.centerX = touch.x
        paint.shader = RadialGradient(
            touch.x, touch.y, SIZE_DP * 4,
            Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP
        )
        canvas.drawCircle(
            touch.x, touch.y,
            size, paint
        )
        postInvalidate()
    }

    companion object {
        const val SIZE_DP = 100.0f
    }
}

我以前没有使用过渐变,但据我所知,当你定义一个(及其坐标)时,你实际上是在 canvas 上“绘制”渐变,当你在 canvas 上的某处画一个圆圈,您基本上是在窥视后面的 pre-defined 渐变。它不以您正在绘制的圆为中心。

因此,在那种情况下,每次您想在其他地方画一个新圆时,必须创建一个新的RadialGradient。您收到的警告基本上是因为 onDraw 可能每帧调用一次(在某些设备上每 16 毫秒或更快),并且在其中分配内容是一种快速积累内存使用量的方法。但这取决于你 - 如果你知道它只会偶尔被调用,你可以取消警告(将光标放在警告上并选择一种快速修复)


但有一个建议 - 您在 onDraw 中调用 postInvalidate()(您可以只使用 invalidate()),这 确保 它会下一帧再次绘制。据我所知,您实际上不需要这样做 - 您只需要在圆圈发生变化时重新绘制视图,也就是当您收到另一个触摸事件时,对吗?

因此,您可以从 dispatchTouchEvent 方法中调用 invalidate()。每次收到新事件时,告诉视图自行更新。您正在那里更新 touch 的当前值 - 您也可以在那里创建 RadialGradient,并将其设置为 paint。这样,您的 onDraw 只需要使用当前的 painttouch 画一个圆圈。而且您不需要抑制任何警告,因为分配不会在那里发生。

由于您也在 onSizeChanged 中更新了 touch,最好将所有更新放在一个函数中,在一个地方更新状态:

fun updateCirclePosition(x: Int, y: Int) {
    touch = PointF(x, y)
    paint.shader = RadialGradient(
        x, y, SIZE_DP * 4,
        Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP
    )
}

并从更新 touch 的函数中调用它。这样 paint 就符合了。你甚至可以用它变得更聪明:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    // no redrawing / reallocating unless necessary
    // you might need to check the type of event though (e.g. lifted finger)
    if (event.x != touch.x || event.y != touch.y) {
        updateCirclePosition(event.x, event.y)
        invalidate()
    }
    return true
}

您甚至可以将 invalidate 粘贴在 updateCirclePosition 中,因为实际上,更新该位置意味着视图内容无效并且需要重新绘制