背景
在动态壁纸中,我有一个 Canvas 实例,我希望将 GIF/WEBP 内容绘制到该实例中,该实例是通过 Glide 加载的。
我希望用 Glide 来做这件事的原因是,它比我过去为同样的事情找到的解决方案提供了一些优势(这里,存储库这里):
- 电影的使用使我只能使用 GIF。使用 Glide 我还可以支持 WEBP 动画
- 电影的使用似乎效率低下,因为它没有告诉我在帧之间等待的时间,所以我必须选择我希望尝试使用的 FPS。它在 Android P 上也已弃用。
- Glide 或许能够简化各种缩放的处理。
- Glide 可能不会像原始代码那样崩溃,并且可能提供更好的机制控制。
问题
Glide 似乎被优化为仅适用于普通 UI(视图)。它有一些基本功能,但我正在尝试做的最重要的功能似乎是私有的。
我发现了什么
我使用官方Glide 库(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()
}
})
网页版:
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 大小的矩形,我找不到如何修改它:它也是私有的,我看不到任何改变它的东西。
问题
假设我得到了我想要使用的 Drawable (GIF/WEBP),我怎样才能得到它可以产生的每一帧(而不仅仅是第一帧),并将它绘制到画布中(帧之间的时间量合适, 当然) ?
我也可以以某种方式设置缩放类型,就像在 ImageView (center-crop, fit-center, center-inside ...) 上一样?
有没有更好的选择呢?也许假设我有一个 GIF/WEBP 动画文件,Glide 是否允许我只使用它的解码器?像这个图书馆的东西?
编辑:
我找到了一个不错的替代库,它允许一帧接一帧地加载 GIF,here。逐帧加载似乎效率不高,但它是开源的,可以轻松修改以更好地工作。
在 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()
}
}