Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change the default image (place holder) loaded in a TextView with Spanned via Html.fromHtml(text)

I have a news section on my app that loads some piece of news from my website and some of them contains images, so I load them from the internet. But while the image is not loaded, there is a green square that only disappears when the image loads.

Image not loaded:

Little green square

Then image loaded:

Image loaded

I want to make that green square invisible.

For simplicity, let's pretend I won't even load the images, just want to make the green square invisible without replacing the image tags with an empty text.

Code:

val exampleText =  "Example <br> <img src=\"https://www.w3schools.com/images/w3schools_green.jpg\" alt=\"W3Schools.com\"> <br> Example"
    tv_body.text = fromHtml(exampleText)

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

Is there a way of changing that default image without doing any shenanigans?

My workaround:

What I did to solve this was adapting a custom fromHtml function.

private var drawable: Drawable? = null
fun fromHtml(context: Activity?, tv: TextView?, text: String?) {
    if (TextUtils.isEmpty(text) || context == null || tv == null) return

   //Replace all image tags with an empty text
    val noImageText = text!!.replace("<img.*?>".toRegex(), "") 

        //Set the textview text with the imageless html
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            tv.text = Html.fromHtml(noImageText, Html.FROM_HTML_MODE_LEGACY)
        } else {
            tv.text = Html.fromHtml(noImageText)
        }

        Thread {
            //Creating the imageGetter
            val imageGetter = ImageGetter { url ->
                drawable = getImageFromNetwork(url)

                if (drawable != null) {
                    var w = drawable!!.intrinsicWidth
                    var h = drawable!!.intrinsicHeight
                    // Scaling the width and height
                    if (w < h && h > 0) {
                        val scale = 400.0f / h
                        w = (scale * w).toInt()
                        h = (scale * h).toInt()
                    } else if (w > h && w > 0) {
                        val scale = 1000.0f / w
                        w = (scale * w).toInt()
                        h = (scale * h).toInt()
                    }
                    drawable!!.setBounds(0, 0, w, h)
                } else if (drawable == null) {
                    return@ImageGetter null
                }
                drawable!!
            }


            val textWithImage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY, imageGetter, null)
            } else {
                Html.fromHtml(text, imageGetter, null)
            }

            // update runOnUiThread and change the textview text from the textWithoutImage to the textWithImage
            context.runOnUiThread(Runnable { tv.text = textWithImage })
        }.start()
}

private fun getImageFromNetwork(imageUrl: String): Drawable? {
    var myFileUrl: URL? = null
    var drawable: Drawable? = null
    try {
        myFileUrl = URL(imageUrl)
        val conn = myFileUrl
                .openConnection() as HttpURLConnection
        conn.doInput = true
        conn.connect()
        val `is` = conn.inputStream
        drawable = Drawable.createFromStream(`is`, null)
        `is`.close()
    } catch (e: Exception) {
        e.printStackTrace()
        return null
    }
    return drawable
}

So when I call this

  val exampleText =  "Example <br> <img src=\"https://www.w3schools.com/images/w3schools_green.jpg\" alt=\"W3Schools.com\"> <br> Example"
    fromHtml((activity as NewsActivity?), tv_body, exampleText)

It first shows this:

imageless textview

(because I replaced image tags with an empty text)

Then when the image loads it shows this:

Image properly displayed

I still think making an imageless text is more a workaround than a proper solution, I think there might be something simpler like:

<style name="LIGHT" parent="Theme.AppCompat.DayNight.DarkActionBar">
    <item name="android:placeHolderDefaultImage">@drawable/invisible</item>

So the green square would be an invisible drawable and I wouldn't need to set the html text twice, although I really don't know how to change that default place holder image. Guess I'll stick to the workaround

like image 965
GLAPPs Mobile Avatar asked Oct 14 '22 23:10

GLAPPs Mobile


1 Answers

The placeholder image you are seeing is from com.android.internal.R.drawable.unknown_image and is set if the ImageGetter returns null. From the function startImg() in Html.

private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
    ...
    if (d == null) {
        d = Resources.getSystem().
                getDrawable(com.android.internal.R.drawable.unknown_image);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
    }
    ....
}

So, somewhere in the original code you are returning a null from the ImageGetter. Because the unknown image drawable is hard-coded, it can't be touched through styles or the theme. You may be able to do something with reflection if that is something you want to tackle.

Rather than manipulating the HTML text before or after the downloaded image is available, I suggest that the drawable returned from the ImageGetter be wrapped such that the image can be changed without direct manipulation of the text. Initially, the wrapper will contain the placeholder image and, later, the wrapper will contain the downloaded image when that becomes available.

Here is some sample code showing this technique. The placeholder is a drawable that displays, but it can be anything you want. I use a visible drawable (the default from Html.java but with an "E" for "empty.") to show that it does display and can be changed. You can supply a drawable that is transparent to show nothing.

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var tvBody: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvBody = findViewById(R.id.tv_body)
        val exampleText =
            "Example <br> <img src=\"https://www.w3schools.com/images/w3schools_green.jpg\" alt=\"W3Schools.com\"> <br> Example"
        tvBody.text = fromHtml(exampleText, this)
    }

    private fun fromHtml(html: String?, context: Context): Spanned? {
        // Define the ImageGetter for Html. The default "no image, yet" drawable is
        // R.drawable.placeholder but can be another drawable.
        val imageGetter = Html.ImageGetter { url ->
            val d = ContextCompat.getDrawable(context, R.drawable.placeholder) as BitmapDrawable
            // Simulate a network fetch of the real image we want to display.
            ImageWrapper(d).apply {
                simulateNetworkFetch(context, this, url)
            }
        }
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, imageGetter, null)
        } else {
            Html.fromHtml(html, imageGetter, null)
        }
    }

    private fun simulateNetworkFetch(context: Context, imageWrapper: ImageWrapper, url: String) {
        GlobalScope.launch {
            Log.d("Applog", "Simulating fetch of $url")
            // Just wait for a busy network to get back to us.
            delay(4000)
            // Get the "downloaded" image and place it in our image wrapper.
            val d = ContextCompat.getDrawable(context, R.drawable.downloaded) as BitmapDrawable
            imageWrapper.setBitmapDrawable(d)
            // Force a remeasure/relayout of the TextView with the new image.
            [email protected] {
                tvBody.text = tvBody.text
            }
        }
    }

    // Simple wrapper for a BitmapDrawable.
    private class ImageWrapper(d: BitmapDrawable) : Drawable() {
        private lateinit var mBitMapDrawable: BitmapDrawable

        init {
            setBitmapDrawable(d)
        }

        override fun draw(canvas: Canvas) {
            mBitMapDrawable.draw(canvas)
        }

        override fun setAlpha(alpha: Int) {
        }

        override fun setColorFilter(colorFilter: ColorFilter?) {
        }

        override fun getOpacity(): Int {
            return PixelFormat.OPAQUE
        }

        fun setBitmapDrawable(bitmapDrawable: BitmapDrawable) {
            mBitMapDrawable = bitmapDrawable
            mBitMapDrawable.setBounds(
                0,
                0,
                mBitMapDrawable.intrinsicWidth,
                mBitMapDrawable.intrinsicHeight
            )
            setBounds(mBitMapDrawable.bounds)
        }
    }
}

Here is how it looks in an emulator:

enter image description here


Sample project is here which includes the use of a DrawableWrapper for API 23+ which, IMO, is a little neater. The code above works just as well. Unfortunately, the AppCompat version of DrawableWrapper is restricted.

like image 98
Cheticamp Avatar answered Oct 17 '22 14:10

Cheticamp