SpannableString looses spans when used multiple times
https://developer.android.com/reference/android/text/TextUtils#concat(java.lang.CharSequence...):
If there are paragraph spans in the source CharSequences that satisfy paragraph boundary requirements in the sources but would no longer satisfy them in the concatenated CharSequence, they may get extended in the resulting CharSequence or not retained.
While this isn't super clear (at least to me) I infer that paragraph boundaries are extended if they should be extended. They should be extended if they are INCLUSIVE boundaries meaning if you insert at the beginning you should use Spannable.SPAN_INCLUSIVE_EXCLUSIVE
or Spannable.SPAN_INCLUSIVE_INCLUSIVE
, if you append at the end, you should use Spannable.SPAN_EXCLUSIVE_INCLUSIVE
or Spannable.SPAN_INCLUSIVE_INCLUSIVE
.
So use Spannable.SPAN_INCLUSIVE_INCLUSIVE
instead of Spannable.SPAN_INCLUSIVE_EXCLUSIVE
(I tested it and it works).
With SPAN_EXCLUSIVE_INCLUSIVE:
With SPAN_INCLUSIVE_INCLUSIVE:
After some digging I found TextUtils is using SpannableStringBuilder.append which is using SpannableStringBuilder.replace (https://developer.android.com/reference/android/text/SpannableStringBuilder#replace(int,%20int,%20java.lang.CharSequence,%20int,%20int)) under the hood and the replace is considered an insert at the end. With SPAN_EXCLUSIVE_INCLUSIVE it would not expand the formatting while it would with SPAN_INCLUSIVE_INCLUSIVE:
If the source contains a span with Spanned#SPAN_PARAGRAPH flag, and it does not satisfy the paragraph boundary constraint, it is not retained.
I also checked the resulting span and printed the start/end of the StyleSpan. The first line with SPAN_EXCLUSIVE_INCLUSIVE the last line with SPAN_INCLUSIVE_INCLUSIVE:
01-14 12:44:11.218 5017 5017 E test : span start/end: 0/5
01-14 12:44:22.575 5079 5079 E test : span start/end: 0/10
If you append text to a Spannable
it doesn't matter whether that text is just plain text or another Spannable
. Spans in the original texts will either expand or not depending on the span's flags (Spannable.SPAN_xyz_INCLUSIVE
vs Spannable.SPAN_xyz_EXCLUSIVE
). That's why the bold formatting will either expand to the whole string with TextUtils.concat("A", s, "B", s, "C")
(using Spannable.SPAN_xyz_INCLUSIVE
) or not expand at all (using Spannable.SPAN_xyz_EXCLUSIVE
).
Your attempt to append an already formatted Spannable
won't work because a span can be used just once.
That's why this:
val s = SpannableStringBuilder("hellohello")
val span = StyleSpan(Typeface.BOLD)
s.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello, while this:
val s = SpannableStringBuilder("hellohello")
val span1 = StyleSpan(Typeface.BOLD)
val span2 = StyleSpan(Typeface.BOLD)
s.setSpan(span1, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span2, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello
This answers your question:
Why aren't both "hello" in bold?
Unfortunately your question isn't very specific on what you're actually trying to achieve. If it's simply to have a Spannable
with AhelloBhelloC, then you now know how. If your goal is to have a generic solution that allows to append arbitrary Spannable
with arbitrary spans and keep those spans (CharacterStyle
and ParagraphStyle
) then you'll have to find a way to clone arbitrary Spanned
texts with arbitrary spans (note if s
is a SpannableStringBuilder
then s.subSequence(0, s.length)
copies spans but doesn't clone them). So here's how I'd do it:
private fun Spannable.clone(): Spannable {
val clone = SpannableStringBuilder(toString())
for (span in getSpans(0, length, Any::class.java)) {
if (span is CharacterStyle || span is ParagraphStyle) {
val st = getSpanStart(span).coerceAtLeast(0)
val en = getSpanEnd(span).coerceAtMost(length)
val fl = getSpanFlags(span)
val clonedSpan = cloneSpan(span)
clone.setSpan(clonedSpan, st, en, fl)
}
}
return clone
}
private fun cloneSpan(span: Any): Any {
return when (span) {
is StyleSpan -> StyleSpan(span.style)
// more clone code to be written...
else -> span
}
}
Then you can do:
val s = SpannableString("hello")
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = TextUtils.concat(s, " not bold ", s.clone())