Allow spoiler paint to be tinted independently per renderer.

This commit is contained in:
Cody Henthorne 2023-04-10 21:50:02 -04:00 committed by Greyson Parrelli
parent a183057b32
commit 99ac2cb333
6 changed files with 59 additions and 89 deletions

View file

@ -2,6 +2,8 @@ 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.drawable.Drawable
@ -12,27 +14,32 @@ import androidx.annotation.ColorInt
*/
class SpoilerDrawable(@ColorInt color: Int) : Drawable() {
private val paint = Paint()
init {
alpha = 255
colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
setTintColor(color)
}
fun setTintColor(@ColorInt color: Int) {
paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
}
override fun draw(canvas: Canvas) {
canvas.drawRect(bounds, SpoilerPaint.paint)
SpoilerPaint.update()
invalidateSelf()
paint.shader = SpoilerPaint.shader
canvas.drawRect(bounds, paint)
}
override fun setAlpha(alpha: Int) {
SpoilerPaint.applyAlpha(alpha)
paint.alpha = alpha
}
@Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.TRANSPARENT", "android.graphics.PixelFormat"))
override fun getOpacity(): Int {
return SpoilerPaint.paint.alpha
return PixelFormat.TRANSPARENT
}
override fun setColorFilter(colorFilter: ColorFilter?) {
SpoilerPaint.applyColorFilter(colorFilter)
throw UnsupportedOperationException("Call setTintColor")
}
}

View file

