Search code examples
androidandroid-jetpack-compose

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?


Solution

  • 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)
    }