Search code examples
androidanimated-gifimagedecoderanimated-webp

Is it possible to get the new ImageDecoder class to return Bitmaps, one frame after another, manually?


Background

I'm trying to go over bitmaps of animated GIF&WEBP files manually (frame by frame), so that it would work not just for Views, but on other cases too (such as a live wallpaper).

The problem

Animated GIF/WEBP files are supported only from Android P, using ImageDecoder API (example here) .

For GIF, I wanted to try Glide for the task, but I've failed, so I've tried overcoming this, by using a library that allows to load them (here, solution here). I think it works fine.

For WebP, I thought I've found another library that could work on older Android versions (here, made fork here), but it seems that it can't handle WebP files well in some cases (reported here). I tried to figure out what's the issue and how to solve it, but I didn't succeed.

So, assuming that some day Google will support GIF&WEBP animation for older Android versions via the support library (they wrote it here), I've decided to try to use ImageDecoder for the task.

Thing is, looking in the entire API of ImageDecoder , it's quite restricted in how we should use it. I don't see how I can overcome its limitations.

What I've found

This is how ImageDecoder can be used to show an animated WebP on an ImageView (just a sample, of course, available here) :

class MainActivity : AppCompatActivity() {
    @SuppressLint("StaticFieldLeak")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val source = ImageDecoder.createSource(resources, R.raw.test)
        object : AsyncTask<Void, Void, Drawable?>() {
            override fun doInBackground(vararg params: Void?): Drawable? {
                return try {
                    ImageDecoder.decodeDrawable(source)
                } catch (e: Exception) {
                    null
                }
            }

            override fun onPostExecute(result: Drawable?) {
                super.onPostExecute(result)
                imageView.setImageDrawable(result)
                if (result is AnimatedImageDrawable) {
                    result.start()
                }
            }

        }.execute()

    }
}

I've tried to read all of the documentations of ImageDecoder and AnimatedImageDrawable, and also look at its code, but I don't see how it's possible to manually go over each frame, and have the time that needs to be waited between them.

