fix images sometimes flickering when interacting with a post (#4971)

Glide compares the previous load with the current load, and if they are
identical, does basically nothing. Thats what we want, because otherwise
it flickers when the requested image is not in the memory cache.
The problem is, we decode the blurhash we use as placeholder everytime.
And the BitmapDrawables we get don't have a proper equals
implementation.
So Glide is like: Aha, different placeholder, better load again ->
Flicker
I added a BlurhashDrawable with custom equals/hashCode and now the
flickering is gone.
This commit is contained in:
Konrad Pozniak 2025-03-05 19:53:53 +01:00 committed by GitHub
commit 3c728d9bea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 63 additions and 14 deletions

View file

@ -54,6 +54,7 @@ import com.keylesspalace.tusky.entity.Translation;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
import com.keylesspalace.tusky.util.AttachmentHelper;
import com.keylesspalace.tusky.util.BlurhashDrawable;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CompositeWithOpaqueBackground;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
@ -473,7 +474,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
private BitmapDrawable decodeBlurHash(String blurhash) {
return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash);
return new BlurhashDrawable(this.avatar.getContext(), blurhash);
}
private void loadImage(MediaPreviewImageView imageView,

View file

@ -17,7 +17,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.decodeBlurHash
import com.keylesspalace.tusky.util.BlurhashDrawable
import com.keylesspalace.tusky.util.getFormattedDescription
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
@ -86,7 +86,7 @@ class AccountMediaGridAdapter(
val blurhash = item.attachment.blurhash
val placeholder = if (useBlurhash && blurhash != null) {
decodeBlurHash(context, blurhash)
BlurhashDrawable(context, blurhash)
} else {
null
}

View file

@ -26,9 +26,9 @@ import com.keylesspalace.tusky.entity.StatusEdit
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.BlurhashDrawable
import com.keylesspalace.tusky.util.TuskyTagHandler
import com.keylesspalace.tusky.util.aspectRatios
import com.keylesspalace.tusky.util.decodeBlurHash
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.parseAsMastodonHtml
@ -186,7 +186,7 @@ class ViewEditsAdapter(
val blurhash = attachment.blurhash
val placeholder: Drawable = if (blurhash != null && useBlurhash) {
decodeBlurHash(context, blurhash)
BlurhashDrawable(context, blurhash)
} else {
ColorDrawable(MaterialColors.getColor(imageView, R.attr.colorBackgroundAccent))
}

View file

@ -0,0 +1,39 @@
/* Copyright 2025 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.drawable.BitmapDrawable
/**
* Drawable to display blurhashes with custom equals and hashCode implementation.
* This is so Glide does not flicker unnecessarily when it is used with blurhashes as placeholder.
*/
class BlurhashDrawable(
context: Context,
val blurhash: String
) : BitmapDrawable(
context.resources,
BlurHashDecoder.decode(blurhash, 32, 32, 1f)
) {
override fun equals(other: Any?): Boolean {
return (other as? BlurhashDrawable)?.blurhash == blurhash
}
override fun hashCode(): Int {
return blurhash.hashCode()
}
}

View file

@ -1,10 +1,23 @@
/* Copyright 2025 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("ImageLoadingHelper")
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.widget.ImageView
import androidx.annotation.Px
import com.bumptech.glide.Glide
@ -52,7 +65,3 @@ fun loadAvatar(
}
}
}
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
}

View file

@ -115,7 +115,7 @@ class StatusViewHelper(private val itemView: View) {
.into(mediaPreviews[i])
} else {
val placeholder = if (attachment.blurhash != null) {
decodeBlurHash(context, attachment.blurhash)
BlurhashDrawable(context, attachment.blurhash)
} else {
mediaPreviewUnloaded
}
@ -143,8 +143,8 @@ class StatusViewHelper(private val itemView: View) {
} else {
mediaPreviews[i].removeFocalPoint()
if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) {
val blurhashBitmap = decodeBlurHash(context, attachment.blurhash)
mediaPreviews[i].setImageDrawable(blurhashBitmap)
val blurhashDrawable = BlurhashDrawable(context, attachment.blurhash)
mediaPreviews[i].setImageDrawable(blurhashDrawable)
} else {
mediaPreviews[i].setImageDrawable(mediaPreviewUnloaded)
}