草庐IT

android - 使用 Glide,如何将 GifDrawable 的每一帧作为 Bitmap 遍历?

coder 2023-11-22 原文

背景

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

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

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

问题

Glide 似乎已优化为仅适用于普通 UI( View )。它具有一些基本功能,但对于我正在尝试做的事情而言,最重要的功能似乎是私有(private)的。

我发现了什么

我用官方Glide library (v 3.8.0) 用于 GIF 加载,以及 GlideWebpDecoder用于 WEBP 加载(具有相同版本)。

加载其中每一个的基本调用如下:

动图:

    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()
                }
            })

网络BP:

        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() 调用获得了它。

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

                    }

但是,当我尝试获取用于当前帧的位图时,我找不到正确的函数。

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

    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 大小的矩形,我找不到如何修改它:它也是私有(private)的,我没有看到任何改变它的东西。

问题

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

  2. 我能否也以某种方式设置缩放类型,就像在 ImageView 上一样(center-crop、fit-center、center-inside...)?

  3. 是否有更好的替代方案?也许假设我有一个 GIF/WEBP 动画文件,Glide 是否允许我只使用它的解码器?类似于 this library


编辑:

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

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

我制作了一个 POC(链接 here),表明它确实可以逐帧进行,等待它们之间的正确时间。如果有人像我一样成功,但在 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()
    }
}

最佳答案

当我想在 Glide 中加载 gif 时显示预览而不是动画时,我有类似的需求。

我的解决方案是从 GifDrawable 中获取第一帧并将其呈现为整个可绘制对象。可以采用相同的方法来显示其他帧(或导出等)

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 的 decoder 来推进动画以获取关键帧或通过索引在回调中获取它们。或者在准备就绪时在可绘制对象上设置一个 Callback(实际类名)。它将由 onFrameReady 调用(每次为您提供 drawable 中的当前帧)。 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://stackoverflow.com/questions/3035692/how-to-convert-a-drawable-to-a-bitmap //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.+ 中的访问受到限制,但我不确定您需要回到多远才能使用我的方法。

关于android - 使用 Glide,如何将 GifDrawable 的每一帧作为 Bitmap 遍历?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52030982/

有关android - 使用 Glide,如何将 GifDrawable 的每一帧作为 Bitmap 遍历?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  4. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  5. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  6. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  9. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  10. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

随机推荐