@ -4,7 +4,6 @@ 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
@ -26,7 +25,7 @@ object SpoilerPaint {
/**
* A paint that can be used to apply the spoiler effect.
*/
val paint: Paint = Paint()
var shader: BitmapShader? = null
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
@ -51,7 +50,8 @@ object SpoilerPaint {
init {
val strokeWidth = DimensionUnit.DP.toPixels(1.5f)
particlePaints.forEach { paint ->
particlePaints.forEachIndexed { index, paint ->
paint.alpha = (255 * alphaStrength[index]).toInt()
paint.strokeCap = Paint.Cap.ROUND
paint.strokeWidth = strokeWidth
}
@ -62,6 +62,8 @@ object SpoilerPaint {
bounds.right + strokeWidth.toInt(),
bounds.bottom + strokeWidth.toInt()
)
update()
}
/**
@ -70,9 +72,11 @@ object SpoilerPaint {
@MainThread
fun update() {
val now = System.currentTimeMillis()
val dt = now - lastDrawTime
if (dt < 16) {
var dt = now - lastDrawTime
if (dt < 32) {
return
} else if (dt > 48) {
dt = 32
}
lastDrawTime = now
@ -87,24 +91,7 @@ object SpoilerPaint {
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()
}
shader = BitmapShader(shaderBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
}
/**

View file

@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.components.spoiler
import android.graphics.Canvas
import android.text.Layout
import org.thoughtcrime.securesms.util.LayoutUtil
import kotlin.math.max
import kotlin.math.min
/**
* Handles drawing the spoiler sparkles for a TextView.
@ -17,8 +15,7 @@ abstract class SpoilerRenderer {
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int,
spoilerDrawables: List<SpoilerDrawable>
endOffset: Int
)
protected fun getLineTop(layout: Layout, line: Int): Int {
@ -33,7 +30,7 @@ abstract class SpoilerRenderer {
return getOrPut(line * 31 + layout.hashCode() * 31, default)
}
class SingleLineSpoilerRenderer : SpoilerRenderer() {
class SingleLineSpoilerRenderer(private val spoilerDrawable: SpoilerDrawable) : SpoilerRenderer() {
private val lineTopCache = HashMap<Int, Int>()
private val lineBottomCache = HashMap<Int, Int>()
@ -43,20 +40,19 @@ abstract class SpoilerRenderer {
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int,
spoilerDrawables: List<SpoilerDrawable>
endOffset: Int
) {
val lineTop = lineTopCache.get(startLine, layout) { getLineTop(layout, startLine) }
val lineBottom = lineBottomCache.get(startLine, layout) { getLineBottom(layout, startLine) }
val left = startOffset.coerceAtMost(endOffset)
val right = startOffset.coerceAtLeast(endOffset)
spoilerDrawables[0].setBounds(left, lineTop, right, lineBottom)
spoilerDrawables[0].draw(canvas)
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
spoilerDrawable.draw(canvas)
}
}
class MultiLineSpoilerRenderer : SpoilerRenderer() {
class MultiLineSpoilerRenderer(private val spoilerDrawable: SpoilerDrawable) : SpoilerRenderer() {
private val lineTopCache = HashMap<Int, Int>()
private val lineBottomCache = HashMap<Int, Int>()
@ -66,56 +62,48 @@ abstract class SpoilerRenderer {
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int,
spoilerDrawables: List<SpoilerDrawable>
endOffset: Int
) {
val paragraphDirection = layout.getParagraphDirection(startLine)
val lineEndOffset: Float = if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) layout.getLineLeft(startLine) else layout.getLineRight(startLine)
var lineBottom = lineBottomCache.get(startLine, layout) { getLineBottom(layout, startLine) }
var lineTop = lineTopCache.get(startLine, layout) { getLineTop(layout, startLine) }
drawStart(canvas, startOffset, lineTop, lineEndOffset.toInt(), lineBottom, spoilerDrawables)
drawStart(canvas, startOffset, lineTop, lineEndOffset.toInt(), lineBottom)
if (startLine + 1 < endLine) {
var left = Int.MAX_VALUE
var right = -1
lineTop = Int.MAX_VALUE
lineBottom = -1
for (line in startLine + 1 until endLine) {
left = min(left, layout.getLineLeft(line).toInt())
right = max(right, layout.getLineRight(line).toInt())
for (line in startLine + 1 until endLine) {
val left: Int = layout.getLineLeft(line).toInt()
val right: Int = layout.getLineRight(line).toInt()
lineTop = min(lineTop, lineTopCache.get(line, layout) { getLineTop(layout, line) })
lineBottom = max(lineBottom, lineBottomCache.get(line, layout) { getLineBottom(layout, line) })
}
spoilerDrawables[1].setBounds(left, lineTop, right, lineBottom)
spoilerDrawables[1].draw(canvas)
lineTop = getLineTop(layout, line)
lineBottom = getLineBottom(layout, line)
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
spoilerDrawable.draw(canvas)
}
val lineStartOffset: Float = if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) layout.getLineRight(startLine) else layout.getLineLeft(startLine)
lineBottom = lineBottomCache.get(endLine, layout) { getLineBottom(layout, endLine) }
lineTop = lineTopCache.get(endLine, layout) { getLineTop(layout, endLine) }
drawEnd(canvas, lineStartOffset.toInt(), lineTop, endOffset, lineBottom, spoilerDrawables)
drawEnd(canvas, lineStartOffset.toInt(), lineTop, endOffset, lineBottom)
}
private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int, spoilerDrawables: List<SpoilerDrawable>) {
private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
if (start > end) {
spoilerDrawables[2].setBounds(end, top, start, bottom)
spoilerDrawables[2].draw(canvas)
spoilerDrawable.setBounds(end, top, start, bottom)
} else {
spoilerDrawables[0].setBounds(start, top, end, bottom)
spoilerDrawables[0].draw(canvas)
spoilerDrawable.setBounds(start, top, end, bottom)
}
spoilerDrawable.draw(canvas)
}
private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int, spoilerDrawables: List<SpoilerDrawable>) {
private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
if (start > end) {
spoilerDrawables[0].setBounds(end, top, start, bottom)
spoilerDrawables[0].draw(canvas)
spoilerDrawable.setBounds(end, top, start, bottom)
} else {
spoilerDrawables[2].setBounds(start, top, end, bottom)
spoilerDrawables[2].draw(canvas)
spoilerDrawable.setBounds(start, top, end, bottom)
}
spoilerDrawable.draw(canvas)
}
}
}

View file

@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.components.spoiler
import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.text.Annotation
import android.text.Layout
import android.text.Spanned
@ -21,36 +19,35 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
private val single: SpoilerRenderer
private val multi: SpoilerRenderer
private val spoilerDrawable: SpoilerDrawable
private var animatorRunning = false
private var textColor: Int
private var spoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
private var nextSpoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
private val cachedAnnotations = HashMap<Int, Map<Annotation, SpoilerClickableSpan?>>()
private val cachedMeasurements = HashMap<Int, SpanMeasurements>()
private val animator = ValueAnimator.ofInt(0, 100).apply {
duration = 1000
interpolator = LinearInterpolator()
addUpdateListener { view.invalidate() }
addUpdateListener {
SpoilerPaint.update()
view.invalidate()
}
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
}
init {
single = SingleLineSpoilerRenderer()
multi = MultiLineSpoilerRenderer()
textColor = view.textColors.defaultColor
spoilerDrawable = SpoilerDrawable(textColor)
single = SingleLineSpoilerRenderer(spoilerDrawable)
multi = MultiLineSpoilerRenderer(spoilerDrawable)
}
fun updateFromTextColor() {
val color = view.textColors.defaultColor
if (color != textColor) {
spoilerDrawablePool
.values
.flatten()
.forEach { it.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) }
spoilerDrawable.setTintColor(color)
textColor = color
}
}
@ -59,7 +56,6 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
var hasSpoilersToRender = false
val annotations: Map<Annotation, SpoilerClickableSpan?> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAndClickAnnotations(text) }
nextSpoilerDrawablePool.clear()
for ((annotation, clickSpan) in annotations.entries) {
if (clickSpan?.spoilerRevealed == true) {
continue
@ -83,17 +79,11 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
}
val renderer: SpoilerRenderer = if (measurements.startLine == measurements.endLine) single else multi
val drawables: List<SpoilerDrawable> = spoilerDrawablePool[annotation] ?: listOf(SpoilerDrawable(textColor), SpoilerDrawable(textColor), SpoilerDrawable(textColor))
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset, drawables)
nextSpoilerDrawablePool[annotation] = drawables
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset)
hasSpoilersToRender = true
}
val temporaryPool = spoilerDrawablePool
spoilerDrawablePool = nextSpoilerDrawablePool
nextSpoilerDrawablePool = temporaryPool
if (hasSpoilersToRender) {
if (!animatorRunning) {
animator.start()

View file

@ -22,7 +22,6 @@
style="@style/Signal.Text.Body"
android:textColor="@color/signal_text_primary"
android:textColorLink="@color/signal_text_primary"
android:textIsSelectable="true"
app:scaleEmojis="true"
tools:text="With great power comes great responsibility."/>

View file

@ -22,7 +22,6 @@
style="@style/Signal.Text.Body"
android:textColor="@color/conversation_item_sent_text_primary_color"
android:textColorLink="@color/conversation_item_sent_text_primary_color"
android:textIsSelectable="true"
app:scaleEmojis="true"
tools:text="With great power comes great responsibility."/>