使用 Glide,如何将 GifDrawable 的每一帧作为位图进行检查? [英] Using Glide, how can I go over each frame of GifDrawable, as Bitmap?

查看:32
本文介绍了使用 Glide,如何将 GifDrawable 的每一帧作为位图进行检查?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在动态壁纸中,我有一个 Canvas 实例,我希望将 GIF/WEBP 内容绘制到其中,该实例是通过 Glide 加载的.

In a live wallpaper, I have a Canvas instance that I wish to draw GIF/WEBP content into, which was loaded via Glide.

我希望用 Glide 做这件事的原因是,它比我过去为同一件事找到的解决方案提供了一些优势(此处,存储库此处) :

The reason I wish to do it with Glide, is that it provides some advantages over a solution I've found in the past for the same thing (here , repository here) :

  1. 电影的使用限制了我只能使用 GIF.使用 Glide,我还可以支持 WEBP 动画
  2. 电影的使用似乎效率低下,因为它没有告诉我在帧之间等待的时间,所以我必须选择我想尝试使用的 FPS.它也在 Android P 上被弃用.
  3. Glide 或许能够简化各种缩放的处理.
  4. Glide 可能不会像原始代码那样崩溃,并且可能会提供更好的机制控制.

问题

Glide 似乎已优化为仅适用于普通 UI(视图).它有一些基本功能,但对于我正在尝试做的事情来说,最重要的功能似乎是私有的.

The problem

Glide seems to be optimized to work only with normal UI (Views). It has some basic functions, but the most important ones for what I'm trying to do seems to be private.

我使用官方 Glide 库 (v 3.8.0)GIF 加载,以及 GlideWebpDecoder 用于 WEBP 加载(相同版本).

I use official Glide library (v 3.8.0) for GIF loading, and GlideWebpDecoder for WEBP loading (with same version).

加载每一个的基本调用是这样的:

The basic call to load each of those, is as such:

动图:

    GlideApp.with(this).asGif()
            .load("https://res.cloudinary.com/demo/image/upload/bored_animation.gif")
            .into(object : SimpleTarget<GifDrawable>() {
                override fun onResourceReady(resource: GifDrawable, transition: Transition<in GifDrawable>?) {
                    //example of usage:
                    imageView.setImageDrawable(resource)
                    resource.start()
                }
            })

WEBP:

        GlideApp.with(this).asDrawable()
                .load("https://res.cloudinary.com/demo/image/upload/fl_awebp/bored_animation.webp")
//                .optionalTransform(WebpDrawable::class.java, WebpDrawableTransformation(CircleCrop()))
                .into(object : SimpleTarget<Drawable>() {
                    override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
                        //example of usage:
                        imageView.setImageDrawable(resource)
                        if (resource is Animatable) {
                            (resource as Animatable).start()
                        }
                    }
                })

现在,请记住我并没有真正的 ImageView,而是只有一个 Canvas,我通过 surfaceHolder.lockCanvas() 调用获得.

Now, remember I don't really have an ImageView, and instead I only have a Canvas, which I get via surfaceHolder.lockCanvas() call.

                    resource.callback = object : Drawable.Callback {
                        override fun invalidateDrawable(who: Drawable) {
                            Log.d("AppLog", "frame ${resource.frameIndex}/${resource.frameCount}")
                        }

                    }

但是,当我尝试获取要用于当前帧的 Bitmap 时,却找不到正确的函数.

However, when I try to fetch the Bitmap to be used for the current frame, I fail to find the correct function.

我试过这个例子(这只是一个例子,看看它是否可以与画布一起工作):

I tried this for example (and this is only an example, to see if it can work with canvas):

    val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)

    ...
    resource.draw(canvas)

但是好像没有把内容绘制到位图中,我觉得是因为它的draw函数有这几行代码:

But it doesn't seem to draw the content into the bitmap, and I think it's because its draw function has these lines of code:

  @Override
  public void draw(@NonNull Canvas canvas) {
    if (isRecycled) {
      return;
    }

    if (applyGravity) {
      Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect());
      applyGravity = false;
    }

    Bitmap currentFrame = state.frameLoader.getCurrentFrame();
    canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
  }

然而 getDestRect() 返回一个 0 大小的矩形,我找不到如何修改它:它也是私有的,我没有看到任何改变它的东西.

Yet the getDestRect() returns a 0-sized rectangle, which I can't find how to modify : it's also private, and I don't see anything that changes it.

  1. 假设我得到了我想使用的 Drawable (GIF/WEBP),我怎样才能得到它可以产生的每个帧(而不仅仅是第一帧),并将它绘制到画布上(使用帧之间的适当时间,当然)?

  1. Suppose I got the Drawable I wish to use (GIF/WEBP), how can I get each of the frames it can produce (and not just the first frame), and draw it into a canvas (with the right amount of time between frames, of course) ?

