Используя Glide, как я могу просмотреть каждый кадр GifDrawable как Bitmap? - PullRequest
0 голосов
/ 27 августа 2018

Фон

В живых обоях у меня есть экземпляр Canvas, в который я хочу рисовать контент GIF / WEBP, который был загружен с помощью Glide.

Причина, по которой я хочу это сделатьс Glide, это то, что он предоставляет некоторые преимущества по сравнению с решением, которое я нашел в прошлом для той же вещи ( здесь , хранилище здесь ):

  1. Использование фильма ограничивает меня только GIF.С Glide я также мог бы поддерживать WEBP анимацию
  2. Использование Movie кажется неэффективным, так как оно не говорит мне время ожидания между кадрами, поэтому я должен выбрать FPS, который я хочу попробовать использовать.Это также не рекомендуется в Android P.
  3. Glide может облегчить обработку различного масштабирования.
  4. Glide может не работать так, как в исходном коде, и может обеспечить лучший контроль над механизмом.

Проблема

Glide, похоже, оптимизирован для работы только с обычным пользовательским интерфейсом (представления).У него есть некоторые основные функции, но наиболее важные из того, что я пытаюсь сделать, кажутся приватными.

То, что я нашел

Я использую официальный Библиотека 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,и вместо этого у меня есть только Canvas, который я получаю с помощью surfaceHolder.lockCanvas() вызова.

                    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, который я не могу найтиd как изменить: это тоже личное, и я не вижу ничего, что изменило бы его.

Вопросы

  1. Предположим, я получил Drawable, который хочу использовать (GIF / WEBP), как я могу получить каждый из кадров, которые он может создать (а не только первый кадр), и нарисовать его на холсте (с правильным количеством времени между кадрами, конечно)?

  2. Можно ли как-то установить тип масштабирования, как в ImageView (центральная обрезка, фит-центр, центр-внутри ...)?

  3. Возможно, есть лучшая альтернатива этому?Может быть, предположим, что у меня есть файл анимации GIF / WEBP, позволяет ли Glide использовать его декодер?Что-то вроде этой библиотеки ?


РЕДАКТИРОВАТЬ:

Я нашел хорошую альтернативную библиотеку, которая позволяет загружать GIF один кадр за другим, здесь.Это не кажется эффективным при загрузке покадрово, но это открытый исходный код, и его легко изменить, чтобы он работал лучше.

Тем не менее, это может быть намного приятнее, если сделать это на 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()
    }
}

1 Ответ

0 голосов
/ 31 августа 2018

У меня было такое же требование, когда я хотел отобразить предварительный просмотр вместо анимации при загрузке GIF-файла в Glide.

Мое решение состояло в том, чтобы взять первый кадр из 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);

Вы можете либо выполнить анимацию, чтобы получить ключевые кадры, либо получить их по индексу в обратном вызове, напрямую обращаясь кdecoder, прикрепленный к GifDrawable.В качестве альтернативы установите 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
        ///2936721/kak-preobrazovat-drawable-v-rastrovoe-izobrazhenie //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. +, Но я не уверен, как далеко вы должны пойти, чтобы использовать мой подход.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...