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 package org.thoughtcrime.securesms.compose
import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -16,41 +16,97 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.viewinterop.AndroidView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.TransitionOptions
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition 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 @Composable
fun <T> GlideImage( fun <T> GlideImage(
model: T?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
model: T?,
imageSize: DpSize? = null, imageSize: DpSize? = null,
scaleType: GlideImageScaleType = GlideImageScaleType.FIT_CENTER,
fallback: Drawable? = null, fallback: Drawable? = null,
error: Drawable? = fallback, 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 diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL
) { ) {
var bitmap by remember { var drawable by remember {
mutableStateOf<ImageBitmap?>(null) mutableStateOf<Drawable?>(null)
} }
val target = remember { val target = remember {
object : CustomTarget<Bitmap>() { object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
bitmap = resource.asImageBitmap() drawable = resource
} }
override fun onLoadCleared(placeholder: Drawable?) { override fun onLoadCleared(placeholder: Drawable?) {
bitmap = null drawable = null
} }
} }
} }
@@ -59,12 +115,14 @@ fun <T> GlideImage(
val context = LocalContext.current val context = LocalContext.current
DisposableEffect(model, fallback, error, diskCacheStrategy, density, imageSize) { DisposableEffect(model, fallback, error, diskCacheStrategy, density, imageSize) {
val builder = Glide.with(context) val builder = Glide.with(context)
.asBitmap()
.load(model) .load(model)
.fallback(fallback) .fallback(fallback)
.error(error) .error(error)
.diskCacheStrategy(diskCacheStrategy) .diskCacheStrategy(diskCacheStrategy)
.fitCenter() .apply {
scaleType.applyTo(this)
transition?.let(this::transition)
}
if (imageSize != null) { if (imageSize != null) {
with(density) { with(density) {
@@ -77,18 +135,40 @@ fun <T> GlideImage(
object : DisposableEffectResult { object : DisposableEffectResult {
override fun dispose() { override fun dispose() {
Glide.with(context).clear(target) Glide.with(context).clear(target)
bitmap = null drawable = null
} }
} }
} }
val bm = bitmap if (drawable != null) {
if (bm != null) {
Image( Image(
bitmap = bm, painter = rememberDrawablePainter(drawable),
contentDescription = null, contentDescription = null,
contentScale = if (model == null) ContentScale.Inside else ContentScale.Crop, contentScale = if (model == null) ContentScale.Inside else ContentScale.Crop,
modifier = modifier 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp 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.compose.GlideImage
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus
import org.thoughtcrime.securesms.util.DeviceProperties
@Composable @Composable
fun StickerPackSectionHeader( fun StickerPackSectionHeader(
@@ -238,6 +240,7 @@ private fun StickerPackInfo(
) { ) {
GlideImage( GlideImage(
model = coverImageUri, model = coverImageUri,
enableApngAnimation = DeviceProperties.shouldAllowApngStickerAnimation(LocalContext.current),
modifier = Modifier modifier = Modifier
.padding(end = 16.dp) .padding(end = 16.dp)
.size(56.dp) .size(56.dp)