我也可以设置缩放类型,就像在 ImageView (center-crop, fit-center, center-inside...) 上一样吗?

Can I also set the scaling type somehow, just like on ImageView (center-crop, fit-center, center-inside...) ?

也许有更好的替代方法?也许假设我有一个 GIF/WEBP 动画文件,Glide 允许我只使用它的解码器吗?类似于 这个库?

Is there perhaps a better alternative to this? Maybe suppose I have a GIF/WEBP animation file, does Glide allow me to just use its decoder? Something like on this library ?

<小时>

我找到了一个不错的替代库,它允许一帧一帧地加载 GIF,在这里.它似乎在逐帧加载时效率不高,但它是开源的,可以轻松修改以更好地工作.

I've found a nice alternative library, that allows to load the GIF one frame after another, here. It doesn't seem as efficient in loading the frame-by-frame, but it's open sourced and can easily be modified to work better.

在 Glide 上做可能会更好,因为它也支持缩放和 WEBP 加载.

Still could be much nicer to do it on Glide, as it supports scaling and WEBP loading too.

我制作了一个 POC(链接此处) 这表明它确实可以逐帧运行,等待它们之间的正确时间.如果有人像我一样成功地做到了,但在 Glide(当然是最新版本的 Glide)上,我会接受答案并授予赏金.代码如下:

I've made a POC (link here) that shows that it can indeed go frame-by-frame, waiting for the right time between them. If anyone succeeds doing the exact same as I did, but on Glide (latest version of Glide, of course), I will accept the answer and grant the bounty. Here's the code:

**GifPlayer.kt ,基于 NsGifPlayer.java **

**GifPlayer.kt , based on NsGifPlayer.java **

open class GifPlayer {
    companion object {
        const val ENABLE_CACHING = false
        const val MEM_CACHE_SIZE_PERCENT = 0.8
        fun calculateMemCacheSize(percent: Double): Long {
            if (percent < 0.05f || percent > 0.8f) {
                throw IllegalArgumentException("setMemCacheSizePercent - percent must be " + "between 0.05 and 0.8 (inclusive)")
            }
            val maxMem = Runtime.getRuntime().maxMemory()
//            Log.d("AppLog", "max mem :$maxMem")
            return Math.round(percent * maxMem)
        }
    }

    private val uiHandler = Handler(Looper.getMainLooper())
    private var playerHandlerThread: HandlerThread? = null
    private var playerHandler: Handler? = null
    private val gifDecoder: GifDecoder = GifDecoder()
    private var currentFrame: Int = -1
    var listener: GifListener? = null
    var state: State = State.IDLE
        private set
    private val playRunnable: Runnable
    private val frames = HashMap<Int, AnimationFrame>()
    private var currentUsedMemByCache = 0L

    class AnimationFrame(val bitmap: Bitmap, val duration: Long)

    enum class State {
        IDLE, PAUSED, PLAYING, RECYCLED, ERROR
    }

    interface GifListener {
        fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int)

