From 3c728d9bea340c7ebad76b3b3f7eff18783c0829 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 5 Mar 2025 19:53:53 +0100 Subject: [PATCH] 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. --- .../tusky/adapter/StatusBaseViewHolder.java | 3 +- .../account/media/AccountMediaGridAdapter.kt | 4 +- .../viewthread/edits/ViewEditsAdapter.kt | 4 +- .../tusky/util/BlurhashDrawable.kt | 39 +++++++++++++++++++ .../tusky/util/ImageLoadingHelper.kt | 21 +++++++--- .../tusky/util/StatusViewHelper.kt | 6 +-- 6 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BlurhashDrawable.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 3117b3a94..c7068fe54 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -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, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt index 613546491..6fc33f23a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt @@ -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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt index ca01bc6c4..aef461e15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt @@ -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)) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BlurhashDrawable.kt b/app/src/main/java/com/keylesspalace/tusky/util/BlurhashDrawable.kt new file mode 100644 index 000000000..4c6fe9a2e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BlurhashDrawable.kt @@ -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 . */ + +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() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt index 0979f5964..641c78fdd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt @@ -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 . */ + @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)) -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index c9f213de2..6210bba76 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -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) }