diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java index 1be73d30d2..c09759a119 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index 65057eb1c6..1994932b14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/SimpleEmojiTextView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/SimpleEmojiTextView.kt index 07a292cf56..2d6f36803a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/SimpleEmojiTextView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/SimpleEmojiTextView.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerAnnotation.kt b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerAnnotation.kt index 22c60760e3..bf7321799b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerAnnotation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerAnnotation.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRenderer.kt index 9251b66b3a..f69593daae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRenderer.kt @@ -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) - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt index d5b2101153..124daa7ad5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt @@ -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 = 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 30148952d8..b2e89864ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -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); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java index 81e6da0600..7513673207 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java @@ -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); diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/ConversationItemTest_linkifyUrlLinks.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/ConversationItemTest_linkifyUrlLinks.kt index f7dd015447..21e8b7ce6d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/ConversationItemTest_linkifyUrlLinks.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/ConversationItemTest_linkifyUrlLinks.kt @@ -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 {