blob: 47e06fb52a7d1896f013ed7b5086be7ec7c49f5d [file] [log] [blame]
package com.bumptech.glide.load.resource.gif;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.Gravity;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.gifdecoder.GifHeader;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
/**
* An animated {@link android.graphics.drawable.Drawable} that plays the frames of an animated GIF.
*/
public class GifDrawable extends GlideDrawable implements GifFrameManager.FrameCallback {
private final Paint paint = new Paint();
private final Rect destRect = new Rect();
private final GifFrameManager frameManager;
private final GifState state;
private final GifDecoder decoder;
/** True if the drawable is currently animating. */
private boolean isRunning;
/** True if the drawable should animate while visible. */
private boolean isStarted;
/** True if the drawable's resources have been recycled. */
private boolean isRecycled;
/**
* True if the drawable is currently visible. Default to true because on certain platforms (at least 4.1.1),
* setVisible is not called on {@link android.graphics.drawable.Drawable Drawables} during
* {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. See issue #130.
*/
private boolean isVisible = true;
/** The number of times we've looped over all the frames in the gif. */
private int loopCount;
/** The number of times to loop through the gif animation. */
private int maxLoopCount = LOOP_FOREVER;
private boolean applyGravity;
/**
* Constructor for GifDrawable.
*
* @see #setFrameTransformation(com.bumptech.glide.load.Transformation, android.graphics.Bitmap)
*
* @param context A context.
* @param bitmapProvider An {@link com.bumptech.glide.gifdecoder.GifDecoder.BitmapProvider} that can be used to
* retrieve re-usable {@link android.graphics.Bitmap}s.
* @param bitmapPool A {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} that can be used to return
* the first frame when this drawable is recycled.
* @param frameTransformation An {@link 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
* {@link 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
* {@link com.bumptech.glide.request.target.Target} this drawable is being loaded into).
* @param gifHeader The header data for this gif.
* @param data The full bytes of the gif.
* @param firstFrame The decoded and transformed first frame of this gif.
*/
public GifDrawable(Context context, GifDecoder.BitmapProvider bitmapProvider, BitmapPool bitmapPool,
Transformation<Bitmap> frameTransformation, int targetFrameWidth, int targetFrameHeight,
GifHeader gifHeader, byte[] data, Bitmap firstFrame) {
this(new GifState(gifHeader, data, context, frameTransformation, targetFrameWidth, targetFrameHeight,
bitmapProvider, bitmapPool, firstFrame));
}
GifDrawable(GifState state) {
if (state == null) {
throw new NullPointerException("GifState must not be null");
}
this.state = state;
this.decoder = new GifDecoder(state.bitmapProvider);
decoder.setData(state.gifHeader, state.data);
frameManager = new GifFrameManager(state.context, decoder, state.targetWidth, state.targetHeight);
frameManager.setFrameTransformation(state.frameTransformation);
}
// Visible for testing.
GifDrawable(GifDecoder decoder, GifFrameManager frameManager, Bitmap firstFrame, BitmapPool bitmapPool) {
this.decoder = decoder;
this.frameManager = frameManager;
this.state = new GifState(null);
state.bitmapPool = bitmapPool;
state.firstFrame = firstFrame;
}
public Bitmap getFirstFrame() {
return state.firstFrame;
}
public void setFrameTransformation(Transformation<Bitmap> frameTransformation, Bitmap firstFrame) {
if (firstFrame == null) {
throw new NullPointerException("The first frame of the GIF must not be null");
}
if (frameTransformation == null) {
throw new NullPointerException("The frame transformation must not be null");
}
state.frameTransformation = frameTransformation;
state.firstFrame = firstFrame;
frameManager.setFrameTransformation(frameTransformation);
}
public GifDecoder getDecoder() {
return decoder;
}
public Transformation<Bitmap> getFrameTransformation() {
return state.frameTransformation;
}
public byte[] getData() {
return state.data;
}
public int getFrameCount() {
return decoder.getFrameCount();
}
private void resetLoopCount() {
loopCount = 0;
}
@Override
public void start() {
isStarted = true;
resetLoopCount();
if (isVisible) {
startRunning();
}
}
@Override
public void stop() {
isStarted = false;
stopRunning();
// On APIs > honeycomb we know our drawable is not being displayed anymore when it's callback is cleared and so
// we can use the absence of a callback as an indication that it's ok to clear our temporary data. Prior to
// honeycomb we can't tell if our callback is null and instead eagerly reset to avoid holding on to resources we
// no longer need.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
reset();
}
}
/**
* Clears temporary data and resets the drawable back to the first frame.
*/
private void reset() {
frameManager.clear();
invalidateSelf();
}
private void startRunning() {
// If we have only a single frame, we don't want to decode it endlessly.
if (decoder.getFrameCount() == 1) {
invalidateSelf();
} else if (!isRunning) {
isRunning = true;
frameManager.getNextFrame(this);
invalidateSelf();
}
}
private void stopRunning() {
isRunning = false;
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
isVisible = visible;
if (!visible) {
stopRunning();
} else if (isStarted) {
startRunning();
}
return super.setVisible(visible, restart);
}
@Override
public int getIntrinsicWidth() {
return state.firstFrame.getWidth();
}
@Override
public int getIntrinsicHeight() {
return state.firstFrame.getHeight();
}
@Override
public boolean isRunning() {
return isRunning;
}
// For testing.
void setIsRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
applyGravity = true;
}
@Override
public void draw(Canvas canvas) {
if (isRecycled) {
return;
}
if (applyGravity) {
Gravity.apply(GifState.GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), destRect);
applyGravity = false;
}
Bitmap currentFrame = frameManager.getCurrentFrame();
Bitmap toDraw = currentFrame != null ? currentFrame : state.firstFrame;
canvas.drawBitmap(toDraw, null, destRect, paint);
}
@Override
public void setAlpha(int i) {
paint.setAlpha(i);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
// We can't tell, so default to transparent to be safe.
return PixelFormat.TRANSPARENT;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onFrameRead(int frameIndex) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && getCallback() == null) {
stop();
reset();
return;
}
if (!isRunning) {
return;
}
invalidateSelf();
if (frameIndex == decoder.getFrameCount() - 1) {
loopCount++;
}
if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
stop();
} else {
frameManager.getNextFrame(this);
}
}
@Override
public ConstantState getConstantState() {
return state;
}
/**
* Clears any resources for loading frames that are currently held on to by this object.
*/
public void recycle() {
isRecycled = true;
state.bitmapPool.put(state.firstFrame);
frameManager.clear();
}
// For testing.
boolean isRecycled() {
return isRecycled;
}
@Override
public boolean isAnimated() {
return true;
}
@Override
public void setLoopCount(int loopCount) {
if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
throw new IllegalArgumentException("Loop count must be greater than 0, or equal to "
+ "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC");
}
if (loopCount == LOOP_INTRINSIC) {
maxLoopCount = decoder.getLoopCount();
} else {
maxLoopCount = loopCount;
}
}
static class GifState extends ConstantState {
private static final int GRAVITY = Gravity.FILL;
GifHeader gifHeader;
byte[] data;
Context context;
Transformation<Bitmap> frameTransformation;
int targetWidth;
int targetHeight;
GifDecoder.BitmapProvider bitmapProvider;
BitmapPool bitmapPool;
Bitmap firstFrame;
public GifState(GifHeader header, byte[] data, Context context,
Transformation<Bitmap> frameTransformation, int targetWidth, int targetHeight,
GifDecoder.BitmapProvider provider, BitmapPool bitmapPool, Bitmap firstFrame) {
if (firstFrame == null) {
throw new NullPointerException("The first frame of the GIF must not be null");
}
gifHeader = header;
this.data = data;
this.bitmapPool = bitmapPool;
this.firstFrame = firstFrame;
this.context = context.getApplicationContext();
this.frameTransformation = frameTransformation;
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
bitmapProvider = provider;
}
public GifState(GifState original) {
if (original != null) {
gifHeader = original.gifHeader;
data = original.data;
context = original.context;
frameTransformation = original.frameTransformation;
targetWidth = original.targetWidth;
targetHeight = original.targetHeight;
bitmapProvider = original.bitmapProvider;
bitmapPool = original.bitmapPool;
firstFrame = original.firstFrame;
}
}
@Override
public Drawable newDrawable(Resources res) {
return newDrawable();
}
@Override
public Drawable newDrawable() {
return new GifDrawable(this);
}
@Override
public int getChangingConfigurations() {
return 0;
}
}
}