Add support for animated images to GlideImage.

Our GlideImage implementation doesn't support animated images, because it loads them as bitmaps and therefore only displays the first image frame as a static image. This change works around that issue by having GlideImage wrap an ImageView to handle cases where we need to display animated images.
This commit is contained in:
Jeffrey Starke
2025-05-12 10:49:23 -04:00
committed by Michelle Tang
parent fb111619d7
commit 288eda5bb1
2 changed files with 100 additions and 17 deletions

View File

@@ -5,8 +5,8 @@
package org.thoughtcrime.securesms.compose
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -16,41 +16,97 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.viewinterop.AndroidView
import com.bumptech.glide.Glide
import com.bumptech.glide.TransitionOptions
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.thoughtcrime.securesms.glide.cache.ApngOptions
/**
* Our very own GlideImage.
* Our very own GlideImage. The GlideImage composable provided by the bumptech library is not suitable because it was is using our encrypted cache decoder/encoder.
*/
@Composable
fun <T> GlideImage(
model: T?,
modifier: Modifier = Modifier,
model: T?,
imageSize: DpSize? = null,
scaleType: GlideImageScaleType = GlideImageScaleType.FIT_CENTER,
fallback: Drawable? = null,
error: Drawable? = fallback,
transition: TransitionOptions<*, Drawable>? = null,
diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL,
enableApngAnimation: Boolean = false
) {
if (enableApngAnimation) {
val density = LocalDensity.current
AndroidView(
factory = { context -> ImageView(context) },
update = { imageView ->
Glide.with(imageView.context)
.load(model)
.fallback(fallback)
.error(error)
.diskCacheStrategy(diskCacheStrategy)
.set(ApngOptions.ANIMATE, enableApngAnimation)
.apply {
scaleType.applyTo(this)
transition?.let(this::transition)
if (imageSize != null) {
with(density) {
this@apply.override(imageSize.width.toPx().toInt(), imageSize.height.toPx().toInt())
}
}
}
.into(imageView)
},
modifier = modifier
)
} else {
GlideImage(
model = model,
imageSize = imageSize,
scaleType = scaleType,
fallback = fallback,
error = error,
transition = transition,
diskCacheStrategy = diskCacheStrategy,
modifier = modifier
)
}
}
@Composable
private fun <T> GlideImage(
modifier: Modifier = Modifier,
model: T?,
imageSize: DpSize? = null,
scaleType: GlideImageScaleType = GlideImageScaleType.FIT_CENTER,
fallback: Drawable? = null,
error: Drawable? = fallback,
transition: TransitionOptions<*, Drawable>? = null,
diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL
) {
var bitmap by remember {
mutableStateOf<ImageBitmap?>(null)
var drawable by remember {
mutableStateOf<Drawable?>(null)
}
val target = remember {
object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
bitmap = resource.asImageBitmap()
object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
drawable = resource
}
override fun onLoadCleared(placeholder: Drawable?) {
bitmap = null
drawable = null
}
}
}
@@ -59,12 +115,14 @@ fun <T> GlideImage(
val context = LocalContext.current
DisposableEffect(model, fallback, error, diskCacheStrategy, density, imageSize) {
val builder = Glide.with(context)
.asBitmap()
.load(model)
.fallback(fallback)
.error(error)
.diskCacheStrategy(diskCacheStrategy)
.fitCenter()
.apply {
scaleType.applyTo(this)
transition?.let(this::transition)
}
if (imageSize != null) {
with(density) {
@@ -77,18 +135,40 @@ fun <T> GlideImage(
object : DisposableEffectResult {
override fun dispose() {
Glide.with(context).clear(target)
bitmap = null
drawable = null
}
}
}
val bm = bitmap
if (bm != null) {
if (drawable != null) {
Image(
bitmap = bm,
painter = rememberDrawablePainter(drawable),
contentDescription = null,
contentScale = if (model == null) ContentScale.Inside else ContentScale.Crop,
modifier = modifier
)
}
}
enum class GlideImageScaleType {
/** @see [com.bumptech.glide.request.RequestOptions.fitCenter] */
FIT_CENTER,
/** @see [com.bumptech.glide.request.RequestOptions.centerInside] */
CENTER_INSIDE,
/** @see [com.bumptech.glide.request.RequestOptions.centerCrop] */
CENTER_CROP,
/** @see [com.bumptech.glide.request.RequestOptions.circleCrop] */
CIRCLE_CROP;
fun <TranscodeT> applyTo(builder: com.bumptech.glide.RequestBuilder<TranscodeT>): com.bumptech.glide.RequestBuilder<TranscodeT> {
return when (this) {
FIT_CENTER -> builder.fitCenter()
CENTER_INSIDE -> builder.centerInside()
CENTER_CROP -> builder.centerCrop()
CIRCLE_CROP -> builder.circleCrop()
}
}
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
@@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressSt
import org.thoughtcrime.securesms.compose.GlideImage
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus
import org.thoughtcrime.securesms.util.DeviceProperties
@Composable
fun StickerPackSectionHeader(
@@ -238,6 +240,7 @@ private fun StickerPackInfo(
) {
GlideImage(
model = coverImageUri,
enableApngAnimation = DeviceProperties.shouldAllowApngStickerAnimation(LocalContext.current),
modifier = Modifier
.padding(end = 16.dp)
.size(56.dp)