        fun onError()
    }

    init {
        val memCacheSize = if (ENABLE_CACHING) calculateMemCacheSize(MEM_CACHE_SIZE_PERCENT) else 0L
//        Log.d("AppLog", "memCacheSize:$memCacheSize = ${memCacheSize / 1024L} MB")
        playRunnable = object : Runnable {
            override fun run() {
                val frameCount = gifDecoder.frameCount
                gifDecoder.setCurIndex(currentFrame)
                currentFrame = (currentFrame + 1) % frameCount
                val animationFrame = if (ENABLE_CACHING) frames[currentFrame] else null
                if (animationFrame != null) {
//                    Log.d("AppLog", "cache hit - $currentFrame")
                    val bitmap = animationFrame.bitmap
                    val delay = animationFrame.duration
                    uiHandler.post {
                        listener?.onGotFrame(bitmap, currentFrame, frameCount)
                        if (state == State.PLAYING)
                            playerHandler!!.postDelayed(this, delay)
                    }
                } else {
//                    Log.d("AppLog", "cache miss - $currentFrame fill:${frames.size}/$frameCount")
                    val bitmap = gifDecoder.bitmap
                    val delay = gifDecoder.decodeNextFrame().toLong()
                    if (ENABLE_CACHING) {
                        val bitmapSize = BitmapCompat.getAllocationByteCount(bitmap)
                        if (bitmapSize + currentUsedMemByCache < memCacheSize) {
                            val cacheBitmap = Bitmap.createBitmap(bitmap)
                            frames[currentFrame] = AnimationFrame(cacheBitmap, delay)
                            currentUsedMemByCache += bitmapSize
                        }
                    }
                    uiHandler.post {
                        listener?.onGotFrame(bitmap, currentFrame, frameCount)
                        if (state == State.PLAYING)
                            playerHandler!!.postDelayed(this, delay)
                    }
                }
            }
        }
    }

    @Suppress("unused")
    protected fun finalize() {
        stop()
    }

    @UiThread
    fun start(filePath: String): Boolean {
        if (state != State.IDLE && state != State.ERROR)
            return false
        currentFrame = -1
        state = State.PLAYING
        playerHandlerThread = HandlerThread("GifPlayer")
        playerHandlerThread!!.start()
        val looper = playerHandlerThread!!.looper
        playerHandler = Handler(looper)
        playerHandler!!.post {
            try {
                gifDecoder.load(filePath)
            } catch (e: Exception) {
                uiHandler.post {
                    state = State.ERROR
                    listener?.onError()
                }
                return@post
            }

            val bitmap = gifDecoder.bitmap
            if (bitmap != null) {
                playRunnable.run()
            } else {
                frames.clear()
                gifDecoder.recycle()
                uiHandler.post {
                    state = State.ERROR
                    listener?.onError()
                }
                return@post
            }
        }
        return true
    }

    @UiThread
    fun stop(): Boolean {
        if (state == State.IDLE)
            return false
        state = State.IDLE
        playerHandler!!.removeCallbacks(playRunnable)
        playerHandlerThread!!.quit()
        playerHandlerThread = null
        playerHandler = null
        return true
    }

    @UiThread
    fun pause(): Boolean {
        if (state != State.PLAYING)
            return false
        state = State.PAUSED
        playerHandler?.removeCallbacks(playRunnable)
        return true
    }

    @UiThread
    fun resume(): Boolean {
        if (state != State.PAUSED)
            return false
        state = State.PLAYING
        playerHandler?.removeCallbacks(playRunnable)
        playRunnable.run()
        return true
    }

    @UiThread
    fun toggle(): Boolean {
        when (state) {
            State.PLAYING -> pause()
            State.PAUSED -> resume()
            else -> return false
        }
        return true
    }

}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var player: GifPlayer

    @SuppressLint("StaticFieldLeak")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val file = File(this@MainActivity.filesDir, "file.gif")
        object : AsyncTask<Void, Void, Void?>() {

            override fun doInBackground(vararg params: Void?): Void? {
                val inputStream = resources.openRawResource(R.raw.fast)
                if (!file.exists()) {
                    file.parentFile.mkdirs()
                    val outputStream = FileOutputStream(file)
                    val buf = ByteArray(1024)
                    var len: Int
                    while (true) {
                        len = inputStream.read(buf)
                        if (len <= 0)
                            break
                        outputStream.write(buf, 0, len)
                    }
                    inputStream.close()
                    outputStream.close()
                }
                return null
            }

            override fun onPostExecute(result: Void?) {
                super.onPostExecute(result)
                player.setFilePath(file.absolutePath)
                player.start()
            }

        }.execute()

        player = GifPlayer(object : GifPlayer.GifListener {
            override fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int) {
                Log.d("AppLog", "onGotFrame $frame/$frameCount")
                imageView.post {
                    imageView.setImageBitmap(bitmap)
                }
            }

            override fun onError() {
                Log.d("AppLog", "onError")
            }
        })
    }

    override fun onStart() {
        super.onStart()
        player.resume()
    }

    override fun onStop() {
        super.onStop()
        player.pause()
    }

    override fun onDestroy() {
        super.onDestroy()
        player.stop()
    }
}

推荐答案

好的,我找到了 3 个可能的解决方案:

OK I've found 3 possible solutions:

  1. 如果您希望在 Drawable 播放时出现帧,您可以这样做:

private fun testGif() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_gif).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE).submit().get() as GifDrawable
    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(1)
    val callback = object : CallbackEx() {
        override fun invalidateDrawable(who: Drawable) {
            super.invalidateDrawable(who)
            val gif = who as GifDrawable
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            who.draw(canvas)
            //image is available here on the bitmap object
            Log.d("AppLog", "frameIndex:${gif.frameIndex} frameCount:${gif.frameCount} firstFrame:${gif.firstFrame}")
        }
    }
    drawable.callback = callback
    drawable.start()
}

private fun testWebp() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_webp).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .submit().get() as WebpDrawable
    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.loopCount = 1
    val callback = object : CallbackEx() {
        override fun invalidateDrawable(who: Drawable) {
            val webp = who as WebpDrawable
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            who.draw(canvas)
            //image is available here on the bitmap object
            Log.d("AppLog", "frameIndex:${webp.frameIndex} frameCount:${webp.frameCount} firstFrame:${webp.firstFrame}")
        }
    }
    drawable.callback = callback
    drawable.start()
}

  1. 如果您对从 Glide 中得到的东西进行反思,您可以这样使用:

