Fix split second spoiler reveal when quoting a message with a spoiler.

This commit is contained in:
Cody Henthorne 2023-05-19 12:42:52 -04:00 committed by Greyson Parrelli
parent 131f9c4bc9
commit f2846efd2c
9 changed files with 34 additions and 51 deletions

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable;
@ -55,6 +56,10 @@ public class EmojiSpan extends AnimatingImageSpan {
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
if (paint.getColor() == Color.TRANSPARENT) {
return;
}
int height = bottom - top;
int centeringMargin = (height - size) / 2;
int adjustedMargin = (int) (centeringMargin * SHIFT_FACTOR);

View file

@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Annotation;
@ -70,7 +68,6 @@ public class EmojiTextView extends AppCompatTextView {
private TextDirectionHeuristic textDirection;
private boolean isJumbomoji;
private boolean forceJumboEmoji;
private boolean isInOnDraw;
private MentionRendererDelegate mentionRendererDelegate;
private final SpoilerRendererDelegate spoilerRendererDelegate;
@ -116,16 +113,11 @@ public class EmojiTextView extends AppCompatTextView {
@Override
protected void onDraw(Canvas canvas) {
isInOnDraw = true;
boolean hasSpannedText = getText() instanceof Spanned;
boolean hasLayout = getLayout() != null;
if (hasSpannedText && hasLayout) {
Path textClipPath = drawSpecialRenderers(canvas, mentionRendererDelegate, spoilerRendererDelegate);
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
canvas.clipPath(textClipPath, Region.Op.DIFFERENCE);
canvas.translate(-getTotalPaddingLeft(), -getTotalPaddingTop());
drawSpecialRenderers(canvas, mentionRendererDelegate, spoilerRendererDelegate);
}
super.onDraw(canvas);
@ -133,18 +125,16 @@ public class EmojiTextView extends AppCompatTextView {
if (hasSpannedText && !hasLayout && getLayout() != null) {
drawSpecialRenderers(canvas, null, spoilerRendererDelegate);
}
isInOnDraw = false;
}
private Path drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @NonNull SpoilerRendererDelegate spoilerDelegate) {
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @NonNull SpoilerRendererDelegate spoilerDelegate) {
int checkpoint = canvas.save();
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
if (mentionDelegate != null) {
mentionDelegate.draw(canvas, (Spanned) getText(), getLayout());
}
return spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
} finally {
canvas.restoreToCount(checkpoint);
}

View file

@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.components.emoji
import android.content.Context
import android.graphics.Canvas
import android.graphics.Path
import android.graphics.Region
import android.text.Spanned
import android.text.TextUtils
import android.util.AttributeSet
@ -29,23 +27,16 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
}
override fun onDraw(canvas: Canvas) {
var textClipPath: Path? = null
if (text is Spanned && layout != null) {
val checkpoint = canvas.save()
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
try {
textClipPath = spoilerRendererDelegate.draw(canvas, (text as Spanned), layout)
spoilerRendererDelegate.draw(canvas, (text as Spanned), layout)
} finally {
canvas.restoreToCount(checkpoint)
}
}
if (textClipPath != null) {
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
canvas.clipPath(textClipPath, Region.Op.DIFFERENCE)
canvas.translate(-totalPaddingLeft.toFloat(), -totalPaddingTop.toFloat())
}
super.onDraw(canvas)
}

View file

@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.components.spoiler
import android.graphics.Color
import android.text.Annotation
import android.text.Selection
import android.text.Spannable
import android.text.Spanned
import android.text.TextPaint
import android.text.style.ClickableSpan
import android.text.style.MetricAffectingSpan
import android.view.View
import android.widget.TextView
@ -55,11 +56,11 @@ object SpoilerAnnotation {
revealedSpoilers.clear()
}
class SpoilerClickableSpan(private val spoiler: Annotation) : ClickableSpan() {
class SpoilerClickableSpan(private val spoiler: Annotation) : MetricAffectingSpan() {
val spoilerRevealed
get() = revealedSpoilers.contains(spoiler.value)
override fun onClick(widget: View) {
fun onClick(widget: View) {
revealedSpoilers.add(spoiler.value)
if (widget is TextView) {
@ -70,6 +71,12 @@ object SpoilerAnnotation {
}
}
override fun updateDrawState(ds: TextPaint) = Unit
override fun updateDrawState(ds: TextPaint) {
if (!spoilerRevealed) {
ds.color = Color.TRANSPARENT
}
}
override fun updateMeasureState(textPaint: TextPaint) = Unit
}
}

View file

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.spoiler
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.text.Layout
import androidx.annotation.ColorInt
import androidx.annotation.Px
@ -28,8 +27,7 @@ class SpoilerRenderer(
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int,
textClipPath: Path
endOffset: Int
) {
if (startLine == endLine) {
val lineTop = lineTopCache.get(startLine, layout) { getLineTop(layout, startLine) }
@ -40,7 +38,6 @@ class SpoilerRenderer(
if (renderForComposing) {
canvas.drawComposeBackground(left, lineTop, right, lineBottom)
} else {
textClipPath.addRect(left, lineTop, right, lineBottom)
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
spoilerDrawable.draw(canvas)
}
@ -53,7 +50,7 @@ class SpoilerRenderer(
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) }
drawPartialLine(canvas, startOffset, lineTop, lineEndOffset.toInt(), lineBottom, textClipPath)
drawPartialLine(canvas, startOffset, lineTop, lineEndOffset.toInt(), lineBottom)
for (line in startLine + 1 until endLine) {
val left: Int = layout.getLineLeft(line).toInt()
@ -65,7 +62,6 @@ class SpoilerRenderer(
if (renderForComposing) {
canvas.drawComposeBackground(left, lineTop, right, lineBottom)
} else {
textClipPath.addRect(left, lineTop, right, lineBottom)
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
spoilerDrawable.draw(canvas)
}
@ -74,20 +70,18 @@ class SpoilerRenderer(
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) }
drawPartialLine(canvas, lineStartOffset.toInt(), lineTop, endOffset, lineBottom, textClipPath)
drawPartialLine(canvas, lineStartOffset.toInt(), lineTop, endOffset, lineBottom)
}
private fun drawPartialLine(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int, textClipPath: Path) {
private fun drawPartialLine(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
if (renderForComposing) {
canvas.drawComposeBackground(start, top, end, bottom)
return
}
if (start > end) {
textClipPath.addRect(end, top, start, bottom)
spoilerDrawable.setBounds(end, top, start, bottom)
} else {
textClipPath.addRect(start, top, end, bottom)
spoilerDrawable.setBounds(start, top, end, bottom)
}
spoilerDrawable.draw(canvas)
@ -116,8 +110,4 @@ class SpoilerRenderer(
paint
)
}
private fun Path.addRect(left: Int, top: Int, end: Int, bottom: Int) {
addRect(left.toFloat() - padding, top.toFloat() - padding, end.toFloat() + padding, bottom.toFloat() + padding, Path.Direction.CCW)
}
}

View file

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.spoiler
import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.Path
import android.text.Annotation
import android.text.Layout
import android.text.Spanned
@ -39,8 +38,6 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
repeatMode = ValueAnimator.REVERSE
}
private val textClipPath: Path = Path()
init {
textColor = view.textColors.defaultColor
spoilerDrawable = SpoilerDrawable(textColor)
@ -65,11 +62,10 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
}
}
fun draw(canvas: Canvas, text: Spanned, layout: Layout): Path {
fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
var hasSpoilersToRender = false
val annotations: Map<Annotation, SpoilerClickableSpan?> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAndClickAnnotations(text) }
textClipPath.reset()
for ((annotation, clickSpan) in annotations.entries) {
if (clickSpan?.spoilerRevealed == true) {
continue
@ -92,7 +88,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
)
}
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset, textClipPath)
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset)
hasSpoilersToRender = true
}
@ -104,8 +100,6 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
} else {
stopAnimating()
}
return textClipPath
}
private fun stopAnimating() {

View file

@ -1512,6 +1512,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
int end = messageBody.getSpanEnd(urlSpan);
URLSpan span = new InterceptableLongClickCopyLinkSpan(urlSpan.getURL(), urlClickHandler);
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
messageBody.removeSpan(urlSpan);
}
}
}

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Color;
import android.text.TextPaint;
import android.text.style.URLSpan;
import android.view.View;
@ -41,6 +42,10 @@ public class LongClickCopySpan extends URLSpan {
@Override
public void updateDrawState(@NonNull TextPaint ds) {
if (ds.getColor() == Color.TRANSPARENT) {
return;
}
super.updateDrawState(ds);
if (textColor != null) {
ds.setColor(textColor);

View file

@ -22,8 +22,8 @@ class ConversationItemTest_linkifyUrlLinks(private val input: String, private va
ConversationItem.linkifyUrlLinks(spannableStringBuilder, true, UrlHandler)
val spans = spannableStringBuilder.getSpans(0, expectedUrl.length, URLSpan::class.java)
assertEquals(2, spans.size)
assertEquals(expectedUrl, spans.get(0).url)
assertEquals(1, spans.size)
assertEquals(expectedUrl, spans[0].url)
}
private object UrlHandler : UrlClickHandler {