5

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?

Adam Lee
  • 436
  • 1
  • 14
  • 49
  • Is the string to be processed just an HTML span like what you show? (`Highlighted string`) Or could the span be embedded in a larger HTML document with other tags? – Cheticamp Jan 08 '20 at 00:12
  • @Cheticamp it is embedded with other tags – Adam Lee Jan 08 '20 at 22:56

5 Answers5

6

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()
    }
}
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
0

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.

BlackHatSamurai
  • 23,275
  • 22
  • 95
  • 156
  • 1
    I tried using StringBuilder, but unfortunately I still got the following error: java.lang.ClassCastException: java.lang.StringBuilder cannot be cast to android.text.SpannableStringBuilder – Adam Lee Jan 06 '20 at 23:36
0
  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.

Sreeram Nair
  • 2,369
  • 12
  • 27
0

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

bwt
  • 17,292
  • 1
  • 42
  • 60
  • You are right. Unfortunately it means that `Html.fromHtml()` (or even `HtmlCompat.fromHtml`) is not a possible solution – bwt Jan 07 '20 at 18:28
  • The only solution I can think of would be to use directly TagSoup with a custom `ContentHandler`, but that would be a lot of work – bwt Jan 08 '20 at 09:50
  • Html.fromHtml() uses TagSoup, from documentation: ```/** * Returns displayable styled text from the provided HTML string with the legacy flags * {@link #FROM_HTML_MODE_LEGACY}. * * @deprecated use {@link #fromHtml(String, int)} instead. */ @Deprecated public static Spanned fromHtml(String source) { return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null); }``` – Evgenii Vorobei May 08 '20 at 12:03
0

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)
    }
}
Nensi Kasundra
  • 1,980
  • 6
  • 21
  • 34