mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Complete text formatting.
This commit is contained in:
committed by
Greyson Parrelli
parent
534c5c3c64
commit
a64bffd83a
@@ -302,9 +302,7 @@ public class ComposeText extends EmojiEditText {
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
if (FeatureFlags.textFormattingSpoilerSend()) {
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
|
||||
}
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
|
||||
|
||||
addTextChangedListener(new ComposeTextStyleWatcher());
|
||||
|
||||
@@ -323,10 +321,7 @@ public class ComposeText extends EmojiEditText {
|
||||
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
|
||||
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
|
||||
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
|
||||
|
||||
if (FeatureFlags.textFormattingSpoilerSend()) {
|
||||
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
|
||||
}
|
||||
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -31,7 +30,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.quotes.QuoteViewColorTheme;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
@@ -166,7 +164,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
setMessageType(messageType);
|
||||
|
||||
bodyView.enableSpoilerFiltering();
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ 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,9 +72,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private boolean forceJumboEmoji;
|
||||
private boolean isInOnDraw;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private SpoilerFilteringSpannableFactory spoilerFilteringSpannableFactory;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@@ -113,11 +114,6 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
setText(getText());
|
||||
}
|
||||
|
||||
public void enableSpoilerFiltering() {
|
||||
spoilerFilteringSpannableFactory = new SpoilerFilteringSpannableFactory(() -> isInOnDraw);
|
||||
setSpannableFactory(spoilerFilteringSpannableFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
isInOnDraw = true;
|
||||
@@ -126,7 +122,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
boolean hasLayout = getLayout() != null;
|
||||
|
||||
if (hasSpannedText && hasLayout) {
|
||||
drawSpecialRenderers(canvas, mentionRendererDelegate, spoilerRendererDelegate);
|
||||
Path textClipPath = drawSpecialRenderers(canvas, mentionRendererDelegate, spoilerRendererDelegate);
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
canvas.clipPath(textClipPath, Region.Op.DIFFERENCE);
|
||||
canvas.translate(-getTotalPaddingLeft(), -getTotalPaddingTop());
|
||||
}
|
||||
|
||||
super.onDraw(canvas);
|
||||
@@ -138,14 +137,14 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
isInOnDraw = false;
|
||||
}
|
||||
|
||||
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @NonNull SpoilerRendererDelegate spoilerDelegate) {
|
||||
private Path 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());
|
||||
}
|
||||
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
return spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint);
|
||||
}
|
||||
@@ -187,9 +186,6 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
textToSet = new SpannableStringBuilder(EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji));
|
||||
}
|
||||
|
||||
if (spoilerFilteringSpannableFactory != null) {
|
||||
textToSet = spoilerFilteringSpannableFactory.wrap(textToSet);
|
||||
}
|
||||
super.setText(textToSet, BufferType.SPANNABLE);
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
@@ -333,11 +329,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
newTextToSet = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
|
||||
}
|
||||
|
||||
if (spoilerFilteringSpannableFactory != null) {
|
||||
spoilerFilteringSpannableFactory.wrap(newTextToSet);
|
||||
}
|
||||
|
||||
super.setText(newContent, BufferType.SPANNABLE);
|
||||
super.setText(newTextToSet, BufferType.SPANNABLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ package org.thoughtcrime.securesms.components.emoji
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.text.Spannable
|
||||
import android.graphics.Path
|
||||
import android.graphics.Region
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
@@ -21,8 +22,6 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
private var bufferType: BufferType? = null
|
||||
private val sizeChangeDebouncer: ThrottledDebouncer = ThrottledDebouncer(200)
|
||||
private val spoilerRendererDelegate: SpoilerRendererDelegate
|
||||
private var spoilerFilteringSpannableFactory: SpoilerFilteringSpannableFactory? = null
|
||||
private var isInOnDraw: Boolean = false
|
||||
|
||||
init {
|
||||
isEmojiCompatEnabled = isInEditMode || SignalStore.settings().isPreferSystemEmoji
|
||||
@@ -30,20 +29,24 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
isInOnDraw = true
|
||||
|
||||
var textClipPath: Path? = null
|
||||
if (text is Spanned && layout != null) {
|
||||
val checkpoint = canvas.save()
|
||||
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
|
||||
try {
|
||||
spoilerRendererDelegate.draw(canvas, (text as Spanned), layout)
|
||||
textClipPath = spoilerRendererDelegate.draw(canvas, (text as Spanned), layout)
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint)
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas)
|
||||
|
||||
isInOnDraw = false
|
||||
if (textClipPath != null) {
|
||||
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
|
||||
canvas.clipPath(textClipPath, Region.Op.DIFFERENCE)
|
||||
canvas.translate(-totalPaddingLeft.toFloat(), -totalPaddingTop.toFloat())
|
||||
}
|
||||
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||
@@ -69,10 +72,6 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
TextUtils.ellipsize(newText, paint, (adjustedWidth * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
|
||||
}
|
||||
|
||||
if (newContent is Spannable && spoilerFilteringSpannableFactory != null) {
|
||||
newContent = spoilerFilteringSpannableFactory!!.wrap(newContent)
|
||||
}
|
||||
|
||||
bufferType = BufferType.SPANNABLE
|
||||
super.setText(newContent, type)
|
||||
}
|
||||
@@ -86,9 +85,4 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableSpoilerFiltering() {
|
||||
spoilerFilteringSpannableFactory = SpoilerFilteringSpannableFactory { isInOnDraw }
|
||||
setSpannableFactory(spoilerFilteringSpannableFactory!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji
|
||||
|
||||
import android.text.Spannable
|
||||
import org.thoughtcrime.securesms.util.SpoilerFilteringSpannable
|
||||
import org.thoughtcrime.securesms.util.SpoilerFilteringSpannable.InOnDrawProvider
|
||||
|
||||
/**
|
||||
* Spannable factory used to help ensure spans are copied/maintained properly through the
|
||||
* Android text handling system.
|
||||
*
|
||||
* @param inOnDraw Used by [SpoilerFilteringSpannable] to remove spans when being called from onDraw
|
||||
*/
|
||||
class SpoilerFilteringSpannableFactory(private val inOnDraw: InOnDrawProvider) : Spannable.Factory() {
|
||||
override fun newSpannable(source: CharSequence): Spannable {
|
||||
return wrap(super.newSpannable(source))
|
||||
}
|
||||
|
||||
fun wrap(source: Spannable): SpoilerFilteringSpannable {
|
||||
return SpoilerFilteringSpannable(source, inOnDraw)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
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.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.ClickableSpan
|
||||
@@ -63,21 +61,15 @@ object SpoilerAnnotation {
|
||||
|
||||
override fun onClick(widget: View) {
|
||||
revealedSpoilers.add(spoiler.value)
|
||||
if (widget is TextView && Selection.getSelectionStart(widget.text) != -1) {
|
||||
val text: Spannable = if (widget.text is Spannable) {
|
||||
widget.text as Spannable
|
||||
} else {
|
||||
SpannableString(widget.text)
|
||||
|
||||
if (widget is TextView) {
|
||||
val text = widget.text
|
||||
if (text is Spannable) {
|
||||
Selection.removeSelection(text)
|
||||
}
|
||||
Selection.removeSelection(text)
|
||||
widget.text = text
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
if (!spoilerRevealed) {
|
||||
ds.color = Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
override fun updateDrawState(ds: TextPaint) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,123 @@
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.LayoutUtil
|
||||
|
||||
/**
|
||||
* Handles drawing the spoiler sparkles for a TextView.
|
||||
*/
|
||||
abstract class SpoilerRenderer {
|
||||
class SpoilerRenderer(
|
||||
private val spoilerDrawable: SpoilerDrawable,
|
||||
private val renderForComposing: Boolean,
|
||||
@Px private val padding: Int,
|
||||
@ColorInt composeBackgroundColor: Int
|
||||
) {
|
||||
|
||||
abstract fun draw(
|
||||
private val lineTopCache = HashMap<Int, Int>()
|
||||
private val lineBottomCache = HashMap<Int, Int>()
|
||||
private val paint = Paint().apply { color = composeBackgroundColor }
|
||||
|
||||
fun draw(
|
||||
canvas: Canvas,
|
||||
layout: Layout,
|
||||
startLine: Int,
|
||||
endLine: Int,
|
||||
startOffset: Int,
|
||||
endOffset: Int
|
||||
)
|
||||
|
||||
protected fun getLineTop(layout: Layout, line: Int): Int {
|
||||
return LayoutUtil.getLineTopWithoutPadding(layout, line)
|
||||
}
|
||||
|
||||
protected fun getLineBottom(layout: Layout, line: Int): Int {
|
||||
return LayoutUtil.getLineBottomWithoutPadding(layout, line)
|
||||
}
|
||||
|
||||
protected inline fun MutableMap<Int, Int>.get(line: Int, layout: Layout, default: () -> Int): Int {
|
||||
return getOrPut(line * 31 + layout.hashCode() * 31, default)
|
||||
}
|
||||
|
||||
class SingleLineSpoilerRenderer(private val spoilerDrawable: SpoilerDrawable) : SpoilerRenderer() {
|
||||
private val lineTopCache = HashMap<Int, Int>()
|
||||
private val lineBottomCache = HashMap<Int, Int>()
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas,
|
||||
layout: Layout,
|
||||
startLine: Int,
|
||||
endLine: Int,
|
||||
startOffset: Int,
|
||||
endOffset: Int
|
||||
) {
|
||||
endOffset: Int,
|
||||
textClipPath: Path
|
||||
) {
|
||||
if (startLine == endLine) {
|
||||
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)
|
||||
|
||||
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
|
||||
spoilerDrawable.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
class MultiLineSpoilerRenderer(private val spoilerDrawable: SpoilerDrawable) : SpoilerRenderer() {
|
||||
private val lineTopCache = HashMap<Int, Int>()
|
||||
private val lineBottomCache = HashMap<Int, Int>()
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas,
|
||||
layout: Layout,
|
||||
startLine: Int,
|
||||
endLine: Int,
|
||||
startOffset: Int,
|
||||
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)
|
||||
|
||||
for (line in startLine + 1 until endLine) {
|
||||
val left: Int = layout.getLineLeft(line).toInt()
|
||||
val right: Int = layout.getLineRight(line).toInt()
|
||||
|
||||
lineTop = getLineTop(layout, line)
|
||||
lineBottom = getLineBottom(layout, line)
|
||||
|
||||
if (renderForComposing) {
|
||||
canvas.drawComposeBackground(left, lineTop, right, lineBottom)
|
||||
} else {
|
||||
textClipPath.addRect(left, lineTop, right, lineBottom)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
|
||||
if (start > end) {
|
||||
spoilerDrawable.setBounds(end, top, start, bottom)
|
||||
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) }
|
||||
drawPartialLine(canvas, startOffset, lineTop, lineEndOffset.toInt(), lineBottom, textClipPath)
|
||||
|
||||
for (line in startLine + 1 until endLine) {
|
||||
val left: Int = layout.getLineLeft(line).toInt()
|
||||
val right: Int = layout.getLineRight(line).toInt()
|
||||
|
||||
lineTop = getLineTop(layout, line)
|
||||
lineBottom = getLineBottom(layout, line)
|
||||
|
||||
if (renderForComposing) {
|
||||
canvas.drawComposeBackground(left, lineTop, right, lineBottom)
|
||||
} else {
|
||||
spoilerDrawable.setBounds(start, top, end, bottom)
|
||||
textClipPath.addRect(left, lineTop, right, lineBottom)
|
||||
spoilerDrawable.setBounds(left, lineTop, right, lineBottom)
|
||||
spoilerDrawable.draw(canvas)
|
||||
}
|
||||
spoilerDrawable.draw(canvas)
|
||||
}
|
||||
|
||||
private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
|
||||
if (start > end) {
|
||||
spoilerDrawable.setBounds(end, top, start, bottom)
|
||||
} else {
|
||||
spoilerDrawable.setBounds(start, top, end, bottom)
|
||||
}
|
||||
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) }
|
||||
drawPartialLine(canvas, lineStartOffset.toInt(), lineTop, endOffset, lineBottom, textClipPath)
|
||||
}
|
||||
|
||||
private fun drawPartialLine(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int, textClipPath: Path) {
|
||||
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)
|
||||
}
|
||||
|
||||
private fun getLineTop(layout: Layout, line: Int): Int {
|
||||
return LayoutUtil.getLineTopWithoutPadding(layout, line)
|
||||
}
|
||||
|
||||
private fun getLineBottom(layout: Layout, line: Int): Int {
|
||||
return LayoutUtil.getLineBottomWithoutPadding(layout, line)
|
||||
}
|
||||
|
||||
private inline fun MutableMap<Int, Int>.get(line: Int, layout: Layout, default: () -> Int): Int {
|
||||
return getOrPut(line * 31 + layout.hashCode() * 31, default)
|
||||
}
|
||||
|
||||
private fun Canvas.drawComposeBackground(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
drawRoundRect(
|
||||
start.toFloat() - padding,
|
||||
top.toFloat() - padding,
|
||||
end.toFloat() + padding,
|
||||
bottom.toFloat(),
|
||||
padding.toFloat(),
|
||||
padding.toFloat(),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -9,18 +10,17 @@ import android.view.View
|
||||
import android.view.View.OnAttachStateChangeListener
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation.SpoilerClickableSpan
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.MultiLineSpoilerRenderer
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.SingleLineSpoilerRenderer
|
||||
|
||||
/**
|
||||
* Performs initial calculation on how to render spoilers and then delegates to the single line or
|
||||
* multi-line version of actually drawing the spoiler sparkles.
|
||||
* Performs initial calculation on how to render spoilers and then delegates to actually drawing the spoiler sparkles.
|
||||
*/
|
||||
class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextView, private val renderForComposing: Boolean = false) {
|
||||
|
||||
private val single: SpoilerRenderer
|
||||
private val multi: SpoilerRenderer
|
||||
private val renderer: SpoilerRenderer
|
||||
private val spoilerDrawable: SpoilerDrawable
|
||||
private var animatorRunning = false
|
||||
private var textColor: Int
|
||||
@@ -39,11 +39,17 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
}
|
||||
|
||||
private val textClipPath: Path = Path()
|
||||
|
||||
init {
|
||||
textColor = view.textColors.defaultColor
|
||||
spoilerDrawable = SpoilerDrawable(textColor)
|
||||
single = SingleLineSpoilerRenderer(spoilerDrawable)
|
||||
multi = MultiLineSpoilerRenderer(spoilerDrawable)
|
||||
renderer = SpoilerRenderer(
|
||||
spoilerDrawable = spoilerDrawable,
|
||||
renderForComposing = renderForComposing,
|
||||
padding = 2.dp,
|
||||
composeBackgroundColor = ContextCompat.getColor(view.context, R.color.signal_colorOnSurfaceVariant1)
|
||||
)
|
||||
|
||||
view.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
|
||||
override fun onViewDetachedFromWindow(v: View) = stopAnimating()
|
||||
@@ -59,10 +65,11 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
}
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
|
||||
fun draw(canvas: Canvas, text: Spanned, layout: Layout): Path {
|
||||
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
|
||||
@@ -85,9 +92,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
)
|
||||
}
|
||||
|
||||
val renderer: SpoilerRenderer = if (measurements.startLine == measurements.endLine) single else multi
|
||||
|
||||
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset)
|
||||
renderer.draw(canvas, layout, measurements.startLine, measurements.endLine, measurements.startOffset, measurements.endOffset, textClipPath)
|
||||
hasSpoilersToRender = true
|
||||
}
|
||||
|
||||
@@ -99,6 +104,8 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
} else {
|
||||
stopAnimating()
|
||||
}
|
||||
|
||||
return textClipPath
|
||||
}
|
||||
|
||||
private fun stopAnimating() {
|
||||
|
||||
Reference in New Issue
Block a user