Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ import com.swmansion.enriched.events.MentionHandler
import com.swmansion.enriched.events.OnInputBlurEvent
import com.swmansion.enriched.events.OnInputFocusEvent
import com.swmansion.enriched.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.spans.EnrichedH1Span
import com.swmansion.enriched.spans.EnrichedH2Span
import com.swmansion.enriched.spans.EnrichedH3Span
import com.swmansion.enriched.spans.EnrichedImageSpan
import com.swmansion.enriched.spans.EnrichedSpans
import com.swmansion.enriched.spans.interfaces.EnrichedSpan
import com.swmansion.enriched.styles.InlineStyles
import com.swmansion.enriched.styles.ListStyles
import com.swmansion.enriched.styles.ParagraphStyles
Expand Down Expand Up @@ -60,6 +64,13 @@ class EnrichedTextInputView : AppCompatEditText {

val mentionHandler: MentionHandler? = MentionHandler(this)
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
set(value) {
if (field != value) {
Comment thread
exploIF marked this conversation as resolved.
val prev = field
field = value
reApplyHtmlStyleForSpans(prev, value)
}
}
var spanWatcher: EnrichedSpanWatcher? = null
var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)

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

fun setFontFamily(family: String?) {
Expand Down Expand Up @@ -592,6 +604,72 @@ class EnrichedTextInputView : AppCompatEditText {
}
}

private fun forceScrollToSelection() {
val textLayout = layout ?: return
val cursorOffset = selectionStart
if (cursorOffset <= 0) return

val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
val visibleTextHeight = height - paddingTop - paddingBottom

if (visibleTextHeight <= 0) return

val visibleTop = scrollY
val visibleBottom = scrollY + visibleTextHeight
var targetScrollY = scrollY

if (selectedLineTop < visibleTop) {
targetScrollY = selectedLineTop
} else if (selectedLineBottom > visibleBottom) {
targetScrollY = selectedLineBottom - visibleTextHeight
}

val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
scrollTo(scrollX, targetScrollY)
}

