How to Display HTML in Textview along with Images in Android?

19 Oct, 2021
There are some situations when one needs to display rich formatted text in the app, like for a blog app or an app like Quora but Android’s inbuilt function doesn’t allow to display inline images by default, moreover they show the ugly blue line to the left of anything inside blockquote tag. Here is the simple solution to show HTML in TextView along with images in Android. Note that we are going to implement this project using Kotlin language in Android. Below is a demo screenshot of the app.
Basic knowledge of the Coroutines library.
Basic knowledge of Picasso library.
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Kotlin as the programming language.
Add some colors in the colors.xml file. The Colors here are for the blockquote Styling. You are free to choose different colors.
<?xml version="1.0" encoding="utf-8"?><resources>    <color name="colorPrimary">#0F9D58</color>    <color name="colorPrimaryDark">#0F9D58</color>    <color name="colorAccent">#03DAC5</color>    <color name="Grey">#878585</color></resources>
Go to build.gradle(Module:app) file and add the following dependency.

// Picasso library to downloading images 
implementation 'com.squareup.picasso:picasso:2.71828'
// Coroutines dependency to put the downloading 
process in background thread implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'


In the following activity_main.xml file we have added the following widgets:
EditText where the user will input the HTML text,
Button to trigger an event to display HTML text,
ScrollView for smooth scrolling,
TextView to display the HTML after processing the input.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:padding="16dp"    tools:context=".MainActivity">        <!-- Edittext to take input for this sample app,          for real app you will not need this -->    <EditText        android:id="@+id/editor"        android:layout_width="0dp"        android:layout_height="40dp"        app:layout_constraintEnd_toStartOf="@+id/display_html"        app:layout_constraintHorizontal_bias="0.0"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent" />      <!-- Button to trigger an event to display html/Rich text-->    <Button        android:id="@+id/display_html"        android:layout_width="100dp"        android:layout_height="50dp"        android:text="Display Html"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintTop_toTopOf="parent" />      <!-- Scroll View for smooth scrolling -->    <ScrollView        android:layout_width="0dp"        android:layout_height="0dp"        android:layout_marginTop="16dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toBottomOf="@+id/editor">          <!-- Text View in which you will display the html after              processing the input-->        <TextView            android:id="@+id/html_viewer"            android:layout_width="match_parent"            android:layout_height="match_parent">        </TextView>              </ScrollView>  </androidx.constraintlayout.widget.ConstraintLayout>
Output UI:
Create a class that will download images contained in the img tag. Below is the complete ImageGetter.kt file. Understand the complete code by referring to the corresponding comments inside the code. 
ImageGetter.kt
import android.content.res.Resourcesimport android.graphics.Bitmapimport android.graphics.Canvasimport android.graphics.drawable.BitmapDrawableimport android.graphics.drawable.Drawableimport android.text.Htmlimport android.widget.TextViewimport com.squareup.picasso.Picassoimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.GlobalScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContext  // Class to download Images which extends [Html.ImageGetter]class ImageGetter(    private val res: Resources,    private val htmlTextView: TextView) : Html.ImageGetter {      // Function needs to overridden when extending [Html.ImageGetter] ,    // which will download the image    override fun getDrawable(url: String): Drawable {        val holder = BitmapDrawablePlaceHolder(res, null)          // Coroutine Scope to download image in Background        GlobalScope.launch(Dispatchers.IO) {            runCatching {                  // downloading image in bitmap format using [Picasso] Library                val bitmap = Picasso.get().load(url).get()                val drawable = BitmapDrawable(res, bitmap)                  // To make sure Images don't go out of screen , Setting width less                // than screen width, You can change image size if you want                val width = getScreenWidth() - 150                  // Images may stretch out if you will only resize width,                // hence resize height to according to aspect ratio                val aspectRatio: Float =                    (drawable.intrinsicWidth.toFloat()) / (drawable.intrinsicHeight.toFloat())                val height = width / aspectRatio                drawable.setBounds(10, 20, width, height.toInt())                holder.setDrawable(drawable)                holder.setBounds(10, 20, width, height.toInt())                withContext(Dispatchers.Main) {                    htmlTextView.text = htmlTextView.text                }            }        }        return holder    }      // Actually Putting images    internal class BitmapDrawablePlaceHolder(res: Resources, bitmap: Bitmap?) :        BitmapDrawable(res, bitmap) {        private var drawable: Drawable? = null          override fun draw(canvas: Canvas) {            drawable?.run { draw(canvas) }        }          fun setDrawable(drawable: Drawable) {            this.drawable = drawable        }    }      // Function to get screenWidth used above    fun getScreenWidth() =        Resources.getSystem().displayMetrics.widthPixels}
Note: One can change the height and the width of the image according to your will in the getDrawable() function.
Create a click-Listener for the button inside the onCreate() method.
MainActivity.kt
class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)          // Click Listener to listener button click events.        // display_html is button id from activity_main.xml         // You don't need to get reference to it because kotlin         // provides synthetic import and         // We are gonna use for the same for rest of Views        display_html.setOnClickListener {            if (editor.text.isNotEmpty()) {                displayHtml(editor.text.toString())            }        }    }}
Create a function displayHtml() which you just called inside click listener.
Note: Kotlin has Synthetic import ,so there was no need of storing reference to views in a variable and we are going to do the same for all other views.
Kotlin
private fun displayHtml(html: String) {          // Creating object of ImageGetter class you just created      val imageGetter = ImageGetter(resources, html_viewer)             // Using Html framework to parse html      val styledText=HtmlCompat.fromHtml(html,                                          HtmlCompat.FROM_HTML_MODE_LEGACY,                                         imageGetter,null)               // to enable image/link clicking      html_viewer.movementMethod = LinkMovementMethod.getInstance()              // setting the text after formatting html and downloading and setting images      html_viewer.text = styledText  }
So now the Image work is done, now it should load the images. Below is the complete code for the MainActivity.kt file.
Kotlin
import android.os.Bundleimport android.text.method.LinkMovementMethodimport androidx.appcompat.app.AppCompatActivityimport androidx.core.text.HtmlCompatimport kotlinx.android.synthetic.main.activity_main.*  class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)          // Click Listener to listener button click events.        // display_html is button id from activity_main.xml         // You don't need to get reference to it         // because kotlin provides synthetic import and         // We are gonna use for the same for rest of Views        display_html.setOnClickListener {            if (editor.text.isNotEmpty()) {                displayHtml(editor.text.toString())            }        }    }      private fun displayHtml(html: String) {        // Creating object of ImageGetter class you just created        val imageGetter = ImageGetter(resources, html_viewer)          // Using Html framework to parse html        val styledText =            HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY, imageGetter,null)          // to enable image/link clicking        html_viewer.movementMethod = LinkMovementMethod.getInstance()          // setting the text after formatting html and downloading and setting images        html_viewer.text = styledText    }}
Let’s see how it’s working now. Use HTML of another webpage. Here we have used the following webpage of the GFG article. Get the HTML string file from here.
Note: Remember that input the HTML text inside the EditText and then click on the “DISPLAY HTML” Button
The images are now loading but you can see, for the blockquote part, there is the plain ugly blue line. We are now gonna fix that. 
Create a Kotlin Class named QuoteSpanClass and add this following code to this file.
QuoteSpanClass.kt
import android.graphics.Canvasimport android.graphics.Paintimport android.text.Layoutimport android.text.style.LeadingMarginSpanimport android.text.style.LineBackgroundSpan  class QuoteSpanClass(    private val backgroundColor: Int,    private val stripeColor: Int,    private val stripeWidth: Float,    private val gap: Float) : LeadingMarginSpan, LineBackgroundSpan {          // Margin for the block quote tag    override fun getLeadingMargin(first: Boolean): Int {        return (stripeWidth + gap).toInt()    }      // this function draws the margin.    override fun drawLeadingMargin(        c: Canvas,        p: Paint,        x: Int,        dir: Int,        top: Int,        baseline: Int,        bottom: Int,        text: CharSequence,        start: Int,        end: Int,        first: Boolean,        layout: Layout    ) {          val style = p.style        val paintColor = p.color        p.style = Paint.Style.FILL        p.color = stripeColor          // Creating margin according to color and stripewidth it recieves        // Press CTRL+Q on function name to read more        c.drawRect(x.toFloat(), top.toFloat(), x + dir * stripeWidth, bottom.toFloat(), p)        p.style = style        p.color = paintColor    }      override fun drawBackground(        c: Canvas,        p: Paint,        left: Int,        right: Int,        top: Int,        baseline: Int,        bottom: Int,        text: CharSequence,        start: Int,        end: Int,        lnum: Int    ) {        val paintColor = p.color        p.color = backgroundColor          // It draws the background on which blockquote text is written        c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), p)        p.color = paintColor    }}
Since the QuoteSpanClass is created, it’s time to use this class in MainActivity.kt, but before you need to create a function to parse blockquote tags and draw the margin and background using the QuoteSpanClass.
MainActivity.kt
private fun replaceQuoteSpans(spannable: Spannable) {  val quoteSpans: Array<QuoteSpan> =       spannable.getSpans(0, spannable.length - 1, QuoteSpan::class.java)    for (quoteSpan in quoteSpans)   {    val start: Int = spannable.getSpanStart(quoteSpan)    val end: Int = spannable.getSpanEnd(quoteSpan)    val flags: Int = spannable.getSpanFlags(quoteSpan)    spannable.removeSpan(quoteSpan)       spannable.setSpan(               QuoteSpanClass(                    // background color                    ContextCompat.getColor(this, R.color.colorPrimary),                      // strip color                    ContextCompat.getColor(this, R.color.colorAccent),                    // strip width                    10F, 50F                ),                start, end, flags            )        }     }
And finally, call this function from displayHtml() function before setting the text for html_viewer. Below is the complete code for the MainActivity.kt file.
Kotlin
import android.os.Bundleimport android.text.Spannableimport android.text.method.LinkMovementMethodimport android.text.style.QuoteSpanimport androidx.appcompat.app.AppCompatActivityimport androidx.core.content.ContextCompatimport androidx.core.text.HtmlCompatimport kotlinx.android.synthetic.main.activity_main.*  class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)          // Click Listener to listener button click events.        // display_html is button id from activity_main.xml         // You don't need to get reference to it        // because kotlin provides synthetic import and        // We are gonna use for the same for rest of Views        display_html.setOnClickListener {            if (editor.text.isNotEmpty()) {                displayHtml(editor.text.toString())            }        }    }      private fun displayHtml(html: String) {        // Creating object of ImageGetter class you just created        val imageGetter = ImageGetter(resources, html_viewer)          // Using Html framework to parse html        val styledText =            HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY, imageGetter,null)          replaceQuoteSpans(styledText as Spannable)          // setting the text after formatting html and downloading and setting images        html_viewer.text = styledText          // to enable image/link clicking        html_viewer.movementMethod = LinkMovementMethod.getInstance()      }      private fun replaceQuoteSpans(spannable: Spannable)    {        val quoteSpans: Array<QuoteSpan> =            spannable.getSpans(0, spannable.length - 1, QuoteSpan::class.java)          for (quoteSpan in quoteSpans)        {            val start: Int = spannable.getSpanStart(quoteSpan)            val end: Int = spannable.getSpanEnd(quoteSpan)            val flags: Int = spannable.getSpanFlags(quoteSpan)            spannable.removeSpan(quoteSpan)            spannable.setSpan(                QuoteSpanClass(                    // background color                    ContextCompat.getColor(this, R.color.colorPrimary),                    // strip color                    ContextCompat.getColor(this, R.color.colorAccent),                    // strip width                    10F, 50F                ),                start, end, flags            )        }    }}
Now let’s see the changes in the output screen.
Note: One can change strip Color, background color, strip width of your choice in replaceQuoteSpans function, and beautify it.
But there is still one problem. What if you click an image? Nothing happens as of now. You need to add a few more lines to handle that.
Kotlin
// Function to parse image tags and enable click eventsfun ImageClick(html: Spannable) {  for (span in html.getSpans(0, html.length, ImageSpan::class.java))  {    val flags = html.getSpanFlags(span)    val start = html.getSpanStart(span)    val end = html.getSpanEnd(span)    html.setSpan(object : URLSpan(span.source) {         override fun onClick(v: View) {                Log.d(TAG, "onClick: url is ${span.source}")            }  }, start, end, flags)}
And then call this function from displayHtml() Function before setting the text for html_viewer. Below is the complete code for the MainActivity.kt file. 
Kotlin
import android.os.Bundleimport android.text.Spannableimport android.text.method.LinkMovementMethodimport android.text.style.ImageSpanimport android.text.style.QuoteSpanimport android.text.style.URLSpanimport android.util.Logimport android.view.Viewimport androidx.appcompat.app.AppCompatActivityimport androidx.core.content.ContextCompatimport androidx.core.text.HtmlCompatimport kotlinx.android.synthetic.main.activity_main.*  private const val TAG="MainActivity"  class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)          // Click Listener to listener button click events.        // display_html is button id from activity_main.xml         // You don't need to get reference to it        // because kotlin provides synthetic import and        // We are gonna use for the same for rest of Views        display_html.setOnClickListener {            if (editor.text.isNotEmpty()) {                displayHtml(editor.text.toString())            }        }    }      private fun displayHtml(html: String) {        // Creating object of ImageGetter class you just created        val imageGetter = ImageGetter(resources, html_viewer)          // Using Html framework to parse html        val styledText =            HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY, imageGetter, null)          replaceQuoteSpans(styledText as Spannable)        ImageClick(styledText as Spannable)          // setting the text after formatting html and downloading and setting images        html_viewer.text = styledText          // to enable image/link clicking        html_viewer.movementMethod = LinkMovementMethod.getInstance()      }      private fun replaceQuoteSpans(spannable: Spannable) {        val quoteSpans: Array<QuoteSpan> =            spannable.getSpans(0, spannable.length - 1, QuoteSpan::class.java)          for (quoteSpan in quoteSpans) {            val start: Int = spannable.getSpanStart(quoteSpan)            val end: Int = spannable.getSpanEnd(quoteSpan)            val flags: Int = spannable.getSpanFlags(quoteSpan)            spannable.removeSpan(quoteSpan)            spannable.setSpan(                QuoteSpanClass(                    // background color                    ContextCompat.getColor(this, R.color.colorPrimary),                    // strip color                    ContextCompat.getColor(this, R.color.colorAccent),                    // strip width                    10F, 50F                ),                start, end, flags            )        }    }      // Function to parse image tags and enable click events    fun ImageClick(html: Spannable) {        for (span in html.getSpans(0, html.length, ImageSpan::class.java)) {            val flags = html.getSpanFlags(span)            val start = html.getSpanStart(span)            val end = html.getSpanEnd(span)            html.setSpan(object : URLSpan(span.source) {                override fun onClick(v: View) {                    Log.d(TAG, "onClick: url is ${span.source}")                }            }, start, end, flags)        }    }}
Now if you click on any Image and check Logcat you will see the URL of the image is being logged. From there you can trigger some function with that URL.
Resource: Get the complete project file here.
anikaseth98
anikakapoor
abhishek0719kadiyan
ruhelaa48
kashishsoda
android
Android
Kotlin
Android
Writing code in comment?
Please use ide.geeksforgeeks.org,
generate link and share the link here.