Skip to content

Commit 77d0ee6

Browse files
fix(android): set piority of applied spans (#629)
# Summary Apply priority to android spans that inline spans will be applied after paragraph spans. See: https://stackoverflow.com/questions/46002947/how-to-define-order-setpriority-of-spannable-in-android ## Test Plan Play around with styles on android and check if inline styles override paragraph styles. For example: Inline code inside blockquote. ## Screenshots / Videos https://github.com/user-attachments/assets/9bacb687-ad62-43fe-a0d0-4c135548f1ef ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable)
1 parent 6723ce9 commit 77d0ee6

11 files changed

Lines changed: 75 additions & 33 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.swmansion.enriched.common
2+
3+
import android.text.Spannable
4+
import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan
5+
import com.swmansion.enriched.common.spans.interfaces.EnrichedInlineSpan
6+
7+
// Higher priority spans are processed first, so styles with lower priorities are painted on top of previously applied styles.
8+
// For example, inline styles are applied on top of paragraph styles, allowing them to override paragraph-level styling.
9+
// Alignment styles are applied last, ensuring they position the final, fully styled text.
10+
object EnrichedSpanFlags {
11+
private const val ALIGNMENT_SPAN_PRIORITY = 0
12+
private const val INLINE_SPAN_PRIORITY = 1
13+
private const val PARAGRAPH_SPAN_PRIORITY = 2
14+
15+
@JvmStatic
16+
@JvmOverloads
17+
fun forSpan(
18+
span: Any?,
19+
baseFlags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
20+
): Int {
21+
val priority =
22+
when (span) {
23+
is EnrichedAlignmentSpan -> ALIGNMENT_SPAN_PRIORITY
24+
is EnrichedInlineSpan -> INLINE_SPAN_PRIORITY
25+
else -> PARAGRAPH_SPAN_PRIORITY
26+
}
27+
return applyPriority(baseFlags, priority)
28+
}
29+
30+
private fun applyPriority(
31+
flags: Int,
32+
priority: Int,
33+
): Int {
34+
// Cleaning up priority bits
35+
val cleared = flags and Spannable.SPAN_PRIORITY.inv()
36+
// Injecting priority bits
37+
return cleared or ((priority shl Spannable.SPAN_PRIORITY_SHIFT) and Spannable.SPAN_PRIORITY)
38+
}
39+
}

android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import android.text.TextUtils;
88
import android.text.style.ParagraphStyle;
99
import com.swmansion.enriched.common.EnrichedConstants;
10+
import com.swmansion.enriched.common.EnrichedSpanFlags;
1011
import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan;
1112
import com.swmansion.enriched.common.spans.EnrichedBoldSpan;
1213
import com.swmansion.enriched.common.spans.EnrichedCheckboxListSpan;
@@ -467,10 +468,7 @@ public Spanned convert() {
467468
if (end == start) {
468469
mSpannableStringBuilder.removeSpan(obj[i]);
469470
} else {
470-
// TODO: verify if Spannable.SPAN_EXCLUSIVE_EXCLUSIVE does not break anything.
471-
// Previously it was SPAN_PARAGRAPH. I've changed that in order to fix ranges for list
472-
// items.
473-
mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
471+
mSpannableStringBuilder.setSpan(obj[i], start, end, EnrichedSpanFlags.forSpan(obj[i]));
474472
}
475473
}
476474

@@ -505,7 +503,7 @@ public Spanned convert() {
505503

506504
mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan);
507505
mSpannableStringBuilder.setSpan(
508-
zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
506+
zeroWidthSpaceSpan, start, end, EnrichedSpanFlags.forSpan(zeroWidthSpaceSpan));
509507
}
510508

511509
return mSpannableStringBuilder;
@@ -802,7 +800,7 @@ private static void setSpanFromMark(Spannable text, Object mark, Object... spans
802800
int len = text.length();
803801
if (where != len) {
804802
for (Object span : spans) {
805-
text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
803+
text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span));
806804
}
807805
}
808806
}
@@ -825,7 +823,7 @@ private static void setParagraphSpanFromMark(Editable text, Object mark, Object.
825823

826824
if (where != len) {
827825
for (Object span : spans) {
828-
text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
826+
text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span));
829827
}
830828
}
831829
}
@@ -850,11 +848,9 @@ private static <T> void startImg(
850848

851849
int len = text.length();
852850
text.append("");
853-
text.setSpan(
854-
spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height)),
855-
len,
856-
text.length(),
857-
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
851+
Object imageSpan =
852+
spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height));
853+
text.setSpan(imageSpan, len, text.length(), EnrichedSpanFlags.forSpan(imageSpan));
858854
}
859855

