25

I have a gif that I would like to place into my app. I know how to insert image resources, but when I try adding the gif it becomes a static image.

DrawImage(image = +imageResource(R.drawable.gif)) 

Has anyone tried adding a gif into Jetpack Compose, as struggling to find docs online as to how?

5
  • 3
    Quick question it's usually these which are the hardest to answer :) Commented Feb 14, 2020 at 15:58
  • @a_local_nobody Annoyingly does seem to be the case a lot of the time :) Commented Feb 14, 2020 at 16:01
  • i haven't worked with compose myself (yet) but i have a feeling this might be a very good question as it might not even be possible, hence my comment (and my upvote). this is all irrelevant conversation (someone will probably flag and remove this, as they should) but i hope you find an answer :) rare to see interesting questions these days, sadly Commented Feb 14, 2020 at 16:06
  • I've only just started using it myself. Experimenting with how it all works compared to the old way of doing things. If I figure out a solution I'll be sure to add it here (if its not already removed) Commented Feb 14, 2020 at 16:12
  • 1
    Do GIFs animate anywhere in the stock Android SDK View system? At least for the first several years of Android's existence, ImageView would not animate a GIF, for example. Developers wound up using WebView or Movie (IIRC) until a bunch of animated-GIF-capable rendering libraries became available. Your question suggests that you expect animation, but is that a reasonable expectation? Commented Feb 14, 2020 at 22:05

6 Answers 6

30

Most answers here are outdated. This is the way to do it now, as of coil 2.1.0

An updated version of Hoby's answer.

implementation "io.coil-kt:coil-compose:2.1.0" implementation "io.coil-kt:coil-gif:2.1.0" 
@Composable fun GifImage( modifier: Modifier = Modifier, ) { val context = LocalContext.current val imageLoader = ImageLoader.Builder(context) .components { if (SDK_INT >= 28) { add(ImageDecoderDecoder.Factory()) } else { add(GifDecoder.Factory()) } } .build() Image( painter = rememberAsyncImagePainter( ImageRequest.Builder(context).data(data = R.drawable.YOUR_GIF_HERE).apply(block = { size(Size.ORIGINAL) }).build(), imageLoader = imageLoader ), contentDescription = null, modifier = modifier.fillMaxWidth(), ) } 
Sign up to request clarification or add additional context in comments.

5 Comments

note that as of Semptember 2023, the latest stable version is 2.4.0
My GIF file is not in my drawables but is in my app's data folder. I have a path to it as /storage/emulated/0/Android/data/com.example.messageapp/files/abW93.gif. How can I use it in the above code?
I'd take a look at the coil docs: coil-kt.github.io/coil/image_requests. There's likely a way to generate an image request for a local file
Thanks, worked like a charm - just added the file path as the data parameter.
Use "AnimatedImageDecoder" instead of "ImageDecoderDecoder".
7

Starting from coil 1.3.0 gif supported is added to Jetpack Compose's version of Coil. So you can use existing coil docs for supporting gif decoding.

TL;DR

Add the following libraries to gradle:

implementation("io.coil-kt:coil:2.0.0-rc02") implementation("io.coil-kt:coil-gif:2.0.0-rc02") implementation("io.coil-kt:coil-compose:2.0.0-rc02") 

Gif set up code:

// Create an ImageLoader val imgLoader = ImageLoader.invoke(context).newBuilder() .componentRegistry { if (SDK_INT >= 28) { add(ImageDecoderDecoder(context)) } else { add(GifDecoder()) } }.build() // Use in Image Image( painter = rememberImagePainter(data = R.drawable.YOURBESTGIF, imageLoader = imgLoader), ... ) 

1 Comment

Can we define the number of cycles which gif shoud show and stop? Or it will show infinitely ?
1