private fun reApplyHtmlStyleForSpans(previousHtmlStyle: HtmlStyle, nextHtmlStyle: HtmlStyle) {
val shouldRemoveBoldSpanFromH1Span = !previousHtmlStyle.h1Bold && nextHtmlStyle.h1Bold
val shouldRemoveBoldSpanFromH2Span = !previousHtmlStyle.h2Bold && nextHtmlStyle.h2Bold
val shouldRemoveBoldSpanFromH3Span = !previousHtmlStyle.h3Bold && nextHtmlStyle.h3Bold

val spannable = text as? Spannable ?: return
if (spannable.isEmpty()) return

var shouldEmitStateChange = false

runAsATransaction {
val spans = spannable.getSpans(0, spannable.length, EnrichedSpan::class.java)
for (span in spans) {
if (!span.dependsOnHtmlStyle) continue

val start = spannable.getSpanStart(span)
val end = spannable.getSpanEnd(span)
val flags = spannable.getSpanFlags(span)

if (start == -1 || end == -1) continue

if ((span is EnrichedH1Span && shouldRemoveBoldSpanFromH1Span) || (span is EnrichedH2Span && shouldRemoveBoldSpanFromH2Span) || (span is EnrichedH3Span && shouldRemoveBoldSpanFromH3Span)) {
val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
if (isRemoved) shouldEmitStateChange = true
}

spannable.removeSpan(span)
val newSpan = span.rebuildWithStyle(htmlStyle)
spannable.setSpan(newSpan, start, end, flags)
}

if (shouldEmitStateChange) {
selection?.validateStyles()
}
}
layoutManager.invalidateLayout()
forceScrollToSelection()
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.swmansion.enriched.styles.HtmlStyle

// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand All @@ -35,4 +37,8 @@ class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffecting
textPaint?.color = color
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBlockQuoteSpan {
return EnrichedBlockQuoteSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ package com.swmansion.enriched.spans

import android.graphics.Typeface
import android.text.style.StyleSpan
import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
import com.swmansion.enriched.styles.HtmlStyle

@Suppress("UNUSED_PARAMETER")
class EnrichedBoldSpan(htmlStyle: HtmlStyle) : StyleSpan(Typeface.BOLD), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = false

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedBoldSpan {
return EnrichedBoldSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateDrawState(paint: TextPaint) {
paint.typeface = Typeface.MONOSPACE
paint.color = htmlStyle.codeBlockColor
Expand Down Expand Up @@ -74,4 +76,8 @@ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingS
canvas.drawPath(path, p)
p.color = previousColor
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedCodeBlockSpan {
return EnrichedCodeBlockSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedH1Span(private val style: HtmlStyle) : AbsoluteSizeSpan(style.h1FontSize), EnrichedHeadingSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateDrawState(tp: TextPaint) {
super.updateDrawState(tp)
val bold = style.h1Bold
if (bold) {
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH1Span {
return EnrichedH1Span(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedH2Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h2FontSize), EnrichedHeadingSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateDrawState(tp: TextPaint) {
super.updateDrawState(tp)
val bold = htmlStyle.h2Bold
if (bold) {
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH2Span {
return EnrichedH2Span(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedH3Span(private val htmlStyle: HtmlStyle) : AbsoluteSizeSpan(htmlStyle.h3FontSize), EnrichedHeadingSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateDrawState(tp: TextPaint) {
super.updateDrawState(tp)
val bold = htmlStyle.h3Bold
if (bold) {
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedH3Span {
return EnrichedH3Span(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import com.swmansion.enriched.utils.AsyncDrawable
import androidx.core.graphics.drawable.toDrawable
import com.swmansion.enriched.R
import com.swmansion.enriched.spans.utils.ForceRedrawSpan
import com.swmansion.enriched.styles.HtmlStyle
import com.swmansion.enriched.utils.ResourceManager

class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = false

private var width: Int = 0
private var height: Int = 0

Expand Down Expand Up @@ -119,6 +122,8 @@ class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
return height
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedImageSpan = this

companion object {
fun createEnrichedImageSpan(src: String, width: Int, height: Int): EnrichedImageSpan {
var imgDrawable = prepareDrawableForImage(src)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateDrawState(textPaint: TextPaint) {
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
textPaint.typeface = typeface
Expand All @@ -18,4 +20,8 @@ class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffecting
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
textPaint.typeface = typeface
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedInlineCodeSpan {
return EnrichedInlineCodeSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ import com.swmansion.enriched.styles.HtmlStyle

@Suppress("UNUSED_PARAMETER")
class EnrichedItalicSpan(private val htmlStyle: HtmlStyle) : StyleSpan(Typeface.ITALIC), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = false

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedItalicSpan {
return EnrichedItalicSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedLinkSpan(private val url: String, private val htmlStyle: HtmlStyle) : ClickableSpan(), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun onClick(view: View) {
// Do nothing, links inside the input are not clickable.
// We are using `ClickableSpan` to allow the text to be styled as a link.
Expand All @@ -21,4 +23,8 @@ class EnrichedLinkSpan(private val url: String, private val htmlStyle: HtmlStyle
fun getUrl(): String {
return url
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedLinkSpan {
return EnrichedLinkSpan(url, htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.swmansion.enriched.styles.HtmlStyle

class EnrichedMentionSpan(private val text: String, private val indicator: String, private val attributes: Map<String, String>, private val htmlStyle: HtmlStyle) :
ClickableSpan(), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun onClick(view: View) {
// Do nothing. Mentions inside the input are not clickable.
// We are using `ClickableSpan` to allow the text to be styled as a clickable element.
Expand All @@ -33,4 +35,8 @@ class EnrichedMentionSpan(private val text: String, private val indicator: Strin
fun getIndicator(): String {
return indicator
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedMentionSpan {
return EnrichedMentionSpan(text, indicator, attributes, htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
import com.swmansion.enriched.styles.HtmlStyle

class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand Down Expand Up @@ -78,4 +80,8 @@ class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: Htm
fun setIndex(i: Int) {
index = i
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedOrderedListSpan {
return EnrichedOrderedListSpan(index, htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ import com.swmansion.enriched.styles.HtmlStyle

@Suppress("UNUSED_PARAMETER")
class EnrichedStrikeThroughSpan(private val htmlStyle: HtmlStyle) : StrikethroughSpan(), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = false

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedStrikeThroughSpan {
return EnrichedStrikeThroughSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ import com.swmansion.enriched.styles.HtmlStyle

@Suppress("UNUSED_PARAMETER")
class EnrichedUnderlineSpan(private val htmlStyle: HtmlStyle) : UnderlineSpan(), EnrichedInlineSpan {
override val dependsOnHtmlStyle: Boolean = false

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedUnderlineSpan {
return EnrichedUnderlineSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.swmansion.enriched.styles.HtmlStyle

// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/BulletSpan.java
class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
override val dependsOnHtmlStyle: Boolean = true

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand Down Expand Up @@ -56,4 +58,8 @@ class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : MetricAffect
paint.style = style
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedUnorderedListSpan {
return EnrichedUnorderedListSpan(htmlStyle)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
package com.swmansion.enriched.spans.interfaces

import com.swmansion.enriched.styles.HtmlStyle

interface EnrichedSpan {
val dependsOnHtmlStyle: Boolean
fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedSpan
}
Loading
Loading