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.