Adam Lee
Adam Lee

Reputation: 586

Android: How to convert HTML string to Spanned to display in TextView (MUST work on API < 24)

For my app, I need to display HTML that contains spans (with background-color) to a Spanned such that it can be displayed on a TextView, as Android's TextView does not support the span tag. I have first tried converting the String to a SpannableStringBuilder, and then retrieving the HTML encoded string from the casted string (Spanned). I need this to work on API 22-23, and thus, I cannot simply use fromHTML as fromHTML does not support span for API below 24. I am writing a function called fromHTML to accomplish this:

Example of the input to fromHTML (the input can be any string with spans):

Not highlighted string<span style=\"background-color: #FF8983\">Highlighted string</span> 
not highlighted string <span style=\"background-color: #FF8983\">Highlighted string</span> 

Below is my code:

private fun fromHtml(source:String):Spanned {
    var htmlText:SpannableStringBuilder = source as SpannableStringBuilder;
    var htmlEncodedString:String = Html.toHtml(htmlText);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
    {
        return Html.fromHtml(htmlEncodedString, Html.FROM_HTML_MODE_LEGACY)
    }
    else
    {
        return Html.fromHtml(htmlEncodedString)
    }
}

However, I get the following error:

java.lang.ClassCastException: java.lang.String cannot be cast to android.text.SpannableStringBuilder

How do I convert an HTML string to a Spanned object to display on a TextView (I need this program to work on API 22-23, and on API 22-23, span is not supported so I cannot just use a simple fromHTML conversion?

Upvotes: 5

Views: 10865

Answers (5)

Cheticamp
Cheticamp

Reputation: 62841

You are getting the class cast exception because you are trying to cast a String to a SpannableStringBuilder.

var htmlText:SpannableStringBuilder = source as SpannableStringBuilder

The way to get a string into a SpannableStringBuilder is

var htmlText = SpannableStringBuilder(source)

Regarding HTML span tag support for APIs below 24, I was thinking that HtmlCompat would provide span tag support for API 24 features, but it doesn't. That means that we have to process the span tag ourselves. (Since the span tag is not supported below API 24, we might consider using a HTML tag handler. Unfortunately, the tag handler is alerted to the presence of the span tag, but the span attributes are not made available to the tag handler :-(, so we have to do the following.)

The sample code below will process the background-color attribute of the span tag for API 18+. (It may support APIs below 18 as well.) The approach is to use a regular expression to extract the appropriate span attribute values and text from the HTML and to convert them to Android text spans in a spanned string. The spanned string can then be set to a TextView. You can find explanatory comments in the code.

Here is what the screen looks like on an emulator running API 18. The highlights were created by the code below while the bolded text was created by a call to Html.fromHtml().

enter image description here

MainActivity.kt

class MainActivity : AppCompatActivity() {
    // Html.fromHTML() seems to be a little picky about how HTML attributes are delimited.
    // Mind the spaces!
    private val mHtmlString =
        "   <span style=\"color: #FFFFFF ; background-color: #FF8983\">Highlighted</span><b> Bold!</b> Not bold <span style=\"background-color: #00FF00\">string</span>"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val tv = findViewById<TextView>(R.id.textView)
        tv.text = processHtml(mHtmlString)
    }

    private fun processHtml(s: String): Spanned? {

        // Easy for API 24+.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return HtmlCompat.fromHtml(s, HtmlCompat.FROM_HTML_MODE_LEGACY)
        }

        // HtmlCompat.fromHtml() for API 24+ can handle more <span> attributes than we try to here.
        // We will just process the background-color attribute.

        // HtmlCompat.fromHtml() will remove the spans in our string. Escape them before processing.
        var escapedSpans = s.replace("<span ", "&lt;span ", true)
        escapedSpans = escapedSpans.replace("</span>", "&lt;/span&gt;", true)

        // Process all the non-span tags the are supported pre-API 24.
        val spanned = HtmlCompat.fromHtml(escapedSpans, HtmlCompat.FROM_HTML_MODE_LEGACY)

        // Process HTML spans. Identify each background-color attribute and replace the effected
        // text with a BackgroundColorSpan. Here we assume that the background color is a hex number
        // starting with "#". Other value such as named colors can be handled with additional
        // processing.
        val sb = SpannableStringBuilder(spanned)
        val m: Matcher = SPAN_PATTERN.matcher(sb)
        do {
            if (m.find()) {
                val regionEnd = m.start(0) + m.group(2).length
                sb.replace(m.start(0), m.end(0), m.group(2))
                    .setSpan(
                        BackgroundColorSpan(parseInt(m.group(1), 16) or HTML_COLOR_OPAQUE_MASK),
                        m.start(0),
                        regionEnd,
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                    )
                m.reset(sb)
                m.region(regionEnd, sb.length)
            }
        } while (!m.hitEnd())
        return sb
    }

    companion object {
        val SPAN_PATTERN: Pattern =
            Pattern.compile("<span.*?background(?:-color)?:\\s*?#([^,]*?)[\\s;\"].*?>(.*?)</span>")
        const val HTML_COLOR_OPAQUE_MASK = 0xFF000000.toInt()
    }
}

Upvotes: 6

Nensi Kardani
Nensi Kardani

Reputation: 2366

You can implement like below

val source= textView.text.toString()
fromHtml(source)

Function:

private fun fromHtml(source:String): Spanned {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
    {
        return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY)
    }
    else
    {
        return Html.fromHtml(source)
    }
}

Upvotes: 0

Philippe Banwarth
Philippe Banwarth

Reputation: 17755

You are actually trying to do a round trip conversion, from spanned to html to spanned. The toHtml() part is not necessary. Something like :

private fun fromHtml(source:String): Spanned {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
    {
        return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY)
    }
    else
    {
        return Html.fromHtml(source)
    }
}

should be sufficient, at least for the example you provided. It takes a String containing HTML, and return a Spanned, suitable for use in a TextView

Upvotes: 0

Sreeram Nair
Sreeram Nair

Reputation: 2397

  1. Try using Spannable like this:

    Spannable WordtoSpan = new SpannableString("Text To Span"); WordtoSpan.setSpan(new BackgroundColorSpan(Color.BLUE), 0, 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

    This will make the background color of Text Blue.

  2. You can also use the HtmlCompat.fromHtml to cast the HTML content

    HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY);

  3. Using TextView#setText()

    // strings.xml: <string name="what_the_html"><b>What</b> <i>the</i> <u>Html</u></string> // Activity.java: textView.setText(R.string.what_the_html);

  4. Using the WebView and its loadDataWithBaseURL method. Try something like this:

    String str="<span style=\"background-color: #FF8983\">Highlighted string</span> "; webView.loadDataWithBaseURL(null, str, "text/html", "utf-8", null);

Using the WebView instead of the textview is the best to do in my opinion.

Hope this helps.

Upvotes: 0

BlackHatSamurai
BlackHatSamurai

Reputation: 23503

You can't cast because SpannableStringBuilder does not inherit from String, per docs. If you want to cast to SpannableStringBuilder you would need to pass the string as a CharacterSequence; or, something like StringBuilder, which is a subclass of CharacterSequence.

Upvotes: 0

Related Questions