diff --git a/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt b/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt new file mode 100644 index 00000000..d9a0ddb5 --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt @@ -0,0 +1,39 @@ +package com.swmansion.enriched.common + +import android.text.Spannable +import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan +import com.swmansion.enriched.common.spans.interfaces.EnrichedInlineSpan + +// Higher priority spans are processed first, so styles with lower priorities are painted on top of previously applied styles. +// For example, inline styles are applied on top of paragraph styles, allowing them to override paragraph-level styling. +// Alignment styles are applied last, ensuring they position the final, fully styled text. +object EnrichedSpanFlags { + private const val ALIGNMENT_SPAN_PRIORITY = 0 + private const val INLINE_SPAN_PRIORITY = 1 + private const val PARAGRAPH_SPAN_PRIORITY = 2 + + @JvmStatic + @JvmOverloads + fun forSpan( + span: Any?, + baseFlags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ): Int { + val priority = + when (span) { + is EnrichedAlignmentSpan -> ALIGNMENT_SPAN_PRIORITY + is EnrichedInlineSpan -> INLINE_SPAN_PRIORITY + else -> PARAGRAPH_SPAN_PRIORITY + } + return applyPriority(baseFlags, priority) + } + + private fun applyPriority( + flags: Int, + priority: Int, + ): Int { + // Cleaning up priority bits + val cleared = flags and Spannable.SPAN_PRIORITY.inv() + // Injecting priority bits + return cleared or ((priority shl Spannable.SPAN_PRIORITY_SHIFT) and Spannable.SPAN_PRIORITY) + } +} diff --git a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java index 02d52f70..08198b3c 100644 --- a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +++ b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java @@ -7,6 +7,7 @@ import android.text.TextUtils; import android.text.style.ParagraphStyle; import com.swmansion.enriched.common.EnrichedConstants; +import com.swmansion.enriched.common.EnrichedSpanFlags; import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan; import com.swmansion.enriched.common.spans.EnrichedBoldSpan; import com.swmansion.enriched.common.spans.EnrichedCheckboxListSpan; @@ -467,10 +468,7 @@ public Spanned convert() { if (end == start) { mSpannableStringBuilder.removeSpan(obj[i]); } else { - // TODO: verify if Spannable.SPAN_EXCLUSIVE_EXCLUSIVE does not break anything. - // Previously it was SPAN_PARAGRAPH. I've changed that in order to fix ranges for list - // items. - mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannableStringBuilder.setSpan(obj[i], start, end, EnrichedSpanFlags.forSpan(obj[i])); } } @@ -505,7 +503,7 @@ public Spanned convert() { mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan); mSpannableStringBuilder.setSpan( - zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + zeroWidthSpaceSpan, start, end, EnrichedSpanFlags.forSpan(zeroWidthSpaceSpan)); } return mSpannableStringBuilder; @@ -802,7 +800,7 @@ private static void setSpanFromMark(Spannable text, Object mark, Object... spans int len = text.length(); if (where != len) { for (Object span : spans) { - text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span)); } } } @@ -825,7 +823,7 @@ private static void setParagraphSpanFromMark(Editable text, Object mark, Object. if (where != len) { for (Object span : spans) { - text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span)); } } } @@ -850,11 +848,9 @@ private static void startImg( int len = text.length(); text.append(""); - text.setSpan( - spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height)), - len, - text.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Object imageSpan = + spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height)); + text.setSpan(imageSpan, len, text.length(), EnrichedSpanFlags.forSpan(imageSpan)); } private static void startA(Editable text, Attributes attributes) { diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt index d3133ee8..53a0a503 100644 --- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt @@ -23,6 +23,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser import com.swmansion.enriched.common.pixelFromSpOrDp @@ -277,7 +278,7 @@ class EnrichedTextView : AppCompatTextView { spannable.removeSpan(span) val newSpan = span.rebuildWithStyle(enrichedStyle) - spannable.setSpan(newSpan, start, end, flags) + spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags)) modified = true } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt index cdcad4e2..b2a0c16c 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -38,6 +38,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser import com.swmansion.enriched.common.pixelFromSpOrDp @@ -1108,7 +1109,7 @@ class EnrichedTextInputView : spannable.removeSpan(span) val newSpan = span.rebuildWithStyle(htmlStyle) - spannable.setSpan(newSpan, start, end, flags) + spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags)) } if (shouldEmitStateChange) { diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt index d7df9510..e7353f67 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt @@ -4,6 +4,7 @@ import android.text.Editable import android.text.Spannable import android.text.SpannableStringBuilder import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputAlignmentSpan import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan @@ -24,12 +25,8 @@ class AlignmentStyles( flags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, ) { val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end) - spannable.setSpan( - EnrichedInputAlignmentSpan(cssValue), - safeStart, - safeEnd, - flags, - ) + val span = EnrichedInputAlignmentSpan(cssValue) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span, flags)) } private fun toCssValue(alignment: String): String = @@ -302,7 +299,9 @@ class AlignmentStyles( // INCLUSIVE_EXCLUSIVE is intentional here: autoStretchAlignmentSpan will convert // it to EXCLUSIVE_EXCLUSIVE once the merge is complete. val (safeStart, safeEnd) = s.getSafeSpanBoundaries(paraStart, paraEnd) - dominantTopSpan?.let { s.setSpan(it, safeStart, safeEnd, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } + dominantTopSpan?.let { + s.setSpan(it, safeStart, safeEnd, EnrichedSpanFlags.forSpan(it, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)) + } return cursorPosition } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt index 26c871ce..802f78f1 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt @@ -2,6 +2,7 @@ package com.swmansion.enriched.textinput.styles import android.text.Editable import android.text.Spannable +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedSpans import com.swmansion.enriched.textinput.utils.getSafeSpanBoundaries @@ -41,7 +42,7 @@ class InlineStyles( val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(minimum, maximum) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun setAndMergeSpans( diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt index 7bce4dcd..236560f1 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt @@ -5,6 +5,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan import com.swmansion.enriched.textinput.spans.EnrichedInputOrderedListSpan @@ -66,18 +67,18 @@ class ListStyles( when (name) { EnrichedSpans.UNORDERED_LIST -> { val span = EnrichedInputUnorderedListSpan(view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } EnrichedSpans.ORDERED_LIST -> { val index = getOrderedListIndex(spannable, safeStart) val span = EnrichedInputOrderedListSpan(index, view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } EnrichedSpans.CHECKBOX_LIST -> { val span = EnrichedInputCheckboxListSpan(isChecked ?: false, view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) // Invalidate layout to update checkbox drawing in case checkbox is bigger than line height view.layoutManager.invalidateLayout() diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt index b7d46637..af822b7f 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt @@ -4,6 +4,7 @@ import android.text.Editable import android.text.Spannable import android.text.SpannableStringBuilder import android.util.Log +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedSpans import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan @@ -89,7 +90,7 @@ class ParagraphStyles( } val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun setSpan( @@ -105,7 +106,7 @@ class ParagraphStyles( val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } // Removes spans of the given type in the specified range. @@ -232,7 +233,7 @@ class ParagraphStyles( val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd) val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) - s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + s.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun handleConflictsDuringNewlineDeletion( diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt index be6265b7..c441224b 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt @@ -5,6 +5,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan @@ -63,7 +64,7 @@ class ParametrizedStyles( val spanEnd = start + text.length val span = EnrichedInputLinkSpan(url, view.htmlStyle, true) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) view.selection?.validateStyles() isSettingLinkSpan = false @@ -160,7 +161,7 @@ class ParametrizedStyles( span, safeStart, safeEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + EnrichedSpanFlags.forSpan(span), ) } } @@ -373,7 +374,7 @@ class ParametrizedStyles( val span = EnrichedInputMentionSpan(text, indicator, attributes, view.htmlStyle) val spanEnd = start + text.length val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' ' if (!hasSpaceAtTheEnd) { diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt index 60ebd1a1..576e1021 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt @@ -3,6 +3,7 @@ package com.swmansion.enriched.textinput.utils import android.text.Spannable import android.text.SpannableString import android.text.SpannableStringBuilder +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.spans.interfaces.EnrichedBlockSpan import com.swmansion.enriched.common.spans.interfaces.EnrichedParagraphSpan import com.swmansion.enriched.common.spans.interfaces.EnrichedSpan @@ -144,7 +145,7 @@ fun Spannable.mergeSpannables( val (_, newParagraphEnd) = builder.getParagraphBounds(spanStart, pasteEnd) val flags = builder.getSpanFlags(span) builder.removeSpan(span) - builder.setSpan(span, spanStart, newParagraphEnd, flags) + builder.setSpan(span, spanStart, newParagraphEnd, EnrichedSpanFlags.forSpan(span, flags)) } } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt index 5f378a32..57f265b1 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt @@ -7,6 +7,7 @@ import android.text.Spanned import android.util.Log import android.view.MotionEvent import android.widget.TextView +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan import org.json.JSONObject @@ -78,7 +79,7 @@ fun TextView.setCheckboxClickListener() { // Reapply span so changes are visible without need to redraw entire TextView spannable.removeSpan(span) - spannable.setSpan(span, start, end, flags) + spannable.setSpan(span, start, end, EnrichedSpanFlags.forSpan(span, flags)) // For focused input, ensure cursor is active for affected paragraph if (tv.isFocused) {