Bitmap / ImageDecoder

ImageDecoder – Animated GIF

The Android P developer preview contains a new ImageDecoder which is quite interesting and can do some useful things. At present it only appears within the Framework so we can only use it on devices running P or later, and I understand that if consists of a chunk of native code and so is unlikely to appear as a support library. However, it is a really nice, powerful yet simple API. In this series we’ll take a look at how to use it, and see some of the cool things that we can achieve with it.

Previously we was the basics of how to use ImageDecoder, but there were no obvious benefits over BitmapFactory. However, we don’t need to dig to far to find some really big benefits. Firstly, and there’s a clue in the class name, ImageDecoder is not just about Bitmap objects, we can also use it to directly decode Drawable objects from image sources using the ImageDecoder.decodeDrawable() static method. This may not seem like a big deal because it’s easy enough to decode a Bitmap and then wrap it inside a BitmapDrawable to achieve much the same thing. But the big deal here is that ImageDecoder is able to decode animated GIF files, and return these inside a new AnimatedImageDrawable type that is also part of the P developer preview.

Using this is simplicity itself:

class AnimatedDrawableFragment : ImageDecoderFragment() {
    
    override val assetName: String = "animated.gif"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                ImageDecoder.decodeDrawable(source).also { drawable ->
                    launch(UI) {
                        image.setImageDrawable(drawable)
                        if (drawable is Animatable2) {
                            drawable.start()
                        }
                    }
                }
            }
        }
    }
}

There’s not an awful lot different here that the previous example where we loaded a static Bitmap. Obviously we’ll have to use a different asset (line 3), but then we just call decodeDrawable(), instead of decodeBitmap() (line 8), and then call setImageDrawable() on the ImageView instead of setImageBitmap().

The only other thing we need to do is to start the animation much as we have to do when we load an AnimatedVectorDrawable. Similarly to AnimatedVectorDrawable, AnimatedImageDrawable implements the Animatable2 interface, and so we can simply check whether the drawable instance implements that interface (line 11), and call start() if it does (line 13). If we run this it looks exactly as we would expect:

While there are third party libraries such as Glide and Picasso 3 which also support Animated GIF, this is the first time we’ve had first class support within the Android framework, and it’s really quick and easy to use.

This is great, but perhaps there are time when we actually want to be a bit selective about if we actually want to open an image. When we’re dealing with an image that we wish to read from either a File or ByteArray there are mechanisms where we can determine the size of the data, and we can put in checks to determine the overall size of the image, we cannot easily determine whether an image is animated. When we are reading from a ContentResolver we don’t even have a vague idea of the overall size. It’s really not good to just blindly load a potentially expensive resource without knowing anything about it.

To aid with this we can add a listener which will get called after ImageDecoder has parsed the image header information (which is proportionally quite small) but before the actual image data itself (the big bit!) is read:

class AnimatedDrawableFragment : ImageDecoderFragment() {

    override val assetName: String = "animated.gif"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                ImageDecoder.decodeDrawable(source, listener).also { drawable ->
                    launch(UI) {
                        image.setImageDrawable(drawable)
                        if (drawable is Animatable2) {
                            drawable.start()
                        }
                    }
                }
            }
        }
    }

    private var listener = ImageDecoder.OnHeaderDecodedListener { _, info, _ ->
        info?.apply {
            println("Decoded Image: ${description()}")
        }
    }

    private fun ImageDecoder.ImageInfo.description(): String =
            "MIME type: $mimeType; size: $size; isAnimated: $isAnimated"
}

The listener is an additional argument to the decodeDrawable() method and is also supported on decodeBitmap(). The OnHeaderDecoderListener interface has a single method named onHeaderDecoded() so it is a Single Abstract Method (SAM) interface, and can be replaced by a lambda in Kotlin. Three parameters are passed in to the lambda: The first is a reference to the ImageDecoder instance itself, the second is an ImageDecoder.ImageInfo instance, and the final one is a reference to the ImageDecoder.Source object that the image is being read from.

In the example we’re only interested in the middle one of these which contains some basic information about the image being read. At present it only contains the mime type, the size of the image in pixels, and whether the image is animated. In this case we get the following output:

Decoded Image: MIME type: image/gif; size: 720x1280; isAnimated: true

It would certainly nicer if we could get more information here such as the total size in bytes (although this may not always be available), the colour depth of the image, or the number of frames in the animation. However this still may not be a final API so there may be more to come.

Nonetheless this still provides us with some advance information about whether the image is suitable for what we may need. If it isn’t we can abort the image loading by calling close() on the ImageDecoder instance. At present this will result in a NullPointerException being thrown because it will result in it attempting to create an AnimatedImageDrawable object will null data. Hopefully this will fail a little more gracefully in the final release.

Using a listener can also enable us to do some other really cool stuff and in the next article in this series we’ll look at some of the more advanced things we can do.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. I have some questions:
    1. What’s the difference between using this, and using the Movie class? Does it support more files? Animated WebP ? mp4 ? WebM ? Is it more efficient?
    2. This is for a View. What should be done for live wallpaper? Is it better to use this API than Movie?

    1. This is predominantly aimed at static images which include Animated GIFs (which sit somewhere between a static image and a video file). I cover the additional features that ImageDecoder offers in other posts.

      With regard to live wallpapers, you can easily render a Drawable to a Canvas. It more comes down to specific use-cases – is the data more efficiently saved as a static image of Animated GIF compared to a dedicated video format? The answer to that question should give a strong hint as the correct approach to use.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.