and*_*per 9 android animated-gif android-glide animated-webp
在动态壁纸中,我有一个Canvas实例,我希望将GIF/WEBP内容绘制成,通过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()
}
})
Run Code Online (Sandbox Code Playgroud)
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()
}
}
})
Run Code Online (Sandbox Code Playgroud)
现在,请记住我并没有真正的ImageView,而是我只有一个Canvas,我可以通过surfaceHolder.lockCanvas()调用获得.
resource.callback = object : Drawable.Callback {
override fun invalidateDrawable(who: Drawable) {
Log.d("AppLog", "frame ${resource.frameIndex}/${resource.frameCount}")
}
}
Run Code Online (Sandbox Code Playgroud)
但是,当我尝试获取要用于当前帧的位图时,我找不到正确的函数.
我试过这个例子(这只是一个例子,看看它是否适用于canvas):
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
...
resource.draw(canvas)
Run Code Online (Sandbox Code Playgroud)
但它似乎没有将内容绘制到位图中,我认为这是因为它的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());
}
Run Code Online (Sandbox Code Playgroud)
然而,getDestRect()返回一个0大小的矩形,我找不到如何修改:它也是私有的,我没有看到任何改变它的东西.
假设我得到了我想要使用的Drawable(GIF/WEBP),我如何获得它可以产生的每个帧(而不仅仅是第一帧),并将其绘制到画布中(帧之间有适当的时间) , 当然) ?
我还可以设置缩放类型莫名其妙,就像在ImageView的(中心作物,装修中心,中心内)?
或许还有更好的选择吗?也许假设我有一个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
}
}
Run Code Online (Sandbox Code Playgroud)
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()
}
}
Run Code Online (Sandbox Code Playgroud)
当我想在 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);
Run Code Online (Sandbox Code Playgroud)
您可以通过直接访问decoder附加到 GifDrawable 的方法来推进动画以获取关键帧或通过回调中的索引获取它们。或者Callback,当它准备好时,在 drawable 上设置一个(实际的类名)。它将由onFrameReady(每次为您提供可绘制对象中的当前帧)调用。gif drawable 类已经管理了位图池。
一旦 GifDrawable 准备就绪,请使用以下方法循环帧:
GifDrawable gd = (GifDrawable) resource;
Bitmap b = gd.getDecoder().getNextFrame();
Run Code Online (Sandbox Code Playgroud)
请注意,如果您正在使用解码器,您应该从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
///sf/ask/212498471/ //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 */ }
});
Run Code Online (Sandbox Code Playgroud)
在当时,这是最好的方法。由于已经一年多了,我不能保证现在没有更有效的方法来做到这一点。
我使用的库版本是 Glide 3.7.0。访问在最新版本 4.7.+ 中受到限制,但我不确定您需要多长时间才能使用我的方法。
好的,我找到了 3 种可能的解决方案:
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()
}
Run Code Online (Sandbox Code Playgroud)
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")
}
Run Code Online (Sandbox Code Playgroud)
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()
}
}
Run Code Online (Sandbox Code Playgroud)
还有洛蒂:
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")
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1473 次 |
| 最近记录: |