441 lines
12 KiB
Kotlin
441 lines
12 KiB
Kotlin
package com.bumptech.glide.load.resource.gif
|
|
|
|
import com.bumptech.glide.gifdecoder.GifDecoder.TOTAL_ITERATION_COUNT_FOREVER
|
|
|
|
import android.content.Context
|
|
import android.graphics.*
|
|
import android.graphics.drawable.Animatable
|
|
import android.graphics.drawable.Drawable
|
|
import androidx.annotation.VisibleForTesting
|
|
import android.view.Gravity
|
|
import com.bumptech.glide.Glide
|
|
import com.bumptech.glide.gifdecoder.GifDecoder
|
|
import com.bumptech.glide.load.Transformation
|
|
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
|
import com.bumptech.glide.util.Preconditions
|
|
import jp.juggler.util.LogCategory
|
|
import java.lang.reflect.Field
|
|
import java.nio.ByteBuffer
|
|
|
|
/**
|
|
* An animated [android.graphics.drawable.Drawable] that plays the frames of an animated GIF.
|
|
*/
|
|
@Suppress("unused", "UNUSED_PARAMETER", "MemberVisibilityCanPrivate")
|
|
class MyGifDrawable internal constructor(
|
|
state : GifDrawable.GifState
|
|
) : Drawable(), GifFrameLoader.FrameCallback, Animatable {
|
|
|
|
private val state : GifDrawable.GifState
|
|
/**
|
|
* True if the drawable is currently animating.
|
|
*/
|
|
private var isRunning : Boolean = false
|
|
/**
|
|
* True if the drawable should animate while visible.
|
|
*/
|
|
private var isStarted : Boolean = false
|
|
/**
|
|
* True if the drawable's resources have been recycled.
|
|
*/
|
|
|
|
// For testing.
|
|
private var isRecycled : Boolean = false
|
|
|
|
/**
|
|
* True if the drawable is currently visible. Default to true because on certain platforms (at
|
|
* least 4.1.1), setVisible is not called on [Drawables][android.graphics.drawable.Drawable]
|
|
* during [android.widget.ImageView.setImageDrawable].
|
|
* See issue #130.
|
|
*/
|
|
private var isVisibleX = true
|
|
/**
|
|
* The number of times we've looped over all the frames in the GIF.
|
|
*/
|
|
private var loopCount : Int = 0
|
|
/**
|
|
* The number of times to loop through the GIF animation.
|
|
*/
|
|
private var maxLoopCount = LOOP_FOREVER
|
|
|
|
private var applyGravity : Boolean = false
|
|
|
|
private var destRect : Rect? = null
|
|
|
|
val size : Int
|
|
get() = state.frameLoader.size
|
|
|
|
private var mCornerRadius : Float = 0f
|
|
|
|
val firstFrame : Bitmap
|
|
get() = state.frameLoader.firstFrame
|
|
|
|
val frameTransformation : Transformation<Bitmap>
|
|
get() = state.frameLoader.frameTransformation
|
|
|
|
val buffer : ByteBuffer
|
|
get() = state.frameLoader.buffer
|
|
|
|
private val frameCount : Int
|
|
get() = state.frameLoader.frameCount
|
|
|
|
/**
|
|
* Returns the current frame index in the range 0..[.getFrameCount] - 1, or -1 if no frame
|
|
* is displayed.
|
|
*/
|
|
// Public API.
|
|
private val frameIndex : Int
|
|
get() = state.frameLoader.currentIndex
|
|
|
|
/**
|
|
* Constructor for GifDrawable.
|
|
*
|
|
* @param context A context.
|
|
* @param bitmapPool Ignored, see deprecation note.
|
|
* @param frameTransformation An [com.bumptech.glide.load.Transformation] that can be
|
|
* applied to each frame.
|
|
* @param targetFrameWidth The desired width of the frames displayed by this drawable (the
|
|
* width of the view or
|
|
* [com.bumptech.glide.request.target.Target]
|
|
* this drawable is being loaded into).
|
|
* @param targetFrameHeight The desired height of the frames displayed by this drawable (the
|
|
* height of the view or
|
|
* [com.bumptech.glide.request.target.Target]
|
|
* this drawable is being loaded into).
|
|
* @param gifDecoder The decoder to use to decode GIF data.
|
|
* @param firstFrame The decoded and transformed first frame of this GIF.
|
|
* @see .setFrameTransformation
|
|
*/
|
|
@Deprecated("Use {@link #GifDrawable(Context, GifDecoder, Transformation, int, int, Bitmap)}")
|
|
constructor(
|
|
context : Context,
|
|
gifDecoder : GifDecoder,
|
|
bitmapPool : BitmapPool,
|
|
frameTransformation : Transformation<Bitmap>,
|
|
targetFrameWidth : Int,
|
|
targetFrameHeight : Int,
|
|
firstFrame : Bitmap) : this(context, gifDecoder, frameTransformation, targetFrameWidth, targetFrameHeight, firstFrame)
|
|
|
|
/**
|
|
* Constructor for GifDrawable.
|
|
*
|
|
* @param context A context.
|
|
* @param frameTransformation An [com.bumptech.glide.load.Transformation] that can be
|
|
* applied to each frame.
|
|
* @param targetFrameWidth The desired width of the frames displayed by this drawable (the
|
|
* width of the view or
|
|
* [com.bumptech.glide.request.target.Target]
|
|
* this drawable is being loaded into).
|
|
* @param targetFrameHeight The desired height of the frames displayed by this drawable (the
|
|
* height of the view or
|
|
* [com.bumptech.glide.request.target.Target]
|
|
* this drawable is being loaded into).
|
|
* @param gifDecoder The decoder to use to decode GIF data.
|
|
* @param firstFrame The decoded and transformed first frame of this GIF.
|
|
* @see .setFrameTransformation
|
|
*/
|
|
constructor(
|
|
context : Context,
|
|
gifDecoder : GifDecoder,
|
|
frameTransformation : Transformation<Bitmap>,
|
|
targetFrameWidth : Int,
|
|
targetFrameHeight : Int,
|
|
firstFrame : Bitmap
|
|
) : this(
|
|
GifDrawable.GifState(
|
|
GifFrameLoader(
|
|
// XXX(b/27524013): Factor out this call to Glide.get()
|
|
Glide.get(context),
|
|
gifDecoder,
|
|
targetFrameWidth,
|
|
targetFrameHeight,
|
|
frameTransformation,
|
|
firstFrame)
|
|
)
|
|
)
|
|
|
|
constructor(other : GifDrawable, radius : Float) : this(cloneState(other)) {
|
|
this.mCornerRadius = radius
|
|
}
|
|
|
|
init {
|
|
this.state = Preconditions.checkNotNull(state)
|
|
}
|
|
|
|
@VisibleForTesting
|
|
internal constructor(frameLoader : GifFrameLoader, paint : Paint) : this(GifDrawable.GifState(frameLoader)) {
|
|
this.paintX = paint
|
|
}
|
|
|
|
// Public API.
|
|
fun setFrameTransformation(frameTransformation : Transformation<Bitmap>,
|
|
firstFrame : Bitmap) {
|
|
state.frameLoader.setFrameTransformation(frameTransformation, firstFrame)
|
|
}
|
|
|
|
private fun resetLoopCount() {
|
|
loopCount = 0
|
|
}
|
|
|
|
/**
|
|
* Starts the animation from the first frame. Can only be called while animation is not running.
|
|
*/
|
|
// Public API.
|
|
fun startFromFirstFrame() {
|
|
Preconditions.checkArgument(! isRunning, "You cannot restart a currently running animation.")
|
|
state.frameLoader.setNextStartFromFirstFrame()
|
|
start()
|
|
}
|
|
|
|
override fun start() {
|
|
isStarted = true
|
|
resetLoopCount()
|
|
if(isVisibleX) {
|
|
startRunning()
|
|
}
|
|
}
|
|
|
|
override fun stop() {
|
|
isStarted = false
|
|
stopRunning()
|
|
}
|
|
|
|
private fun startRunning() {
|
|
Preconditions.checkArgument(! isRecycled, "You cannot start a recycled Drawable. Ensure that" + "you clear any references to the Drawable when clearing the corresponding request.")
|
|
// If we have only a single frame, we don't want to decode it endlessly.
|
|
if(state.frameLoader.frameCount == 1) {
|
|
invalidateSelf()
|
|
} else if(! isRunning) {
|
|
isRunning = true
|
|
state.frameLoader.subscribe(this)
|
|
invalidateSelf()
|
|
}
|
|
}
|
|
|
|
private fun stopRunning() {
|
|
isRunning = false
|
|
state.frameLoader.unsubscribe(this)
|
|
}
|
|
|
|
override fun setVisible(visible : Boolean, restart : Boolean) : Boolean {
|
|
Preconditions.checkArgument(! isRecycled, "Cannot change the visibility of a recycled resource."
|
|
+ " Ensure that you unset the Drawable from your View before changing the View's"
|
|
+ " visibility.")
|
|
isVisibleX = visible
|
|
if(! visible) {
|
|
stopRunning()
|
|
} else if(isStarted) {
|
|
startRunning()
|
|
}
|
|
return super.setVisible(visible, restart)
|
|
}
|
|
|
|
override fun getIntrinsicWidth() : Int {
|
|
return state.frameLoader.width
|
|
}
|
|
|
|
override fun getIntrinsicHeight() : Int {
|
|
return state.frameLoader.height
|
|
}
|
|
|
|
override fun isRunning() : Boolean {
|
|
return isRunning
|
|
}
|
|
|
|
// For testing.
|
|
internal fun setIsRunning(isRunning : Boolean) {
|
|
this.isRunning = isRunning
|
|
}
|
|
|
|
override fun onBoundsChange(bounds : Rect) {
|
|
super.onBoundsChange(bounds)
|
|
applyGravity = true
|
|
}
|
|
|
|
override fun draw(canvas : Canvas) {
|
|
if(isRecycled) {
|
|
return
|
|
}
|
|
|
|
if(applyGravity) {
|
|
Gravity.apply(GRAVITY, intrinsicWidth, intrinsicHeight, bounds, getDestRect())
|
|
applyGravity = false
|
|
}
|
|
|
|
val currentFrame = state.frameLoader.currentFrame
|
|
|
|
if(mCornerRadius <= 0f) {
|
|
val paint = getPaint()
|
|
paint.shader = null
|
|
canvas.drawBitmap(currentFrame, null, getDestRect(), paint)
|
|
} else {
|
|
drawRoundImage(canvas, currentFrame)
|
|
}
|
|
|
|
}
|
|
|
|
private val mShaderMatrix = Matrix()
|
|
private val mDstRectF = RectF()
|
|
|
|
private fun drawRoundImage(canvas : Canvas, src : Bitmap?) {
|
|
if(src == null) return
|
|
val src_w = src.width
|
|
val src_h = src.height
|
|
if(src_w < 1 || src_h < 1) return
|
|
// int outWidth = destRect.width();
|
|
// int outHeight = destRect.height();
|
|
|
|
mDstRectF.set(destRect)
|
|
mShaderMatrix.reset()
|
|
mShaderMatrix.preScale(mDstRectF.width() / src_w, mDstRectF.height() / src_h)
|
|
|
|
val mBitmapShader = BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
|
mBitmapShader.setLocalMatrix(mShaderMatrix)
|
|
|
|
val paint = getPaint()
|
|
paint.shader = mBitmapShader
|
|
canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint)
|
|
|
|
}
|
|
|
|
override fun setAlpha(i : Int) {
|
|
getPaint().alpha = i
|
|
}
|
|
|
|
override fun setColorFilter(colorFilter : ColorFilter?) {
|
|
getPaint().colorFilter = colorFilter
|
|
}
|
|
|
|
private fun getDestRect() : Rect {
|
|
var r = destRect
|
|
if(r == null) {
|
|
r = Rect()
|
|
destRect = r
|
|
}
|
|
return r
|
|
}
|
|
|
|
private var paintX : Paint? = null
|
|
private fun getPaint() : Paint {
|
|
var p = paintX
|
|
if(p == null) {
|
|
p = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
paintX = p
|
|
}
|
|
return p
|
|
}
|
|
|
|
override fun getOpacity() : Int {
|
|
// We can't tell, so default to transparent to be safe.
|
|
return PixelFormat.TRANSPARENT
|
|
}
|
|
|
|
// See #1087.
|
|
private fun findCallback() : Drawable.Callback? {
|
|
var callback : Drawable.Callback? = callback
|
|
while(callback is Drawable) {
|
|
callback = (callback as Drawable).callback
|
|
}
|
|
return callback
|
|
}
|
|
|
|
override fun onFrameReady() {
|
|
if(findCallback() == null) {
|
|
stop()
|
|
invalidateSelf()
|
|
return
|
|
}
|
|
|
|
invalidateSelf()
|
|
|
|
if(frameIndex == frameCount - 1) {
|
|
loopCount ++
|
|
}
|
|
|
|
if(maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
|
|
stop()
|
|
}
|
|
}
|
|
|
|
override fun getConstantState() : Drawable.ConstantState? {
|
|
return state
|
|
}
|
|
|
|
/**
|
|
* Clears any resources for loading frames that are currently held on to by this object.
|
|
*/
|
|
fun recycle() {
|
|
isRecycled = true
|
|
state.frameLoader.clear()
|
|
}
|
|
|
|
// Public API.
|
|
fun setLoopCount(loopCount : Int) {
|
|
if(loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
|
|
throw IllegalArgumentException("Loop count must be greater than 0, or equal to " + "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC")
|
|
}
|
|
|
|
maxLoopCount = if(loopCount == LOOP_INTRINSIC) {
|
|
val intrinsicCount = state.frameLoader.loopCount
|
|
if(intrinsicCount == TOTAL_ITERATION_COUNT_FOREVER) LOOP_FOREVER else intrinsicCount
|
|
} else {
|
|
loopCount
|
|
}
|
|
}
|
|
|
|
// internal class GifState(@field:VisibleForTesting
|
|
// val frameLoader : GifFrameLoader) : Drawable.ConstantState() {
|
|
//
|
|
// override fun newDrawable(res : Resources?) : Drawable {
|
|
// return newDrawable()
|
|
// }
|
|
//
|
|
// override fun newDrawable() : Drawable {
|
|
// return MyGifDrawable(this)
|
|
// }
|
|
//
|
|
// override fun getChangingConfigurations() : Int {
|
|
// return 0
|
|
// }
|
|
// }
|
|
|
|
companion object {
|
|
const val LOOP_FOREVER = GifDrawable.LOOP_FOREVER
|
|
const val LOOP_INTRINSIC = GifDrawable.LOOP_INTRINSIC
|
|
|
|
private const val GRAVITY = Gravity.FILL
|
|
|
|
//////////////////////////////////////////////////////////////////
|
|
|
|
internal val log = LogCategory("MyGifDrawable")
|
|
|
|
private val field_state : Field by lazy {
|
|
val rv = GifDrawable::class.java.getDeclaredField("state")
|
|
rv.isAccessible = true
|
|
rv
|
|
}
|
|
|
|
internal fun cloneState(other : GifDrawable) : GifDrawable.GifState {
|
|
try {
|
|
val other_state = field_state.get(other) as GifDrawable.GifState
|
|
val frameLoader : GifFrameLoader = other_state.frameLoader
|
|
|
|
return GifDrawable.GifState(frameLoader)
|
|
|
|
// other_state.gifHeader,
|
|
// other_state.data,
|
|
// other_state.context,
|
|
// other.frameTransformation,
|
|
// other_state.targetWidth,
|
|
// other_state.targetHeight,
|
|
// other_state.bitmapProvider,
|
|
// other_state.bitmapPool,
|
|
// other.firstFrame
|
|
} catch(ex : Throwable) {
|
|
throw RuntimeException("cloning GifDrawable.GifState failed.", ex)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|