private fun testWebp2() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_webp).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .submit().get() as WebpDrawable
    drawable.constantState
    val state = drawable.constantState as Drawable.ConstantState
    val frameLoader: Field = state::class.java.getDeclaredField("frameLoader")
    frameLoader.isAccessible = true
    @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
    val webpFrameLoader = frameLoader.get(state) as WebpFrameLoader
    val webpDecoder: Field = webpFrameLoader.javaClass.getDeclaredField("webpDecoder")
    webpDecoder.isAccessible = true
    val standardGifDecoder = webpDecoder.get(webpFrameLoader) as GifDecoder
    Log.d("AppLog", "got ${standardGifDecoder.frameCount} frames:")
    for (i in 0 until standardGifDecoder.frameCount) {
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        //image is available here on the bitmap object
        Log.d("AppLog", "${standardGifDecoder.currentFrameIndex} - $delay ${bitmap?.width}x${bitmap?.height}")
        standardGifDecoder.advance()
    }
    Log.d("AppLog", "done")
}

private fun testGif2() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_gif).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE).submit().get() as GifDrawable
    val state = drawable.constantState as Drawable.ConstantState
    val frameLoader: Field = state::class.java.getDeclaredField("frameLoader")
    frameLoader.isAccessible = true
    val gifFrameLoader: Any = frameLoader.get(state)
    val gifDecoder: Field = gifFrameLoader.javaClass.getDeclaredField("gifDecoder")
    gifDecoder.isAccessible = true
    val standardGifDecoder = gifDecoder.get(gifFrameLoader) as StandardGifDecoder
    Log.d("AppLog", "got ${standardGifDecoder.frameCount} frames:")
    val parent = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "gifFrames")
    parent.mkdirs()
    for (i in 0 until standardGifDecoder.frameCount) {
        val file = File(parent, "${String.format("%07d", i)}.png")
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        if (bitmap == null) {
            Log.d("AppLog", "error getting frame")
            break
        }
        //image is available here on the bitmap object
        Log.d("AppLog", "${standardGifDecoder.currentFrameIndex} - $delay ${bitmap?.width}x${bitmap?.height}")
        standardGifDecoder.advance()
    }
    Log.d("AppLog", "done")
}

  1. 最后,如果你想要一个更底层的解决方案,你可以这样做:

    private fun testGif3() {
        // found from GifDrawableResource StreamGifDecoder StandardGifDecoder
        val data = resources.openRawResource(R.raw.test_gif).readBytes()
        val byteBuffer = ByteBuffer.wrap(data)
        val glide = GlideApp.get(this)
        val gifBitmapProvider = GifBitmapProvider(glide.bitmapPool,  glide.arrayPool)
        val header = GifHeaderParser().setData(byteBuffer).parseHeader()
        val standardGifDecoder = StandardGifDecoder(gifBitmapProvider, header, byteBuffer, 1)
        //alternative, without getting header and needing sample size:
//        val standardGifDecoder = StandardGifDecoder(gifBitmapProvider)
//        standardGifDecoder.read(data)
        val frameCount = standardGifDecoder.frameCount
        standardGifDecoder.advance()
        for (i in 0 until frameCount) {
            val delay = standardGifDecoder.nextDelay
            val bitmap = standardGifDecoder.nextFrame
            //bitmap ready here
            standardGifDecoder.advance()
        }
    }

    private fun testWebP3() {
        //found from  ByteBufferWebpDecoder  StreamWebpDecoder  WebpDecoder
        val data = resources.openRawResource(R.raw.test_webp).readBytes()
        val cacheStrategy: WebpFrameCacheStrategy? = Options().get(WebpFrameLoader.FRAME_CACHE_STRATEGY)
        val glide = GlideApp.get(this)
        val bitmapPool = glide.bitmapPool
        val arrayPool = glide.arrayPool
        val gifBitmapProvider = GifBitmapProvider(bitmapPool, arrayPool)
        val webpImage = WebpImage.create(data)
        val sampleSize = 1
        val webpDecoder = WebpDecoder(gifBitmapProvider, webpImage, ByteBuffer.wrap(data), sampleSize, cacheStrategy)
        val frameCount = webpDecoder.frameCount
        webpDecoder.advance()
        for (i in 0 until frameCount) {
            val delay = webpDecoder.nextDelay
            val bitmap = webpDecoder.nextFrame
            //bitmap ready here
            webpDecoder.advance()
        }
    }

这篇关于使用 Glide,如何将 GifDrawable 的每一帧作为位图进行检查?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