Allow spoiler paint to be tinted independently per renderer.
This commit is contained in:
parent
a183057b32
commit
99ac2cb333
6 changed files with 59 additions and 89 deletions
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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."/>
|
||||
|
||||
|
|
|
@ -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."/>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue