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.
[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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With