860856
private static void startA(Editable text, Attributes attributes) {

android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
2323
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
2424
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
2525
import com.swmansion.enriched.common.EnrichedConstants
26+
import com.swmansion.enriched.common.EnrichedSpanFlags
2627
import com.swmansion.enriched.common.GumboNormalizer
2728
import com.swmansion.enriched.common.parser.EnrichedParser
2829
import com.swmansion.enriched.common.pixelFromSpOrDp
@@ -277,7 +278,7 @@ class EnrichedTextView : AppCompatTextView {
277278

278279
spannable.removeSpan(span)
279280
val newSpan = span.rebuildWithStyle(enrichedStyle)
280-
spannable.setSpan(newSpan, start, end, flags)
281+
spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags))
281282
modified = true
282283
}
283284

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
3838
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
3939
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
4040
import com.swmansion.enriched.common.EnrichedConstants
41+
import com.swmansion.enriched.common.EnrichedSpanFlags
4142
import com.swmansion.enriched.common.GumboNormalizer
4243
import com.swmansion.enriched.common.parser.EnrichedParser
4344
import com.swmansion.enriched.common.pixelFromSpOrDp
@@ -1108,7 +1109,7 @@ class EnrichedTextInputView :
11081109

11091110
spannable.removeSpan(span)
11101111
val newSpan = span.rebuildWithStyle(htmlStyle)
1111-
spannable.setSpan(newSpan, start, end, flags)
1112+
spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags))
11121113
}
11131114

11141115
if (shouldEmitStateChange) {

android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.text.Editable
44
import android.text.Spannable
55
import android.text.SpannableStringBuilder
66
import com.swmansion.enriched.common.EnrichedConstants
7+
import com.swmansion.enriched.common.EnrichedSpanFlags
78
import com.swmansion.enriched.textinput.EnrichedTextInputView
89
import com.swmansion.enriched.textinput.spans.EnrichedInputAlignmentSpan
910
import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan
@@ -24,12 +25,8 @@ class AlignmentStyles(
2425
flags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2526
) {
2627
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
27-
spannable.setSpan(
28-
EnrichedInputAlignmentSpan(cssValue),
29-
safeStart,
30-
safeEnd,
31-
flags,
32-
)
28+
val span = EnrichedInputAlignmentSpan(cssValue)
29+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span, flags))
3330
}
3431

3532
private fun toCssValue(alignment: String): String =
@@ -302,7 +299,9 @@ class AlignmentStyles(
302299
// INCLUSIVE_EXCLUSIVE is intentional here: autoStretchAlignmentSpan will convert
303300
// it to EXCLUSIVE_EXCLUSIVE once the merge is complete.
304301
val (safeStart, safeEnd) = s.getSafeSpanBoundaries(paraStart, paraEnd)
305-
dominantTopSpan?.let { s.setSpan(it, safeStart, safeEnd, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) }
302+
dominantTopSpan?.let {
303+
s.setSpan(it, safeStart, safeEnd, EnrichedSpanFlags.forSpan(it, Spannable.SPAN_INCLUSIVE_EXCLUSIVE))
304+
}
306305
return cursorPosition
307306
}
308307

android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.swmansion.enriched.textinput.styles
22

33
import android.text.Editable
44
import android.text.Spannable
5+
import com.swmansion.enriched.common.EnrichedSpanFlags
56
import com.swmansion.enriched.textinput.EnrichedTextInputView
67
import com.swmansion.enriched.textinput.spans.EnrichedSpans
78
import com.swmansion.enriched.textinput.utils.getSafeSpanBoundaries
@@ -41,7 +42,7 @@ class InlineStyles(
4142

4243
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
4344
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(minimum, maximum)
44-
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
45+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
4546
}
4647

4748
private fun <T> setAndMergeSpans(

android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.text.Spannable
55
import android.text.SpannableStringBuilder
66
import android.text.Spanned
77
import com.swmansion.enriched.common.EnrichedConstants
8+
import com.swmansion.enriched.common.EnrichedSpanFlags
89
import com.swmansion.enriched.textinput.EnrichedTextInputView
910
import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan
1011
import com.swmansion.enriched.textinput.spans.EnrichedInputOrderedListSpan
@@ -66,18 +67,18 @@ class ListStyles(
6667
when (name) {
6768
EnrichedSpans.UNORDERED_LIST -> {
6869
val span = EnrichedInputUnorderedListSpan(view.htmlStyle)
69-
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
70+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
7071
}
7172

7273
EnrichedSpans.ORDERED_LIST -> {
7374
val index = getOrderedListIndex(spannable, safeStart)
7475
val span = EnrichedInputOrderedListSpan(index, view.htmlStyle)
75-
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
76+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
7677
}
7778

7879
EnrichedSpans.CHECKBOX_LIST -> {
7980
val span = EnrichedInputCheckboxListSpan(isChecked ?: false, view.htmlStyle)
80-
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
81+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
8182

8283
// Invalidate layout to update checkbox drawing in case checkbox is bigger than line height
8384
view.layoutManager.invalidateLayout()

android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.text.Editable
44
import android.text.Spannable
55
import android.text.SpannableStringBuilder
66
import android.util.Log
7+
import com.swmansion.enriched.common.EnrichedSpanFlags
78
import com.swmansion.enriched.textinput.EnrichedTextInputView
89
import com.swmansion.enriched.textinput.spans.EnrichedSpans
910
import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
@@ -89,7 +90,7 @@ class ParagraphStyles(
8990
}
9091

9192
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd)
92-
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
93+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
9394
}
9495

9596
private fun <T> setSpan(
@@ -105,7 +106,7 @@ class ParagraphStyles(
105106

106107
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
107108
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
108-
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
109+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
109110
}
110111

111112
// Removes spans of the given type in the specified range.
@@ -232,7 +233,7 @@ class ParagraphStyles(
232233
val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd)
233234
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
234235

235-
s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
236+
s.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
236237
}
237238

238239
private fun handleConflictsDuringNewlineDeletion(

android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.text.Spannable
55
import android.text.SpannableStringBuilder
66
import android.text.Spanned
77
import com.swmansion.enriched.common.EnrichedConstants
8+
import com.swmansion.enriched.common.EnrichedSpanFlags
89
import com.swmansion.enriched.textinput.EnrichedTextInputView
910
import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
1011
import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan
@@ -63,7 +64,7 @@ class ParametrizedStyles(
6364
val spanEnd = start + text.length
6465
val span = EnrichedInputLinkSpan(url, view.htmlStyle, true)
6566
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
66-
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
67+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
6768

6869
view.selection?.validateStyles()
6970
isSettingLinkSpan = false
@@ -160,7 +161,7 @@ class ParametrizedStyles(
160161
span,
161162
safeStart,
162163
safeEnd,
163-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
164+
EnrichedSpanFlags.forSpan(span),
164165
)
165166
}
166167
}
@@ -373,7 +374,7 @@ class ParametrizedStyles(
373374
val span = EnrichedInputMentionSpan(text, indicator, attributes, view.htmlStyle)
374375
val spanEnd = start + text.length
375376
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
376-
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
377+
spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span))
377378

378379
val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
379380
if (!hasSpaceAtTheEnd) {

android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swmansion.enriched.textinput.utils
33
import android.text.Spannable
44
import android.text.SpannableString
55
import android.text.SpannableStringBuilder
6+
import com.swmansion.enriched.common.EnrichedSpanFlags
67
import com.swmansion.enriched.common.spans.interfaces.EnrichedBlockSpan
78
import com.swmansion.enriched.common.spans.interfaces.EnrichedParagraphSpan
89
import com.swmansion.enriched.common.spans.interfaces.EnrichedSpan
@@ -144,7 +145,7 @@ fun Spannable.mergeSpannables(
144145
val (_, newParagraphEnd) = builder.getParagraphBounds(spanStart, pasteEnd)
145146
val flags = builder.getSpanFlags(span)
146147
builder.removeSpan(span)
147-
builder.setSpan(span, spanStart, newParagraphEnd, flags)
148+
builder.setSpan(span, spanStart, newParagraphEnd, EnrichedSpanFlags.forSpan(span, flags))
148149
}
149150
}
150151

0 commit comments

Comments
 (0)