I was able to display an animated GIF in Compose 0.1.0-dev16 using this code (taken from https://github.com/luca992/coil-composable/blob/master/coil-composable/src/androidMain/kotlin/com/luca992/compose/image/CoilImage.kt and modified):

import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.os.Build.VERSION.SDK_INT import androidx.annotation.Px import androidx.compose.foundation.Image import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.WithConstraints import androidx.compose.ui.geometry.Size.Companion.Zero import androidx.compose.ui.graphics.ImageAsset import androidx.compose.ui.graphics.asImageAsset import androidx.compose.ui.platform.ContextAmbient import androidx.compose.ui.unit.Constraints.Companion.Infinity import androidx.core.graphics.drawable.toBitmap import androidx.ui.tooling.preview.Preview import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.request.CachePolicy import coil.request.LoadRequest import coil.request.LoadRequestBuilder import coil.size.Scale import coil.target.Target import kotlinx.coroutines.* @Composable fun CoilImage( model: Any, modifier : Modifier = Modifier, customize: LoadRequestBuilder.() -> Unit = {} ) { WithConstraints(modifier) { var width = if (constraints.maxWidth > Zero.width && constraints.maxWidth < Infinity) { constraints.maxWidth } else { -1 } var height = if (constraints.maxHeight > Zero.height && constraints.maxHeight < Infinity) { constraints.maxHeight } else { -1 } //if height xor width not able to be determined, make image a square of the determined dimension if (width == -1) width = height if (height == -1) height = width val image = state<ImageAsset> { ImageAsset(width,height) } val context = ContextAmbient.current var animationJob : Job? = remember { null } onCommit(model) { val target = object : Target { override fun onStart(placeholder: Drawable?) { placeholder?.apply { animationJob?.cancel() if(height != -1 && width != -1) { animationJob = image.update(this, width, height) } else if (height == -1) { val scaledHeight = intrinsicHeight * (width / intrinsicWidth ) animationJob = image.update(this, width, scaledHeight) } else if (width == -1) { val scaledWidth = intrinsicWidth * (height / intrinsicHeight) animationJob = image.update(this, scaledWidth, height) } } } override fun onSuccess(result: Drawable) { animationJob?.cancel() animationJob = image.update(result) } override fun onError(error: Drawable?) { error?.run { animationJob?.cancel() animationJob = image.update(error) } } } val loader = ImageLoader.Builder(context) .componentRegistry { if (SDK_INT >= 28) { add(ImageDecoderDecoder()) } else { add(GifDecoder()) } }.build() val request = LoadRequest.Builder(context) .data(model) .size(width, height) .scale(Scale.FILL) .diskCachePolicy(CachePolicy.ENABLED) .apply{customize(this)} .target(target) val requestDisposable = loader.execute(request.build()) onDispose { image.value = ImageAsset(width,height) requestDisposable.dispose() animationJob?.cancel() } } Image(modifier = modifier, asset = image.value) } } internal fun MutableState<ImageAsset>.update(drawable: Drawable, @Px width: Int? = null, @Px height: Int? = null) : Job? { if (drawable is Animatable) { (drawable as Animatable).start() return GlobalScope.launch(Dispatchers.Default) { while (true) { val asset = drawable.toBitmap( width = width ?: drawable.intrinsicWidth, height = height ?: drawable.intrinsicHeight) .asImageAsset() withContext(Dispatchers.Main) { value = asset } delay(16) //1000 ms / 60 fps = 16.666 ms/fps //TODO: figure out most efficient way to dispaly a gif } } } else { value = drawable.toBitmap( width = width ?: drawable.intrinsicWidth, height = height ?: drawable.intrinsicHeight) .asImageAsset() return null } } 

This depends on Coil:

implementation 'io.coil-kt:coil:0.11.0' implementation 'io.coil-kt:coil-gif:0.11.0' 

Use as follows:

setContent { CoilImage("https://example.com/image.gif") } 

Comments

1

This can easily be done using coil as following

@Composable fun GifImage( modifier: Modifier = Modifier, imageID: Int ){ val context = LocalContext.current val imageLoader = ImageLoader.Builder(context) .componentRegistry { if (SDK_INT >= 28) { add(ImageDecoderDecoder(context)) } else { add(GifDecoder()) } } .build() Image( painter = rememberImagePainter( imageLoader = imageLoader, data = imageID, builder = { size(OriginalSize) } ), contentDescription = null, modifier = modifier ) } 

using the following dependencies

implementation "io.coil-kt:coil-compose:1.4.0" implementation "io.coil-kt:coil-gif:1.4.0" 

Comments

1

With coil 3.3.0 (or maybe for all coil 3 and up) adding coil-gif extension library is enough

implementation("io.coil-kt.coil3:coil-gif:3.3.0")

ImageLoaders will automatically detect gif by file headers and decode it correctly. AsyncImage will work without any effort.

Also, manually adding the decoder to component registry is possible. For reference: coil gif reference

Comments

0

I adopted this solution here: https://github.com/jaredsburrows/android-gif-example/commit/5690523c6dc40c435c3d81868d89ba26d21e3663 since it was removed from Accompanist here.

Code:

class ImageService @Inject constructor(@ApplicationContext private val context: Context) { /** Compose views */ fun loadGif( imageUrl: String, thumbnailUrl: String, onResourceReady: (GifDrawable?) -> Unit, onLoadFailed: () -> Unit, ) { loadGif(imageUrl) .override(SIZE_ORIGINAL, SIZE_ORIGINAL) .thumbnail(loadGif(thumbnailUrl)) .into(object : CustomTarget<GifDrawable>() { override fun onLoadFailed(errorDrawable: Drawable?) { super.onLoadFailed(errorDrawable) onLoadFailed.invoke() } override fun onLoadCleared(placeholder: Drawable?) { onLoadFailed.invoke() } override fun onResourceReady( resource: GifDrawable, transition: Transition<in GifDrawable>?, ) { onResourceReady.invoke(resource) } }) } /** ImageViews */ fun loadGif( imageUrl: String, thumbnailUrl: String, imageView: ImageView, onResourceReady: () -> Unit, onLoadFailed: (GlideException?) -> Unit, ) { loadGif(imageUrl) .override(SIZE_ORIGINAL, SIZE_ORIGINAL) .thumbnail(loadGif(thumbnailUrl)) .listener( object : RequestListener<GifDrawable> { override fun onResourceReady( resource: GifDrawable?, model: Any?, target: Target<GifDrawable>?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { onResourceReady.invoke() return false } override fun onLoadFailed( e: GlideException?, model: Any?, target: Target<GifDrawable>?, isFirstResource: Boolean ): Boolean { onLoadFailed.invoke(e) return false } } ) .into(imageView) .clearOnDetach() } private fun loadGif(imageUrl: String): RequestBuilder<GifDrawable> { return GlideApp.with(context) .asGif() .transition(withCrossFade()) .load(imageUrl) } } 

See the code here: https://github.com/jaredsburrows/android-gif-example/blob/52914cd63b528b3a9365df6bfa2134ffdfa0e0d7/app/src/main/java/com/burrowsapps/example/gif/data/ImageService.kt#L22

Usage:

 composeView.setContent { val showProgressBar = remember { mutableStateOf(true) } val state = remember { mutableStateOf<GifDrawable?>(null) } GifTheme { // Load images - 'tinyGifPreviewUrl' -> 'tinyGifUrl' imageService.loadGif( imageUrl = imageInfoModel.tinyGifUrl, thumbnailUrl = imageInfoModel.tinyGifPreviewUrl, onResourceReady = { resource -> showProgressBar.value = false state.value = resource }, onLoadFailed = { showProgressBar.value = false state.value = null }, ) // Show loading indicator when image is not loaded if (showProgressBar.value) { CircularProgressIndicator( modifier = Modifier .fillMaxWidth() .height(128.dp) .padding(all = 24.dp), ) } else { Image( painter = rememberDrawablePainter(drawable = state.value), contentDescription = stringResource(id = R.string.gif_image), contentScale = ContentScale.Crop, modifier = Modifier .fillMaxWidth() .height(135.dp), ) } } } 

See the code here: https://github.com/jaredsburrows/android-gif-example/blob/52914cd63b528b3a9365df6bfa2134ffdfa0e0d7/app/src/main/java/com/burrowsapps/example/gif/ui/giflist/GifAdapter.kt#L73

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.