In a live wallpaper, I have a Canvas instance that I wish to draw GIF/WEBP content into, which was loaded via 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) :
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.
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:
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()
}
}
})
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}")
}
}
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)
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());
}
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.
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) ?
Can I also set the scaling type somehow, just like on ImageView (center-crop, fit-center, center-inside...) ?
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 ?
EDIT:
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.
Still could be much nicer to do it on Glide, as it supports scaling and WEBP loading too.
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 , 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()
}
}
OK I've found 3 possible solutions:
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()
}
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()
}
}
Also 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")
}
}