The questions

  1. Is there a way to use ImageDecoder API to go over each frame manually, getting a Bitmap to draw and knowing how much time it's needed to wait between frames? Any workaround available? Maybe even using AnimatedImageDrawable ?

  2. I'd like to do the same on older Android versions. Is it possible? If so how? Maybe on a different API/library? Google wrote it works on a way to use ImageDecoder on older Android versions, but I don't see it being mentioned anywhere (except for the link I've provided). Probably not ready yet... Android P didn't even reach 0.1% of users yet... Maybe Fresco can do it? I've tried to check it there too, but I don't see that it's capable of such a thing either, and it's a huge library to use just for this task, so I'd prefer to use a different library instead... I also know that libwebp is available, but it's in C/C++ and not sure if it's suited for Android, and whether there is a port for it on Java/Kotlin for Android.


EDIT:

Since I think I got what I wanted, for both a third party library and for ImageDecoder, to be able to get bitmaps out of animated WebP, I'd still want to know how to get the frame count and current frame using ImageDecoder, if that's possible. I tried using ImageDecoder.decodeDrawable(source, object : ImageDecoder.OnHeaderDecodedListener... , but it doesn't provide frame count information, and there is no way in the API that I can see that I can go to a specific frame index and start from there, or to know for a specific frame how long it needs to go to the next frame. So I made a reuqest about those here.

Sadly I also could not find that Google has ImageDecoder available for older Android versions, either.

It's also interesting if there is some kind of way to do the same as I did for the relatively new animation file of HEIC. Currently it's supported only on Android P.


Solution

  • OK, I got a possible solution, using Glide library, together with GlideWebpDecoder library .

    I'm not sure if that's the best way to do it, but I think it should work fine. The next code shows how it's possible to make the drawable draw into the Bitmap instance that I create, for each frame that the animation needs to show. It's not exactly what I asked, but it might help others.

    Here's the code (project available here) :

    CallbackEx.kt

    abstract class CallbackEx : Drawable.Callback {
        override fun unscheduleDrawable(who: Drawable, what: Runnable) {}
        override fun invalidateDrawable(who: Drawable) {}
        override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {}
    }
    

    MyAppGlideModule.kt

    @GlideModule
    class MyAppGlideModule : AppGlideModule()
    

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        var webpDrawable: WebpDrawable? = null
        var gifDrawable: GifDrawable? = null
        var callback: Drawable.Callback? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            useFrameByFrameDecoding()
    //        useNormalDecoding()
        }
    
        fun useNormalDecoding() {
            //webp url : https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp
            Glide.with(this)
                    //                .load(R.raw.test)
                    //                .load(R.raw.fast)
                    .load(R.raw.example2)
    
                    //                .load("https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp")
                    .into(object : SimpleTarget<Drawable>() {
                        override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
                            imageView.setImageDrawable(drawable)
                            when (drawable) {
                                is GifDrawable -> {
                                    drawable.start()
                                }
                                is WebpDrawable -> {
                                    drawable.start()
                                }
                            }
                        }
                    })
        }
    
        fun useFrameByFrameDecoding() {
            //webp url : https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp
            Glide.with(this)
                    .load(R.raw.test)
                    //                .load(R.raw.fast)
                    //                .load(R.raw.example2)
                    //                .load("https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp")
                    .into(object : SimpleTarget<Drawable>() {
                        override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
                            //                        val callback
                            when (drawable) {
                                is GifDrawable -> {
                                    gifDrawable = drawable
                                    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                                    val canvas = Canvas(bitmap)
                                    drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                                    drawable.setLoopCount(GifDrawable.LOOP_FOREVER)
                                    callback = object : CallbackEx() {
                                        override fun invalidateDrawable(who: Drawable) {
                                            who.draw(canvas)
                                            imageView.setImageBitmap(bitmap)
                                            Log.d("AppLog", "invalidateDrawable ${drawable.toString().substringAfter('@')} ${drawable.frameIndex}/${drawable.frameCount}")
                                        }
                                    }
                                    drawable.callback = callback
                                    drawable.start()
                                }
                                is WebpDrawable -> {
                                    webpDrawable = drawable
                                    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                                    val canvas = Canvas(bitmap)
                                    drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                                    drawable.setLoopCount(WebpDrawable.LOOP_FOREVER)
                                    callback = object : CallbackEx() {
                                        override fun invalidateDrawable(who: Drawable) {
                                            who.draw(canvas)
                                            imageView.setImageBitmap(bitmap)
                                            Log.d("AppLog", "invalidateDrawable ${drawable.toString().substringAfter('@')} ${drawable.frameIndex}/${drawable.frameCount}")
                                        }
                                    }
                                    drawable.callback = callback
                                    drawable.start()
                                }
                            }
                        }
                    })
        }
    
        override fun onStart() {
            super.onStart()
            gifDrawable?.start()
            gifDrawable?.start()
        }
    
        override fun onStop() {
            super.onStop()
            Log.d("AppLog", "onStop")
            webpDrawable?.stop()
            gifDrawable?.stop()
        }
    
    }
    

    Not sure why SimpleTarget is marked as deprecated, and what I should use instead, though.

    Using a similar technique, I've also found out how to do it using ImageDecoder, but not with the same functionality for some reason. A sample project available here.

    Here's the code:

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        var webpDrawable: AnimatedImageDrawable? = null
    
        @SuppressLint("StaticFieldLeak")
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val source = ImageDecoder.createSource(resources, R.raw.test)
            object : AsyncTask<Void, Void, Drawable?>() {
                override fun doInBackground(vararg params: Void?): Drawable? {
                    return try {
                        ImageDecoder.decodeDrawable(source)
                    } catch (e: Exception) {
                        null
                    }
                }
    
                override fun onPostExecute(drawable: Drawable?) {
                    super.onPostExecute(drawable)
    //                imageView.setImageDrawable(result)
                    if (drawable is AnimatedImageDrawable) {
                        webpDrawable = drawable
                        val bitmap =
                            Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
                        val canvas = Canvas(bitmap)
                        drawable.setBounds(0, 0, bitmap.width, bitmap.height)
                        drawable.repeatCount = AnimatedImageDrawable.REPEAT_INFINITE
                        drawable.callback = object : Drawable.Callback {
                            val handler = Handler()
                            override fun unscheduleDrawable(who: Drawable, what: Runnable) {
                                Log.d("AppLog", "unscheduleDrawable")
                            }
    
                            override fun invalidateDrawable(who: Drawable) {
                                who.draw(canvas)
                                imageView.setImageBitmap(bitmap)
                                Log.d("AppLog", "invalidateDrawable")
                            }
    
                            override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
                                Log.d("AppLog", "scheduleDrawable next frame in ${`when` - SystemClock.uptimeMillis()} ms")
                                handler.postAtTime(what, `when`)
                            }
                        }
                        drawable.start()
                    }
                }
            }.execute()
        }
    
        override fun onStart() {
            super.onStart()
            webpDrawable?.start()
        }
    
        override fun onStop() {
            super.onStop()
            webpDrawable?.stop()
        }
    
    }