mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 04:06:14 +00:00
Fix rendering of links and mentions covered by spoilers.
This commit is contained in:
@@ -363,7 +363,7 @@ public class ComposeText extends EmojiEditText {
|
||||
} else if (item.getItemId() == R.id.edittext_monospace) {
|
||||
style = MessageStyler.monoStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_spoiler) {
|
||||
style = MessageStyler.spoilerStyle(start, charSequence.length(), text);
|
||||
style = MessageStyler.spoilerStyle(MessageStyler.COMPOSE_ID, start, charSequence.length(), text);
|
||||
}
|
||||
|
||||
if (style != null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -27,8 +28,10 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
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;
|
||||
@@ -82,7 +85,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private ViewGroup mainView;
|
||||
private ViewGroup footerView;
|
||||
private TextView authorView;
|
||||
private TextView bodyView;
|
||||
private EmojiTextView bodyView;
|
||||
private View quoteBarView;
|
||||
private ShapeableImageView thumbnailView;
|
||||
private View attachmentVideoOverlayView;
|
||||
@@ -163,6 +166,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
setMessageType(messageType);
|
||||
|
||||
bodyView.enableSpoilerFiltering();
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextDirectionHeuristic;
|
||||
@@ -26,7 +27,6 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRendererDelegate;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.SpoilerFilteringSpannable;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -67,9 +68,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private TextDirectionHeuristic textDirection;
|
||||
private boolean isJumbomoji;
|
||||
private boolean forceJumboEmoji;
|
||||
private boolean isInOnDraw;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private SpoilerFilteringSpannableFactory spoilerFilteringSpannableFactory;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@@ -105,8 +108,14 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
setEmojiCompatEnabled(useSystemEmoji());
|
||||
}
|
||||
|
||||
public void enableSpoilerFiltering() {
|
||||
spoilerFilteringSpannableFactory = new SpoilerFilteringSpannableFactory();
|
||||
setSpannableFactory(spoilerFilteringSpannableFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
isInOnDraw = true;
|
||||
if (getText() instanceof Spanned && getLayout() != null) {
|
||||
int checkpoint = canvas.save();
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
@@ -120,6 +129,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
isInOnDraw = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,13 +161,18 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
Spannable textToSet;
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.ofNullable(text).orElse("")), BufferType.SPANNABLE);
|
||||
textToSet = new SpannableStringBuilder(Optional.ofNullable(text).orElse(""));
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
|
||||
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||
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)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
@@ -410,4 +425,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
mentionRendererDelegate.setTint(mentionBackgroundTint);
|
||||
}
|
||||
}
|
||||
|
||||
private class SpoilerFilteringSpannableFactory extends Spannable.Factory {
|
||||
@Override
|
||||
public @NonNull Spannable newSpannable(CharSequence source) {
|
||||
return wrap(super.newSpannable(source));
|
||||
}
|
||||
|
||||
@NonNull SpoilerFilteringSpannable wrap(Spannable source) {
|
||||
return new SpoilerFilteringSpannable(source, () -> isInOnDraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,27 +22,23 @@ object SpoilerAnnotation {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSpoilerAnnotation(annotation: Annotation): Boolean {
|
||||
return SPOILER_ANNOTATION == annotation.key
|
||||
fun isSpoilerAnnotation(annotation: Any): Boolean {
|
||||
return SPOILER_ANNOTATION == (annotation as? Annotation)?.key
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSpoilerAnnotations(spanned: Spanned): List<Annotation> {
|
||||
val spoilerAnnotations: Map<Pair<Int, Int>, Annotation> = spanned.getSpans(0, spanned.length, Annotation::class.java)
|
||||
fun getSpoilerAndClickAnnotations(spanned: Spanned, start: Int = 0, end: Int = spanned.length): Map<Annotation, SpoilerClickableSpan?> {
|
||||
val spoilerAnnotations: Map<Pair<Int, Int>, Annotation> = spanned.getSpans(start, end, Annotation::class.java)
|
||||
.filter { isSpoilerAnnotation(it) }
|
||||
.associateBy { (spanned.getSpanStart(it) to spanned.getSpanEnd(it)) }
|
||||
|
||||
val spoilerClickSpans: Map<Pair<Int, Int>, SpoilerClickableSpan> = spanned.getSpans(0, spanned.length, SpoilerClickableSpan::class.java)
|
||||
val spoilerClickSpans: Map<Pair<Int, Int>, SpoilerClickableSpan> = spanned.getSpans(start, end, SpoilerClickableSpan::class.java)
|
||||
.associateBy { (spanned.getSpanStart(it) to spanned.getSpanEnd(it)) }
|
||||
|
||||
return spoilerAnnotations.mapNotNull { (position, annotation) ->
|
||||
if (spoilerClickSpans[position]?.spoilerRevealed != true && !revealedSpoilers.contains(annotation.value)) {
|
||||
annotation
|
||||
} else {
|
||||
revealedSpoilers.add(annotation.value)
|
||||
null
|
||||
return spoilerAnnotations
|
||||
.map { (position, annotation) ->
|
||||
annotation to spoilerClickSpans[position]
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -57,18 +53,12 @@ object SpoilerAnnotation {
|
||||
revealedSpoilers.clear()
|
||||
}
|
||||
|
||||
class SpoilerClickableSpan(spoiler: Annotation) : ClickableSpan() {
|
||||
private val spoiler: Annotation
|
||||
var spoilerRevealed = false
|
||||
private set
|
||||
|
||||
init {
|
||||
this.spoiler = spoiler
|
||||
spoilerRevealed = revealedSpoilers.contains(spoiler.value)
|
||||
}
|
||||
class SpoilerClickableSpan(private val spoiler: Annotation) : ClickableSpan() {
|
||||
val spoilerRevealed
|
||||
get() = revealedSpoilers.contains(spoiler.value)
|
||||
|
||||
override fun onClick(widget: View) {
|
||||
spoilerRevealed = true
|
||||
revealedSpoilers.add(spoiler.value)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.text.Layout
|
||||
import android.text.Spanned
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation.SpoilerClickableSpan
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.MultiLineSpoilerRenderer
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.SingleLineSpoilerRenderer
|
||||
|
||||
@@ -26,7 +27,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
private var spoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
|
||||
private var nextSpoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
|
||||
|
||||
private val cachedAnnotations = HashMap<Int, List<Annotation>>()
|
||||
private val cachedAnnotations = HashMap<Int, Map<Annotation, SpoilerClickableSpan?>>()
|
||||
private val cachedMeasurements = HashMap<Int, SpanMeasurements>()
|
||||
|
||||
private val animator = ValueAnimator.ofInt(0, 100).apply {
|
||||
@@ -56,10 +57,14 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
|
||||
fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
|
||||
var hasSpoilersToRender = false
|
||||
val annotations: List<Annotation> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAnnotations(text) }
|
||||
val annotations: Map<Annotation, SpoilerClickableSpan?> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAndClickAnnotations(text) }
|
||||
|
||||
nextSpoilerDrawablePool.clear()
|
||||
for (annotation in annotations) {
|
||||
for ((annotation, clickSpan) in annotations.entries) {
|
||||
if (clickSpan?.spoilerRevealed == true) {
|
||||
continue
|
||||
}
|
||||
|
||||
val spanStart: Int = text.getSpanStart(annotation)
|
||||
val spanEnd: Int = text.getSpanEnd(annotation)
|
||||
if (spanStart >= spanEnd) {
|
||||
|
||||
@@ -342,6 +342,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
bodyText.setOnLongClickListener(passthroughClickListener);
|
||||
bodyText.setOnClickListener(passthroughClickListener);
|
||||
bodyText.enableSpoilerFiltering();
|
||||
footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class ConversationMessage {
|
||||
: BodyRangeUtil.adjustBodyRanges(messageRecord.getMessageRanges(), mentionsUpdate.getBodyAdjustments());
|
||||
|
||||
styledAndMentionBody = SpannableString.valueOf(mentionsUpdate != null ? mentionsUpdate.getBody() : body);
|
||||
styleResult = MessageStyler.style(bodyRanges, styledAndMentionBody);
|
||||
styleResult = MessageStyler.style(messageRecord.getId(), bodyRanges, styledAndMentionBody);
|
||||
}
|
||||
|
||||
return new ConversationMessage(messageRecord,
|
||||
|
||||
@@ -19,6 +19,9 @@ object MessageStyler {
|
||||
|
||||
const val MONOSPACE = "monospace"
|
||||
const val SPAN_FLAGS = Spanned.SPAN_EXCLUSIVE_INCLUSIVE
|
||||
const val DRAFT_ID = "DRAFT"
|
||||
const val COMPOSE_ID = "COMPOSE"
|
||||
const val QUOTE_ID = "QUOTE"
|
||||
|
||||
@JvmStatic
|
||||
fun boldStyle(): CharacterStyle {
|
||||
@@ -41,13 +44,13 @@ object MessageStyler {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun spoilerStyle(start: Int, length: Int, body: Spannable? = null): Annotation {
|
||||
return SpoilerAnnotation.spoilerAnnotation(arrayOf(start, length, body?.toString()).contentHashCode())
|
||||
fun spoilerStyle(id: Any, start: Int, length: Int, body: Spannable? = null): Annotation {
|
||||
return SpoilerAnnotation.spoilerAnnotation(arrayOf(id, start, length, body?.toString()).contentHashCode())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun style(messageRanges: BodyRangeList?, span: Spannable, hideSpoilerText: Boolean = true): Result {
|
||||
fun style(id: Any, messageRanges: BodyRangeList?, span: Spannable, hideSpoilerText: Boolean = true): Result {
|
||||
if (messageRanges == null) {
|
||||
return Result.none()
|
||||
}
|
||||
@@ -67,7 +70,7 @@ object MessageStyler {
|
||||
BodyRangeList.BodyRange.Style.STRIKETHROUGH -> strikethroughStyle()
|
||||
BodyRangeList.BodyRange.Style.MONOSPACE -> monoStyle()
|
||||
BodyRangeList.BodyRange.Style.SPOILER -> {
|
||||
val spoiler = spoilerStyle(range.start, range.length, span)
|
||||
val spoiler = spoilerStyle(id, range.start, range.length, span)
|
||||
if (hideSpoilerText) {
|
||||
span.setSpan(SpoilerAnnotation.SpoilerClickableSpan(spoiler), range.start, range.start + range.length, SPAN_FLAGS)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class DraftRepository(
|
||||
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
MessageStyler.style(messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
|
||||
MessageStyler.style(id = MessageStyler.DRAFT_ID, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
|
||||
}
|
||||
|
||||
DatabaseDraft(drafts, updatedText)
|
||||
|
||||
@@ -652,7 +652,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
|
||||
} else {
|
||||
SpannableStringBuilder sourceBody = new SpannableStringBuilder(thread.getBody());
|
||||
MessageStyler.style(thread.getBodyRanges(), sourceBody);
|
||||
MessageStyler.style(thread.getDate(), thread.getBodyRanges(), sourceBody);
|
||||
|
||||
CharSequence body = StringUtil.replace(sourceBody, '\n', " ");
|
||||
LiveData<SpannableString> finalBody = Transformations.map(createFinalBodyWithMediaIcon(context, body, thread, glideRequests, thumbSize, thumbTarget), updatedBody -> {
|
||||
|
||||
@@ -67,7 +67,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment.DisplayOrderComparator
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler.style
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.database.EarlyReceiptCache.Receipt
|
||||
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
|
||||
@@ -5109,7 +5109,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val updated: UpdatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions)
|
||||
val styledText = SpannableString(updated.body)
|
||||
|
||||
style(bodyRanges.adjustBodyRanges(updated.bodyAdjustments), styledText)
|
||||
MessageStyler.style(id = quoteId, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = styledText)
|
||||
|
||||
quoteText = styledText
|
||||
quoteMentions = updated.mentions
|
||||
|
||||
@@ -256,11 +256,11 @@ public class SearchRepository {
|
||||
|
||||
if (ranges != null) {
|
||||
updatedBody = SpannableString.valueOf(updatedBody);
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
MessageStyler.style(result.getMessageId(), BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
|
||||
updatedSnippet = SpannableString.valueOf(updatedSnippet);
|
||||
//noinspection ConstantConditions
|
||||
updateSnippetWithStyles(updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
|
||||
updateSnippetWithStyles(result.getMessageId(), updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
|
||||
}
|
||||
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
@@ -302,7 +302,7 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSnippetWithStyles(@NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
|
||||
private void updateSnippetWithStyles(long id, @NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
|
||||
CharSequence cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
|
||||
@@ -326,7 +326,7 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
MessageStyler.style(builder.build(), bodySnippet);
|
||||
MessageStyler.style(id, builder.build(), bodySnippet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ public class SearchRepository {
|
||||
SpannableString body = new SpannableString(record.getBody());
|
||||
|
||||
if (bodyRanges != null) {
|
||||
MessageStyler.style(bodyRanges, body);
|
||||
MessageStyler.style(record.getId(), bodyRanges, body);
|
||||
}
|
||||
|
||||
CharSequence updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions).getBody();
|
||||
|
||||
@@ -134,7 +134,7 @@ class StoryTextPostView @JvmOverloads constructor(
|
||||
} else {
|
||||
val body = SpannableString(storyTextPost.body)
|
||||
if (font == TextFont.REGULAR && bodyRanges != null) {
|
||||
MessageStyler.style(bodyRanges, body)
|
||||
MessageStyler.style(System.currentTimeMillis(), bodyRanges, body)
|
||||
}
|
||||
setText(body, false)
|
||||
}
|
||||
|
||||
@@ -815,7 +815,7 @@ class StoryViewerPageFragment :
|
||||
val displayBodySpan = SpannableString(storyPost.content.attachment.caption ?: "")
|
||||
val ranges: BodyRangeList? = storyPost.conversationMessage.messageRecord.messageRanges
|
||||
if (ranges != null && displayBodySpan.isNotEmpty()) {
|
||||
MessageStyler.style(ranges, displayBodySpan)
|
||||
MessageStyler.style(storyPost.id, ranges, displayBodySpan)
|
||||
}
|
||||
|
||||
displayBodySpan
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.text.Annotation
|
||||
import android.text.Spannable
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
|
||||
|
||||
/**
|
||||
* Filters the results of [getSpans] to exclude spans covered by an unrevealed spoiler when drawing or
|
||||
* processing clicks. Since [getSpans] can also be called when making copies of spannables, we do not filter
|
||||
* the call unless we know we are drawing or getting click spannables.
|
||||
*/
|
||||
class SpoilerFilteringSpannable(private val spannable: Spannable, private val inOnDrawProvider: InOnDrawProvider) : Spannable by spannable {
|
||||
|
||||
override fun <T : Any> getSpans(start: Int, end: Int, type: Class<T>): Array<T> {
|
||||
val spans: Array<T> = spannable.getSpans(start, end, type)
|
||||
|
||||
if (spans.isEmpty() || !(inOnDrawProvider.isInOnDraw() || type == LongClickCopySpan::class.java)) {
|
||||
return spans
|
||||
}
|
||||
|
||||
if (spannable.getSpans(0, spannable.length, Annotation::class.java).none { SpoilerAnnotation.isSpoilerAnnotation(it) }) {
|
||||
return spans
|
||||
}
|
||||
|
||||
val spansToExclude = HashSet<Any>()
|
||||
val spoilers: Map<Annotation, SpoilerAnnotation.SpoilerClickableSpan?> = SpoilerAnnotation.getSpoilerAndClickAnnotations(spannable, start, end)
|
||||
val allOtherTheSpans: Map<T, Pair<Int, Int>> = spans
|
||||
.filterNot { SpoilerAnnotation.isSpoilerAnnotation(it) || it is SpoilerAnnotation.SpoilerClickableSpan }
|
||||
.associateWith { (spannable.getSpanStart(it) to spannable.getSpanEnd(it)) }
|
||||
|
||||
spoilers.forEach { (spoiler, click) ->
|
||||
if (click?.spoilerRevealed == true) {
|
||||
spansToExclude += spoiler
|
||||
spansToExclude += click
|
||||
} else {
|
||||
val spoilerStart = spannable.getSpanStart(spoiler)
|
||||
val spoilerEnd = spannable.getSpanEnd(spoiler)
|
||||
|
||||
for ((span, position) in allOtherTheSpans) {
|
||||
if (position.first in spoilerStart..spoilerEnd) {
|
||||
spansToExclude += span
|
||||
} else if (position.second in spoilerStart..spoilerEnd) {
|
||||
spansToExclude += span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spans.filter(spansToExclude)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin does not handle generic JVM arrays well so instead of using all the nice collection functions
|
||||
* we do a move desired objects down and overwrite undesired objects and then copy the array to trim
|
||||
* it to the correct length. For our use case, it's okay to modify the original array.
|
||||
*/
|
||||
private fun <T : Any> Array<T>.filter(set: Set<Any>): Array<T> {
|
||||
var index = 0
|
||||
for (i in this.indices) {
|
||||
this[index] = this[i]
|
||||
if (!set.contains(this[index])) {
|
||||
index++
|
||||
}
|
||||
}
|
||||
return copyOfRange(0, index)
|
||||
}
|
||||
|
||||
override fun toString(): String = spannable.toString()
|
||||
override fun hashCode(): Int = spannable.hashCode()
|
||||
override fun equals(other: Any?): Boolean = spannable == other
|
||||
|
||||
interface InOnDrawProvider {
|
||||
fun isInOnDraw(): Boolean
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user