Skip to content

Commit 7f6c535

Browse files
fix: recalculating layout after changing html styles - Android (#233)
### Description If we change the HTML styles on Android, they will not be applied in any way until we first enter any styles. We also got a bug with the initial defaultValue, because everything started to depend on who was the first to be counted (it depends on who was the first to be passed from JS to the component). Thus, I added the calculation of the lead when changing the HTML style on Android. ### Videos Before: https://github.com/user-attachments/assets/0ccaae0c-d5e0-4270-b000-534260803ea3 https://github.com/user-attachments/assets/e89536be-c787-477a-8aba-fba2e0a6919d After: https://github.com/user-attachments/assets/6d27d9f7-cbbf-40f3-9010-557298a3eaf7 --------- Co-authored-by: Igor Furgala <exploif@icloud.com> Co-authored-by: Igor Furgała <74370735+exploIF@users.noreply.github.com>
1 parent 3625574 commit 7f6c535

18 files changed

Lines changed: 246 additions & 0 deletions

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ import com.swmansion.enriched.events.MentionHandler
3030
import com.swmansion.enriched.events.OnInputBlurEvent
3131
import com.swmansion.enriched.events.OnInputFocusEvent
3232
import com.swmansion.enriched.events.OnRequestHtmlResultEvent
33+
import com.swmansion.enriched.spans.EnrichedH1Span
34+
import com.swmansion.enriched.spans.EnrichedH2Span
35+
import com.swmansion.enriched.spans.EnrichedH3Span
3336
import com.swmansion.enriched.spans.EnrichedImageSpan
3437
import com.swmansion.enriched.spans.EnrichedSpans
38+
import com.swmansion.enriched.spans.interfaces.EnrichedSpan
3539
import com.swmansion.enriched.styles.InlineStyles
3640
import com.swmansion.enriched.styles.ListStyles
3741
import com.swmansion.enriched.styles.ParagraphStyles
@@ -60,6 +64,13 @@ class EnrichedTextInputView : AppCompatEditText {
6064

6165
val mentionHandler: MentionHandler? = MentionHandler(this)
6266
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
67+
set(value) {
68+
if (field != value) {
69+
val prev = field
70+
field = value
71+
reApplyHtmlStyleForSpans(prev, value)
72+
}
73+
}
6374
var spanWatcher: EnrichedSpanWatcher? = null
6475
var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
6576

@@ -336,6 +347,7 @@ class EnrichedTextInputView : AppCompatEditText {
336347
// This ensured that newly created spans will take the new font size into account
337348
htmlStyle.invalidateStyles()
338349
layoutManager.invalidateLayout()
350+
forceScrollToSelection()
339351
}
340352

341353
fun setFontFamily(family: String?) {
@@ -592,6 +604,72 @@ class EnrichedTextInputView : AppCompatEditText {
592604
}
593605
}
594606

607+
private fun forceScrollToSelection() {
608+
val textLayout = layout ?: return
609+
val cursorOffset = selectionStart
610+
if (cursorOffset <= 0) return
611+
612+
val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
613+
val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
614+
val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
615+
val visibleTextHeight = height - paddingTop - paddingBottom
616+
617+
if (visibleTextHeight <= 0) return
618+
619+
val visibleTop = scrollY
620+
val visibleBottom = scrollY + visibleTextHeight
621+
var targetScrollY = scrollY
622+
623+
if (selectedLineTop < visibleTop) {
624+
targetScrollY = selectedLineTop
625+
} else if (selectedLineBottom > visibleBottom) {
626+
targetScrollY = selectedLineBottom - visibleTextHeight
627+
}
628+
629+
val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
630+
targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
631+
scrollTo(scrollX, targetScrollY)
632+
}
633+
634+
private fun reApplyHtmlStyleForSpans(previousHtmlStyle: HtmlStyle, nextHtmlStyle: HtmlStyle) {
635+
val shouldRemoveBoldSpanFromH1Span = !previousHtmlStyle.h1Bold && nextHtmlStyle.h1Bold
636+
val shouldRemoveBoldSpanFromH2Span = !previousHtmlStyle.h2Bold && nextHtmlStyle.h2Bold
637+
val shouldRemoveBoldSpanFromH3Span = !previousHtmlStyle.h3Bold && nextHtmlStyle.h3Bold
638+
639+
val spannable = text as? Spannable ?: return
640+
if (spannable.isEmpty()) return
641+
642+
var shouldEmitStateChange = false
643+
644+
runAsATransaction {
645+
val spans = spannable.getSpans(0, spannable.length, EnrichedSpan::class.java)
646+
for (span in spans) {
647+
if (!span.dependsOnHtmlStyle) continue
648+
649+
val start = spannable.getSpanStart(span)
650+
val end = spannable.getSpanEnd(span)
651+
val flags = spannable.getSpanFlags(span)
652+
653+
if (start == -1 || end == -1) continue
654+
655+
if ((span is EnrichedH1Span && shouldRemoveBoldSpanFromH1Span) || (span is EnrichedH2Span && shouldRemoveBoldSpanFromH2Span) || (span is EnrichedH3Span && shouldRemoveBoldSpanFromH3Span)) {
656+
val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
657+
if (isRemoved) shouldEmitStateChange = true
658+
}
659+
660+
spannable.removeSpan(span)
661+
val newSpan = span.rebuildWithStyle(htmlStyle)
662+
spannable.setSpan(newSpan, start, end, flags)
663+
}
664+
665+
if (shouldEmitStateChange) {
666+
selection?.validateStyles()
667+
}
668+
}
669+
layoutManager.invalidateLayout()
670+
forceScrollToSelection()
671+
}
672+
595673
override fun onAttachedToWindow() {
596674
super.onAttachedToWindow()
597675

android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.swmansion.enriched.styles.HtmlStyle
1111

1212
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
1313
class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
14+
override val dependsOnHtmlStyle: Boolean = true
15+
1416
override fun updateMeasureState(p0: TextPaint) {
1517
// Do nothing, but inform layout that this span affects text metrics
1618
}
@@ -35,4 +37,8 @@ class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffecting
3537
textPaint?.color = color
3638
}
3739
}
40+
41+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBlockQuoteSpan {
42+
return EnrichedBlockQuoteSpan(htmlStyle)
43+
}
3844
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ package com.swmansion.enriched.spans
22

33
import android.graphics.Typeface
44
import android.text.style.StyleSpan
5+
import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
56
import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
67
import com.swmansion.enriched.styles.HtmlStyle
78

89
@Suppress("UNUSED_PARAMETER")
910
class EnrichedBoldSpan(htmlStyle: HtmlStyle) : StyleSpan(Typeface.BOLD), EnrichedInlineSpan {
11+
override val dependsOnHtmlStyle: Boolean = false
12+
13+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBoldSpan {
14+
return EnrichedBoldSpan(htmlStyle)
15+
}
1016
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
1313
import com.swmansion.enriched.styles.HtmlStyle
1414

1515
class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
16+
override val dependsOnHtmlStyle: Boolean = true
17+
1618
override fun updateDrawState(paint: TextPaint) {
1719
paint.typeface = Typeface.MONOSPACE
1820
paint.color = htmlStyle.codeBlockColor
@@ -74,4 +76,8 @@ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingS
7476
canvas.drawPath(path, p)
7577
p.color = previousColor
7678
}
79+
80+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedCodeBlockSpan {
81+
return EnrichedCodeBlockSpan(htmlStyle)
82+
}
7783
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
77
import com.swmansion.enriched.styles.HtmlStyle
88

99
class EnrichedH1Span(private val style: HtmlStyle) : AbsoluteSizeSpan(style.h1FontSize), EnrichedHeadingSpan {
10+
override val dependsOnHtmlStyle: Boolean = true
11+
1012
override fun updateDrawState(tp: TextPaint) {
1113
super.updateDrawState(tp)
1214
val bold = style.h1Bold
1315
if (bold) {
1416
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
1517
}
1618
}
19+
20+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH1Span {
21+
return EnrichedH1Span(htmlStyle)
22+
}
1723
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
77
import com.swmansion.enriched.styles.HtmlStyle
88

99
class EnrichedH2Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h2FontSize), EnrichedHeadingSpan {
10+
override val dependsOnHtmlStyle: Boolean = true
11+
1012
override fun updateDrawState(tp: TextPaint) {
1113
super.updateDrawState(tp)
1214
val bold = htmlStyle.h2Bold
1315
if (bold) {
1416
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
1517
}
1618
}
19+
20+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH2Span {
21+
return EnrichedH2Span(htmlStyle)
22+
}
1723
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
77
import com.swmansion.enriched.styles.HtmlStyle
88

99
class EnrichedH3Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h3FontSize), EnrichedHeadingSpan {
10+
override val dependsOnHtmlStyle: Boolean = true
11+
1012
override fun updateDrawState(tp: TextPaint) {
1113
super.updateDrawState(tp)
1214
val bold = htmlStyle.h3Bold
1315
if (bold) {
1416
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
1517
}
1618
}
19+
20+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH3Span {
21+
return EnrichedH3Span(htmlStyle)
22+
}
1723
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ import com.swmansion.enriched.utils.AsyncDrawable
1818
import androidx.core.graphics.drawable.toDrawable
1919
import com.swmansion.enriched.R
2020
import com.swmansion.enriched.spans.utils.ForceRedrawSpan
21+
import com.swmansion.enriched.styles.HtmlStyle
2122
import com.swmansion.enriched.utils.ResourceManager
2223

2324
class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
25+
override val dependsOnHtmlStyle: Boolean = false
26+
2427
private var width: Int = 0
2528
private var height: Int = 0
2629

@@ -119,6 +122,8 @@ class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
119122
return height
120123
}
121124

125+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedImageSpan = this
126+
122127
companion object {
123128
fun createEnrichedImageSpan(src: String, width: Int, height: Int): EnrichedImageSpan {
124129
var imgDrawable = prepareDrawableForImage(src)

android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
77
import com.swmansion.enriched.styles.HtmlStyle
88

99
class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), EnrichedInlineSpan {
10+
override val dependsOnHtmlStyle: Boolean = true
11+
1012
override fun updateDrawState(textPaint: TextPaint) {
1113
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
1214
textPaint.typeface = typeface
@@ -18,4 +20,8 @@ class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffecting
1820
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
1921
textPaint.typeface = typeface
2022
}
23+
24+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedInlineCodeSpan {
25+
return EnrichedInlineCodeSpan(htmlStyle)
26+
}
2127
}

android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ import com.swmansion.enriched.styles.HtmlStyle
77

88
@Suppress("UNUSED_PARAMETER")
99
class EnrichedItalicSpan(private val htmlStyle: HtmlStyle) : StyleSpan(Typeface.ITALIC), EnrichedInlineSpan {
10+
override val dependsOnHtmlStyle: Boolean = false
11+
12+
override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedItalicSpan {
13+
return EnrichedItalicSpan(htmlStyle)
14+
}
1015
}

0 commit comments

Comments
 (0)