mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Improve spoiler drawing performance.
This commit is contained in:
@@ -2,147 +2,37 @@ package org.thoughtcrime.securesms.components.spoiler
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Drawable that animates a sparkle effect for spoilers.
|
||||
*/
|
||||
class SpoilerDrawable(@ColorInt color: Int) : Drawable() {
|
||||
|
||||
private val alphaStrength = arrayOf(0.9f, 0.7f, 0.5f)
|
||||
private val paints = listOf(Paint(), Paint(), Paint())
|
||||
private var lastDrawTime: Long = 0
|
||||
|
||||
private var particleCount = 60
|
||||
|
||||
private var allParticles = Array(3) { Array(particleCount) { Particle(random) } }
|
||||
private var allPoints = Array(3) { FloatArray(particleCount * 2) { 0f } }
|
||||
|
||||
init {
|
||||
for (paint in paints) {
|
||||
paint.strokeCap = Paint.Cap.ROUND
|
||||
paint.strokeWidth = DimensionUnit.DP.toPixels(1.5f)
|
||||
}
|
||||
|
||||
alpha = 255
|
||||
colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
val pixelArea = (bounds.right - bounds.left) * (bounds.bottom - bounds.top)
|
||||
|
||||
val newParticleCount = (pixelArea.toFloat() * PARTICLES_PER_PIXEL).toInt()
|
||||
if (newParticleCount != particleCount) {
|
||||
if (newParticleCount > allParticles[0].size) {
|
||||
allParticles = Array(3) { i ->
|
||||
Array(newParticleCount) { particleIndex ->
|
||||
allParticles[i].getOrNull(particleIndex) ?: Particle(random)
|
||||
}
|
||||
}
|
||||
|
||||
allPoints = Array(3) { i ->
|
||||
FloatArray(newParticleCount * 2) { pointIndex ->
|
||||
allPoints[i].getOrNull(pointIndex) ?: 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
particleCount = newParticleCount
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val left = bounds.left
|
||||
val top = bounds.top
|
||||
val right = bounds.right
|
||||
val bottom = bounds.bottom
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val dt = now - lastDrawTime
|
||||
lastDrawTime = now
|
||||
|
||||
for (allIndex in allParticles.indices) {
|
||||
val particles = allParticles[allIndex]
|
||||
for (index in 0 until particleCount) {
|
||||
val particle = particles[index]
|
||||
|
||||
particle.timeRemaining = particle.timeRemaining - dt
|
||||
if (particle.timeRemaining < 0 || !bounds.contains(particle.x.toInt(), particle.y.toInt())) {
|
||||
particle.x = (random.nextFloat() * (right - left)) + left
|
||||
particle.y = (random.nextFloat() * (bottom - top)) + top
|
||||
particle.xVel = nextDirection()
|
||||
particle.yVel = nextDirection()
|
||||
particle.timeRemaining = 350 + 750 * random.nextFloat()
|
||||
} else {
|
||||
val change = dt * velocity
|
||||
particle.x += particle.xVel * change
|
||||
particle.y += particle.yVel * change
|
||||
}
|
||||
|
||||
allPoints[allIndex][index * 2] = particle.x
|
||||
allPoints[allIndex][index * 2 + 1] = particle.y
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPoints(allPoints[0], 0, particleCount * 2, paints[0])
|
||||
canvas.drawPoints(allPoints[1], 0, particleCount * 2, paints[1])
|
||||
canvas.drawPoints(allPoints[2], 0, particleCount * 2, paints[2])
|
||||
canvas.drawRect(bounds, SpoilerPaint.paint)
|
||||
SpoilerPaint.update()
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paints.forEachIndexed { index, paint ->
|
||||
paint.alpha = (alpha * alphaStrength[index]).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
for (paint in paints) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
SpoilerPaint.applyAlpha(alpha)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.TRANSPARENT", "android.graphics.PixelFormat"))
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSPARENT
|
||||
return SpoilerPaint.paint.alpha
|
||||
}
|
||||
|
||||
data class Particle(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
var xVel: Float,
|
||||
var yVel: Float,
|
||||
var timeRemaining: Float
|
||||
) {
|
||||
constructor(random: Random) : this(
|
||||
-1f,
|
||||
-1f,
|
||||
if (random.nextFloat() < 0.5f) 1f else -1f,
|
||||
if (random.nextFloat() < 0.5f) 1f else -1f,
|
||||
500 + 1000 * random.nextFloat()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PARTICLES_PER_PIXEL = if (Util.isLowMemory(ApplicationDependencies.getApplication())) 0.002f else 0.005f
|
||||
private val velocity: Float = DimensionUnit.DP.toPixels(16f) / 1000f
|
||||
private val random = Random(System.currentTimeMillis())
|
||||
|
||||
fun nextDirection(): Float {
|
||||
val rand = random.nextFloat()
|
||||
return if (rand < 0.5f) {
|
||||
0.1f + 0.9f * rand
|
||||
} else {
|
||||
-0.1f - 0.9f * (rand - 0.5f)
|
||||
}
|
||||
}
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
SpoilerPaint.applyColorFilter(colorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.thoughtcrime.securesms.components.spoiler
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapShader
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Shader
|
||||
import androidx.annotation.MainThread
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A wrapper around a paint object that can be used to apply the spoiler effect.
|
||||
*
|
||||
* The intended usage pattern is to call [update] before your item needs to be drawn.
|
||||
* Then, draw with the [paint].
|
||||
*/
|
||||
object SpoilerPaint {
|
||||
|
||||
/**
|
||||
* A paint that can be used to apply the spoiler effect.
|
||||
*/
|
||||
val paint: Paint = Paint()
|
||||
|
||||
private val SIZE = if (Util.isLowMemory(ApplicationDependencies.getApplication())) 100.dp else 200.dp
|
||||
private val PARTICLES_PER_PIXEL = if (Util.isLowMemory(ApplicationDependencies.getApplication())) 0.002f else 0.005f
|
||||
|
||||
private var shaderBitmap: Bitmap = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888)
|
||||
private var bufferBitmap: Bitmap = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888)
|
||||
|
||||
private val bounds: Rect = Rect(0, 0, SIZE, SIZE)
|
||||
private val paddedBounds: Rect
|
||||
|
||||
private val alphaStrength = arrayOf(0.9f, 0.7f, 0.5f)
|
||||
private val particlePaints = listOf(Paint(), Paint(), Paint())
|
||||
private var lastDrawTime: Long = 0
|
||||
private val random = Random(System.currentTimeMillis())
|
||||
|
||||
private var particleCount = ((bounds.width() * bounds.height()) * PARTICLES_PER_PIXEL).toInt()
|
||||
private val allParticles = Array(3) { Array(particleCount) { Particle(random) } }
|
||||
private val allPoints = Array(3) { FloatArray(particleCount * 2) { 0f } }
|
||||
|
||||
private val velocity: Float = DimensionUnit.DP.toPixels(16f) / 1000f
|
||||
|
||||
init {
|
||||
val strokeWidth = DimensionUnit.DP.toPixels(1.5f)
|
||||
|
||||
particlePaints.forEach { paint ->
|
||||
paint.strokeCap = Paint.Cap.ROUND
|
||||
paint.strokeWidth = strokeWidth
|
||||
}
|
||||
|
||||
paddedBounds = Rect(
|
||||
bounds.left - strokeWidth.toInt(),
|
||||
bounds.top - strokeWidth.toInt(),
|
||||
bounds.right + strokeWidth.toInt(),
|
||||
bounds.bottom + strokeWidth.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke every time before you need to use the [paint].
|
||||
*/
|
||||
@MainThread
|
||||
fun update() {
|
||||
val now = System.currentTimeMillis()
|
||||
val dt = now - lastDrawTime
|
||||
if (dt < 16) {
|
||||
return
|
||||
}
|
||||
lastDrawTime = now
|
||||
|
||||
// The shader draws the live contents of the bitmap at potentially any point.
|
||||
// That means that if we draw directly to the in-use bitmap, it could potentially flicker.
|
||||
// To avoid that, we draw into a buffer, then swap the buffer into the shader when it's fully drawn.
|
||||
val canvas = Canvas(bufferBitmap)
|
||||
bufferBitmap.eraseColor(Color.TRANSPARENT)
|
||||
draw(canvas, dt)
|
||||
|
||||
val swap = shaderBitmap
|
||||
shaderBitmap = bufferBitmap
|
||||
bufferBitmap = swap
|
||||
|
||||
paint.shader = BitmapShader(shaderBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun applyColorFilter(colorFilter: ColorFilter?) {
|
||||
for (paint in particlePaints) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
lastDrawTime = 0
|
||||
update()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun applyAlpha(alpha: Int) {
|
||||
particlePaints.forEachIndexed { index, paint ->
|
||||
paint.alpha = (alpha * alphaStrength[index]).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the newest particle state into the canvas based on [dt].
|
||||
*
|
||||
* Note that we use [paddedBounds] here so that the draw space starts just outside the visible area.
|
||||
*
|
||||
* This is because we're going to end up tiling this, and being fuzzy around the edges helps reduce
|
||||
* the visual "gaps" between the tiles.
|
||||
*/
|
||||
private fun draw(canvas: Canvas, dt: Long) {
|
||||
for (allIndex in allParticles.indices) {
|
||||
val particles = allParticles[allIndex]
|
||||
for (index in 0 until particleCount) {
|
||||
val particle = particles[index]
|
||||
|
||||
particle.timeRemaining = particle.timeRemaining - dt
|
||||
if (particle.timeRemaining < 0 || !paddedBounds.contains(particle.x.toInt(), particle.y.toInt())) {
|
||||
particle.x = (random.nextFloat() * paddedBounds.width()) + paddedBounds.left
|
||||
particle.y = (random.nextFloat() * paddedBounds.height()) + paddedBounds.top
|
||||
particle.xVel = nextDirection()
|
||||
particle.yVel = nextDirection()
|
||||
particle.timeRemaining = 350 + 750 * random.nextFloat()
|
||||
} else {
|
||||
val change = dt * velocity
|
||||
particle.x += particle.xVel * change
|
||||
particle.y += particle.yVel * change
|
||||
}
|
||||
|
||||
allPoints[allIndex][index * 2] = particle.x
|
||||
allPoints[allIndex][index * 2 + 1] = particle.y
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPoints(allPoints[0], 0, particleCount * 2, particlePaints[0])
|
||||
canvas.drawPoints(allPoints[1], 0, particleCount * 2, particlePaints[1])
|
||||
canvas.drawPoints(allPoints[2], 0, particleCount * 2, particlePaints[2])
|
||||
}
|
||||
|
||||
private fun nextDirection(): Float {
|
||||
val rand = random.nextFloat()
|
||||
return if (rand < 0.5f) {
|
||||
0.1f + 0.9f * rand
|
||||
} else {
|
||||
-0.1f - 0.9f * (rand - 0.5f)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Particle(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
var xVel: Float,
|
||||
var yVel: Float,
|
||||
var timeRemaining: Float
|
||||
) {
|
||||
constructor(random: Random) : this(
|
||||
x = -1f,
|
||||
y = -1f,
|
||||
xVel = if (random.nextFloat() < 0.5f) 1f else -1f,
|
||||
yVel = if (random.nextFloat() < 0.5f) 1f else -1f,
|
||||
timeRemaining = 500 + 1000 * random.nextFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user