2018-01-10 16:47:35 +01:00
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
2019-02-15 02:51:22 +01:00
import androidx.annotation.VisibleForTesting
2018-01-10 16:47:35 +01:00
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
2018-12-01 00:02:18 +01:00
import jp.juggler.util.LogCategory
2018-01-10 16:47:35 +01:00
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 " )
2018-01-18 21:30:22 +01:00
class MyGifDrawable internal constructor (
state : GifDrawable . GifState
) : Drawable ( ) , GifFrameLoader . FrameCallback , Animatable {
2018-01-10 16:47:35 +01:00
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 .
* /
2018-01-21 13:46:36 +01:00
2018-01-10 16:47:35 +01:00
// For testing.
2018-01-21 13:46:36 +01:00
private var isRecycled : Boolean = false
2018-01-10 16:47:35 +01:00
/ * *
* 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
2018-01-21 13:46:36 +01:00
private val frameCount : Int
2018-01-10 16:47:35 +01:00
get ( ) = state . frameLoader . frameCount
/ * *
* Returns the current frame index in the range 0. . [ . getFrameCount ] - 1 , or - 1 if no frame
* is displayed .
* /
// Public API.
2018-01-21 13:46:36 +01:00
private val frameIndex : Int
2018-01-10 16:47:35 +01:00
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 (
2018-01-17 18:39:16 +01:00
// XXX(b/27524013): Factor out this call to Glide.get()
2018-01-10 16:47:35 +01:00
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 {
2018-01-18 21:30:22 +01:00
const val LOOP _FOREVER = GifDrawable . LOOP _FOREVER
const val LOOP _INTRINSIC = GifDrawable . LOOP _INTRINSIC
private const val GRAVITY = Gravity . FILL
2018-01-10 16:47:35 +01:00
//////////////////////////////////////////////////////////////////
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 )
}
}
}
}