简体   繁体   中英

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:

小绿广场

Then 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:

无图像文本视图

(because I replaced image tags with an empty text)

Then when the image loads it shows this:

图像正确显示

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

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.
            this@MainActivity.runOnUiThread {
                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:

在此处输入图片说明


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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM