使用Glide,如何将GifDrawable的每一帧作为位图遍历?

12

背景

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

我之所以选择使用Glide,是因为它相对于我过去发现的解决方案(这里,存储库这里)提供了一些优势:

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

问题

Glide似乎仅针对普通UI(视图)进行了优化。它具有一些基本功能,但对于我正在尝试做的最重要的功能似乎是私有的。

我发现的

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

加载每个文件的基本调用方式如下:

GIF:

    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,而是通过surfaceHolder.lockCanvas()调用获得的Canvas。

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

                    }

然而,当我尝试获取用于当前帧的位图时,我无法找到正确的函数。

例如,我尝试了这个(这只是一个示例,以查看它是否可以与画布一起使用):

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

    ...
    resource.draw(canvas)

但是它似乎没有将内容绘制到位图中,我认为这是因为它的 draw 函数有以下代码行:

  @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的矩形,我找不到如何修改它:它也是私有的,我没有看到任何改变它的方法。

问题

  1. 假设我已经获得了我想要使用的Drawable(GIF/WEBP),如何获取它可以生成的每一帧(而不仅仅是第一帧),并在画布上绘制它(当然需要正确的帧之间的时间)?

  2. 我是否也可以以某种方式设置缩放类型,就像ImageView上那样(居中裁剪、适合中心、居中内部…)?

  3. 也许有比这更好的选择吗?也许假设我有一个GIF/WEBP动画文件,Glide能让我只使用它的解码器吗?就像this library上那样?


编辑:

我找到了一个不错的替代库,可以逐帧加载 GIF,在这里。它似乎没有逐帧加载的效率高,但是它是开源的,可以很容易地进行修改以提高效率。

当然,如果能够在 Glide 上完成这个功能会更好,因为 Glide 还支持缩放和 WEBP 加载。

我做了一个 POC(链接 在这里),显示它确实可以逐帧运行,并在它们之间等待合适的时间。如果有人能成功地在 Glide(最新版本的 Glide)上完成与我相同的操作,我将接受答案并授予悬赏。以下是代码:

** GifPlayer.kt,基于 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个回答

6
我曾经有一个类似的需求,当我想在Glide中加载gif时,我希望显示预览而不是动画。我的解决方案是从GifDrawable中获取第一帧并将其作为整个drawable呈现。同样的方法也可以适用于获取其他帧以进行显示(或导出等)。
DrawableRequestBuilder builder = Glide.with(ctx).load(someUrl);
builder.listener(new RequestListener<String, GlideDrawable>() {
    @Override
    public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
        return false;
    }

    @Override
    public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
        if (resource.isAnimated()) {
            target.onResourceReady(new GlideBitmapDrawable(null, ((GifDrawable) resource).getFirstFrame()), null);
        }
        return handled;
    }
});
builder.into(mImageView);

您可以通过推进动画以获取关键帧,或者在回调中通过直接访问附加到 GifDrawable 的解码器按索引获取它们。或者在可绘制对象准备就绪时设置一个“Callback”(实际类名)。每次都会通过“onFrameReady”调用它(为您提供可绘制对象中的当前帧)。gif 可绘制对象类已经管理了位图池。
一旦 GifDrawable 准备就绪,请使用以下方法循环遍历帧:
GifDrawable gd = (GifDrawable) resource;
Bitmap b = gd.getDecoder().getNextFrame();  

请注意,如果您正在使用解码器,则应该真正从我上面提到的onResourceReady回调中执行。当我尝试早期执行时,我遇到了间歇性问题。 如果让解码器自动运行,您可以获得帧的回调
gifDrawable.setCallback(new Drawable.Callback() {
    @Override
    public void invalidateDrawable(@NonNull Drawable who) {
        //NOTE: this method is called each time the GifDrawable updates itself with a new frame
        //who.draw(canvas); //if you already have a canvas
        //https://dev59.com/NnA75IYBdhLWcg3w6Nr8 //if you really want a bitmap
    }

    @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { /* ignore */ }
    @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { /* ignore */ }
});

这是当时可用的最佳方法。由于已经过去一年多,我无法保证现在没有更有效的方法。
我使用的库版本是Glide 3.7.0。在最新版本4.7.+中访问受限,但我不确定需要回退到哪个版本才能使用我的方法。

2

无论如何,我们将使用来自Glide的非文档方法,并希望有一天Glide团队将其公开。您需要具备一定的Java反射经验:)以下是从GIF文件中提取位图的代码示例:

ArrayList bitmaps = new ArrayList<>();
Glide.with(AppObj.getContext())
            .asGif()
            .load(GIF_PATH)
            .into(new SimpleTarget<GifDrawable>() {
                @Override
                public void onResourceReady(@NonNull GifDrawable resource, @Nullable Transition<? super GifDrawable> transition) {
                    try {
                        Object GifState = resource.getConstantState();
                        Field frameLoader = GifState.getClass().getDeclaredField("frameLoader");
                        frameLoader.setAccessible(true);
                        Object gifFrameLoader = frameLoader.get(GifState);

                        Field gifDecoder = gifFrameLoader.getClass().getDeclaredField("gifDecoder");
                        gifDecoder.setAccessible(true);
                        StandardGifDecoder standardGifDecoder = (StandardGifDecoder) gifDecoder.get(gifFrameLoader);
                        for (int i = 0; i < standardGifDecoder.getFrameCount(); i++) {
                            standardGifDecoder.advance();
                            bitmaps.add(standardGifDecoder.getNextFrame());
                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }

                }
            });

}


1
你知道如何获取帧之间的时间吗? - android developer

1
好的,我找到了3个可能的解决方案:
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()
}

如果你对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")
}

最后,如果你想要一个更底层的解决方案,你可以这样做:
    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()
        }
    }

还有 Lottie:

LottieCompositionFactory.fromRawRes(this, R.raw.car_driving_landscape).addListener { composition -> // Create a LottieDrawable from the LottieComposition
    val drawable = LottieDrawable().apply {
        setComposition(composition)
    }
    thread {
        val totalFrames = composition.durationFrames.toInt()
        val frameDuration = composition.duration / totalFrames
        val frameDurationInt = frameDuration.roundToInt()
        Log.d("AppLog", "duration of each frame:$frameDurationInt ms . Frames count:$totalFrames")
        val startTime = System.currentTimeMillis()
        val width = drawable.intrinsicWidth
        val height = drawable.intrinsicHeight
        val bitmap =
            Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        for (i in 0 until totalFrames) {
            drawable.frame = i
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            drawable.draw(canvas)
            //bitmap ready here
            Log.d("AppLog", "bitmap for frame $i  ")
        }
        val endTime = System.currentTimeMillis()
        Log.d("AppLog", "it took ${endTime - startTime} ms to get all frames as bitmaps")
    }
}

谢谢,它实际上打印了每个帧的信息。我们能否调整动画WebP和GIF的大小,还是必须调整每个位图,然后将这些帧编码为WebP / GIF? - CrackerKSR
这个答案就像海洋中的珍珠。 - CrackerKSR
drawable.constantState(method 2, testWebp3() ) 是否返回 drawable.constantState。实际上我想用 Java 写这个。 - CrackerKSR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接