Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert Spannable to AnnotatedString in Android?

I have a large project that uses regular android layouts. I'm starting to use compose on this project. However, I already have a large codebase and a lot of utils that deal with CharSequence and Spannable. For example, currency formatter that returns Spannable.

Compose Text doesn't accept neither CharSequence nor Spannable. However, it does accept AnnotatedString and, from what I can tell, they are basically the same thing. So I'm thinking that there must be a way to easily convert Spannable to AnnotatedString, something like spannable.toAnnotatedString(), but I can't find anything so far.

Can I convert Spannable to AnnotatedString or do I have to write a lot of code from scratch?

like image 360
Alexey Avatar asked Dec 18 '25 22:12

Alexey


2 Answers

This solution works for basic html, but you might want to add more Copier classes to support more exotic spans.

fun Spannable.toAnnotatedString(primaryColor: Color): AnnotatedString {
    val builder = AnnotatedString.Builder(this.toString())
    val copierContext = CopierContext(primaryColor)
    SpanCopier.values().forEach { copier ->
        getSpans(0, length, copier.spanClass).forEach { span ->
            copier.copySpan(span, getSpanStart(span), getSpanEnd(span), builder, copierContext)
        }
    }
    return builder.toAnnotatedString()
}

private data class CopierContext(
    val primaryColor: Color,
)

private enum class SpanCopier {
    URL {
        override val spanClass = URLSpan::class.java
        override fun copySpan(
            span: Any,
            start: Int,
            end: Int,
            destination: AnnotatedString.Builder,
            context: CopierContext
        ) {
            val urlSpan = span as URLSpan
            destination.addStringAnnotation(
                tag = name,
                annotation = urlSpan.url,
                start = start,
                end = end,
            )
            destination.addStyle(
                style = SpanStyle(color = context.primaryColor, textDecoration = TextDecoration.Underline),
                start = start,
                end = end,
            )
        }
    },
    FOREGROUND_COLOR {
        override val spanClass = ForegroundColorSpan::class.java
        override fun copySpan(
            span: Any,
            start: Int,
            end: Int,
            destination: AnnotatedString.Builder,
            context: CopierContext
        ) {
            val colorSpan = span as ForegroundColorSpan
            destination.addStyle(
                style = SpanStyle(color = Color(colorSpan.foregroundColor)),
                start = start,
                end = end,
            )
        }
    },
    UNDERLINE {
        override val spanClass = UnderlineSpan::class.java
        override fun copySpan(
            span: Any,
            start: Int,
            end: Int,
            destination: AnnotatedString.Builder,
            context: CopierContext
        ) {
            destination.addStyle(
                style = SpanStyle(textDecoration = TextDecoration.Underline),
                start = start,
                end = end,
            )
        }
    },
    STYLE {
        override val spanClass = StyleSpan::class.java
        override fun copySpan(
            span: Any,
            start: Int,
            end: Int,
            destination: AnnotatedString.Builder,
            context: CopierContext
        ) {
            val styleSpan = span as StyleSpan

            destination.addStyle(
                style = when (styleSpan.style) {
                    Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
                    Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
                    Typeface.BOLD_ITALIC -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
                    else -> SpanStyle()
                },
                start = start,
                end = end,
            )
        }
    };

    abstract val spanClass: Class<out CharacterStyle>
    abstract fun copySpan(span: Any, start: Int, end: Int, destination: AnnotatedString.Builder, context: CopierContext)
}
like image 116
Alexey Avatar answered Dec 20 '25 11:12

Alexey


There is a really handy library which already solves this problem. You can convert Android's Spanned into AnnotatedString with a lot of customisations.

https://github.com/Aghajari/AnnotatedText?tab=readme-ov-file

like image 37
goemic Avatar answered Dec 20 '25 11:12

goemic



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!