package nya.miku.wishmaster.lib.gifdrawable;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.StrictMode;
import android.os.SystemClock;
import android.widget.MediaController.MediaPlayerControl;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A {@link Drawable} which can be used to hold GIF images, especially animations.
* Basic GIF metadata can be also obtained.
*
* @author koral--
*/
public class GifDrawable extends Drawable implements Animatable, MediaPlayerControl {
static {
System.loadLibrary("gif");
}
/**
* Decodes a frame if needed.
*
* @param pixels frame destination
* @param gifFileInPtr GifInfo pointer
* @param metaData metadata array
* @return true if loop of the animation is completed
*/
private static native boolean renderFrame(int[] pixels, long gifFileInPtr, int[] metaData);
static native long openFd(int[] metaData, FileDescriptor fd, long offset, boolean justDecodeMetaData) throws GifIOException;
static native long openByteArray(int[] metaData, byte[] bytes, boolean justDecodeMetaData) throws GifIOException;
static native long openDirectByteBuffer(int[] metaData, ByteBuffer buffer, boolean justDecodeMetaData) throws GifIOException;
static native long openStream(int[] metaData, InputStream stream, boolean justDecodeMetaData) throws GifIOException;
static native long openFile(int[] metaData, String filePath, boolean justDecodeMetaData) throws GifIOException;
static native void free(long gifFileInPtr);
private static native void reset(long gifFileInPtr);
private static native void setSpeedFactor(long gifFileInPtr, float factor);
private static native String getComment(long gifFileInPtr);
static native int getLoopCount(long gifFileInPtr);
static native int getDuration(long gifFileInPtr);
private static native int getCurrentPosition(long gifFileInPtr);
private static native void seekToTime(long gifFileInPtr, int pos, int[] pixels);
private static native void seekToFrame(long gifFileInPtr, int frameNr, int[] pixels);
private static native void saveRemainder(long gifFileInPtr);
private static native void restoreRemainder(long gifFileInPtr);
private static native long getAllocationByteCount(long gifFileInPtr);
private volatile long mGifInfoPtr;
private volatile boolean mIsRunning = true;
private final int[] mMetaData = new int[5];//[w,h,imageCount,errorCode,post invalidation time]
private final long mInputSourceLength;
private float mSx = 1f;
private float mSy = 1f;
private boolean mApplyTransformation;
private final Rect mDstRect = new Rect();
/**
* Paint used to draw on a Canvas
*/
protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
/**
* Frame buffer, holds current frame.
* Each element is a packed int representing a {@link Color} at the given pixel.
*/
private int[] mColors;
private final ConcurrentLinkedQueue<AnimationListener> mListeners = new ConcurrentLinkedQueue<>();
private final Runnable mResetTask = new Runnable() {
@Override
public void run() {
reset(mGifInfoPtr);
}
};
private final Runnable mStartTask = new Runnable() {
@Override
public void run() {
restoreRemainder(mGifInfoPtr);
invalidateSelf();
}
};
private final Runnable mSaveRemainderTask = new Runnable() {
@Override
public void run() {
saveRemainder(mGifInfoPtr);
}
};
private final Runnable mInvalidateTask = new Runnable() {
@Override
public void run() {
invalidateSelf();
}
};
private void runOnUiThread(Runnable task) {
scheduleSelf(task, SystemClock.uptimeMillis());
}
/**
* Creates drawable from resource.
*
* @param res Resources to read from
* @param id resource id
* @throws NotFoundException if the given ID does not exist.
* @throws IOException when opening failed
* @throws NullPointerException if res is null
*/
public GifDrawable(Resources res, int id) throws NotFoundException, IOException {
this(res.openRawResourceFd(id));
}
/**
* Creates drawable from asset.
*
* @param assets AssetManager to read from
* @param assetName name of the asset
* @throws IOException when opening failed
* @throws NullPointerException if assets or assetName is null
*/
public GifDrawable(AssetManager assets, String assetName) throws IOException {
this(assets.openFd(assetName));
}
/**
* Constructs drawable from given file path.<br>
* Only metadata is read, no graphic data is decoded here.
* In practice can be called from main thread. However it will violate
* {@link StrictMode} policy if disk reads detection is enabled.<br>
*
* @param filePath path to the GIF file
* @throws IOException when opening failed
* @throws NullPointerException if filePath is null
*/
public GifDrawable(String filePath) throws IOException {
if (filePath == null)
throw new NullPointerException("Source is null");
mInputSourceLength = new File(filePath).length();
mGifInfoPtr = openFile(mMetaData, filePath, false);
mColors = new int[mMetaData[0] * mMetaData[1]];
}
/**
* Equivalent to {@code} GifDrawable(file.getPath())}
*
* @param file the GIF file
* @throws IOException when opening failed
* @throws NullPointerException if file is null
*/
public GifDrawable(File file) throws IOException {
if (file == null)
throw new NullPointerException("Source is null");
mInputSourceLength = file.length();
mGifInfoPtr = openFile(mMetaData, file.getPath(), false);
mColors = new int[mMetaData[0] * mMetaData[1]];
}
/**
* Creates drawable from InputStream.
* InputStream must support marking, IllegalArgumentException will be thrown otherwise.
*
* @param stream stream to read from
* @throws IOException when opening failed
* @throws IllegalArgumentException if stream does not support marking
* @throws NullPointerException if stream is null
*/
public GifDrawable(InputStream stream) throws IOException {
if (stream == null)
throw new NullPointerException("Source is null");
if (!stream.markSupported())
throw new IllegalArgumentException("InputStream does not support marking");
mGifInfoPtr = openStream(mMetaData, stream, false);
mColors = new int[mMetaData[0] * mMetaData[1]];
mInputSourceLength = -1L;
}
/**
* Creates drawable from AssetFileDescriptor.
* Convenience wrapper for {@link GifDrawable#GifDrawable(FileDescriptor)}
*
* @param afd source
* @throws NullPointerException if afd is null
* @throws IOException when opening failed
*/
public GifDrawable(AssetFileDescriptor afd) throws IOException {
if (afd == null)
throw new NullPointerException("Source is null");
FileDescriptor fd = afd.getFileDescriptor();
try {
mGifInfoPtr = openFd(mMetaData, fd, afd.getStartOffset(), false);
} catch (IOException ex) {
afd.close();
throw ex;
}
mColors = new int[mMetaData[0] * mMetaData[1]];
mInputSourceLength = afd.getLength();
}
/**
* Creates drawable from FileDescriptor
*
* @param fd source
* @throws IOException when opening failed
* @throws NullPointerException if fd is null
*/
public GifDrawable(FileDescriptor fd) throws IOException {
if (fd == null)
throw new NullPointerException("Source is null");
mGifInfoPtr = openFd(mMetaData, fd, 0, false);
mColors = new int[mMetaData[0] * mMetaData[1]];
mInputSourceLength = -1L;
}
/**
* Creates drawable from byte array.<br>
* It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
*
* @param bytes raw GIF bytes
* @throws IOException if bytes does not contain valid GIF data
* @throws NullPointerException if bytes are null
*/
public GifDrawable(byte[] bytes) throws IOException {
if (bytes == null)
throw new NullPointerException("Source is null");
mGifInfoPtr = openByteArray(mMetaData, bytes, false);
mColors = new int[mMetaData[0] * mMetaData[1]];
mInputSourceLength = bytes.length;
}
/**
* Creates drawable from {@link ByteBuffer}. Only direct buffers are supported.
* Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
*
* @param buffer buffer containing GIF data
* @throws IOException if buffer does not contain valid GIF data
* @throws IllegalArgumentException if buffer is indirect
* @throws NullPointerException if buffer is null
*/
public GifDrawable(ByteBuffer buffer) throws IOException {
if (buffer == null)
throw new NullPointerException("Source is null");
if (!buffer.isDirect())
throw new IllegalArgumentException("ByteBuffer is not direct");
mGifInfoPtr = openDirectByteBuffer(mMetaData, buffer, false);
mColors = new int[mMetaData[0] * mMetaData[1]];
mInputSourceLength = buffer.capacity();
}
/**
* Creates drawable from {@link android.net.Uri} which is resolved using {@code resolver}.
* {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)}
* is used to open an Uri.
*
* @param uri GIF Uri, cannot be null.
* @param resolver resolver, cannot be null.
* @throws IOException if resolution fails or destination is not a GIF.
*/
public GifDrawable(ContentResolver resolver, Uri uri) throws IOException {
this(resolver.openAssetFileDescriptor(uri, "r"));
}
/**
* Frees any memory allocated native way.
* Operation is irreversible. After this call, nothing will be drawn.
* This method is idempotent, subsequent calls have no effect.
* Like {@link android.graphics.Bitmap#recycle()} this is an advanced call and
* is invoked implicitly by finalizer.
*/
public void recycle() {
mIsRunning = false;
long tmpPtr = mGifInfoPtr;
mGifInfoPtr = 0L;
mColors = null;
free(tmpPtr);
}
@Override
protected void finalize() throws Throwable {
try {
recycle();
} finally {
super.finalize();
}
}
@Override
public int getIntrinsicHeight() {
return mMetaData[1];
}
@Override
public int getIntrinsicWidth() {
return mMetaData[0];
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
/**
* See {@link Drawable#getOpacity()}
*
* @return always {@link PixelFormat#TRANSPARENT}
*/
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
/**
* Starts the animation. Does nothing if GIF is not animated.
* This method is thread-safe.
*/
@Override
public void start() {
mIsRunning = true;
runOnUiThread(mStartTask);
}
/**
* Causes the animation to start over.
* If animation is stopped any effects will occur after restart.<br>
* If rewinding input source fails then state is not affected.
* This method is thread-safe.
*/
public void reset() {
runOnUiThread(mResetTask);
}
/**
* Stops the animation. Does nothing if GIF is not animated.
* This method is thread-safe.
*/
@Override
public void stop() {
mIsRunning = false;
runOnUiThread(mSaveRemainderTask);
}
@Override
public boolean isRunning() {
return mIsRunning;
}
/**
* Returns GIF comment
*
* @return comment or null if there is no one defined in file
*/
public String getComment() {
return getComment(mGifInfoPtr);
}
/**
* Returns loop count previously read from GIF's application extension block.
* Defaults to 0 (infinite loop) if there is no such extension.
*
* @return loop count, 0 means that animation is infinite
*/
public int getLoopCount() {
return getLoopCount(mGifInfoPtr);
}
/**
* @return basic description of the GIF including size and number of frames
*/
@Override
public String toString() {
return String.format(Locale.US, "GIF: size: %dx%d, frames: %d, error: %d", mMetaData[0], mMetaData[1], mMetaData[2], mMetaData[3]);
}
/**
* @return number of frames in GIF, at least one
*/
public int getNumberOfFrames() {
return mMetaData[2];
}
/**
* Retrieves last error which is also the indicator of current GIF status.
*
* @return current error or {@link GifError#NO_ERROR} if there was no error
*/
public GifError getError() {
return GifError.fromCode(mMetaData[3]);
}
/**
* An {@link GifDrawable#GifDrawable(Resources, int)} wrapper but returns null
* instead of throwing exception if creation fails.
*
* @param res resources to read from
* @param resourceId resource id
* @return correct drawable or null if creation failed
*/
public static GifDrawable createFromResource(Resources res, int resourceId) {
try {
return new GifDrawable(res, resourceId);
} catch (IOException ignored) {
//ignored
}
return null;
}
/**
* Sets new animation speed factor.<br>
* Note: If animation is in progress ({@link #draw(Canvas)}) was already called)
* then effects will be visible starting from the next frame. Duration of the currently rendered
* frame is not affected.
*
* @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed
* @throws IllegalArgumentException if factor<=0
*/
public void setSpeed(float factor) {
if (factor <= 0f)
throw new IllegalArgumentException("Speed factor is not positive");
setSpeedFactor(mGifInfoPtr, factor);
}
/**
* Equivalent of {@link #stop()}
*/
@Override
public void pause() {
stop();
}
/**
* Retrieves duration of one loop of the animation.
* If there is no data (no Graphics Control Extension blocks) 0 is returned.
* Note that one-frame GIFs can have non-zero duration defined in Graphics Control Extension block,
* use {@link #getNumberOfFrames()} to determine if there is one or more frames.
*
* @return duration of of one loop the animation in milliseconds. Result is always multiple of 10.
*/
@Override
public int getDuration() {
return getDuration(mGifInfoPtr);
}
/**
* Retrieves elapsed time from the beginning of a current loop of animation.
* If there is only 1 frame, 0 is returned.
*
* @return elapsed time from the beginning of a loop in ms
*/
@Override
public int getCurrentPosition() {
return getCurrentPosition(mGifInfoPtr);
}
/**
* Seeks animation to given absolute position (within given loop) and refreshes the canvas.<br>
* <b>NOTE: only seeking forward is supported.</b><br>
* If position is less than current position or GIF has only one frame then nothing happens.
* If position is greater than duration of the loop of animation
* (or whole animation if there is no loop) then animation will be sought to the end.<br>
* NOTE: all frames from current to desired must be rendered sequentially to perform seeking.
* It may take a lot of time if number of such frames is large.
* This method can be called from any thread but actual work will be performed on UI thread.
*
* @param position position to seek to in milliseconds
* @throws IllegalArgumentException if position<0
*/
@Override
public void seekTo(final int position) {
if (position < 0)
throw new IllegalArgumentException("Position is not positive");
runOnUiThread(new Runnable() {
@Override
public void run() {
seekToTime(mGifInfoPtr, position, mColors);
invalidateSelf();
}
});
}
/**
* Like {@link #seekToTime(long, int, int[])} but uses index of the frame instead of time.
*
* @param frameIndex index of the frame to seek to (zero based)
* @throws IllegalArgumentException if frameIndex<0
*/
public void seekToFrame(final int frameIndex) {
if (frameIndex < 0)
throw new IllegalArgumentException("frameIndex is not positive");
runOnUiThread(new Runnable() {
@Override
public void run() {
seekToFrame(mGifInfoPtr, frameIndex, mColors);
invalidateSelf();
}
});
}
/**
* Equivalent of {@link #isRunning()}
*
* @return true if animation is running
*/
@Override
public boolean isPlaying() {
return mIsRunning;
}
/**
* Used by MediaPlayer for secondary progress bars.
* There is no buffer in GifDrawable, so buffer is assumed to be always full.
*
* @return always 100
*/
@Override
public int getBufferPercentage() {
return 100;
}
/**
* Checks whether pause is supported.
*
* @return always true, even if there is only one frame
*/
@Override
public boolean canPause() {
return true;
}
/**
* Checks whether seeking backward can be performed.
* Due to different frame disposal methods it is not supported now.
*
* @return always false
*/
@Override
public boolean canSeekBackward() {
return false;
}
/**
* Checks whether seeking forward can be performed.
*
* @return true if GIF has at least 2 frames
*/
@Override
public boolean canSeekForward() {
return getNumberOfFrames() > 1;
}
/**
* Used by MediaPlayer.
* GIFs contain no sound, so 0 is always returned.
*
* @return always 0
*/
@Override
public int getAudioSessionId() {
return 0;
}
/**
* Returns the minimum number of bytes that can be used to store pixels of the single frame.
* Returned value is the same for all the frames since it is based on the size of GIF screen.
*
* @return width * height (of the GIF screen ix pixels) * 4
*/
public int getFrameByteCount() {
return mMetaData[0] * mMetaData[1] * 4;
}
/**
* Returns size of the allocated memory used to store pixels of this object.
* It counts length of all frame buffers. Returned value does not change during runtime.
*
* @return size of the allocated memory used to store pixels of this object
*/
public long getAllocationByteCount() {
long nativeSize = getAllocationByteCount(mGifInfoPtr);
final int[] colors = mColors;
if (colors == null)
return nativeSize;
return nativeSize + colors.length * 4;
}
/**
* Returns length of the input source obtained at the opening time or -1 if
* length is unknown. Returned value does not change during runtime.
* For GifDrawables constructed from {@link InputStream} and {@link FileDescriptor} -1 is always returned.
* In case of {@link File}, file path, byte array and {@link ByteBuffer} length is always known.
*
* @return number of bytes backed by input source or -1 if it is unknown
*/
public long getInputSourceByteCount() {
return mInputSourceLength;
}
/**
* Returns in pixels[] a copy of the data in the current frame. Each value is a packed int representing a {@link Color}.
* If GifDrawable is recycled pixels[] is left unchanged.
*
* @param pixels the array to receive the frame's colors
* @throws ArrayIndexOutOfBoundsException if the pixels array is too small to receive required number of pixels
*/
public void getPixels(int[] pixels) {
final int[] colors = mColors;
if (colors == null)
return;
if (pixels.length < colors.length)
throw new ArrayIndexOutOfBoundsException("Pixels array is too small. Required length: " + colors.length);
System.arraycopy(colors, 0, pixels, 0, colors.length);
}
/**
* Returns the {@link Color} at the specified location. Throws an exception
* if x or y are out of bounds (negative or >= to the width or height
* respectively). The returned color is a non-premultiplied ARGB value.
*
* @param x The x coordinate (0...width-1) of the pixel to return
* @param y The y coordinate (0...height-1) of the pixel to return
* @return The argb {@link Color} at the specified coordinate
* @throws IllegalArgumentException if x, y exceed the drawable's bounds or drawable is recycled
*/
public int getPixel(int x, int y) {
if (x < 0)
throw new IllegalArgumentException("x must be >= 0");
if (y < 0)
throw new IllegalArgumentException("y must be >= 0");
if (x >= mMetaData[0])
throw new IllegalArgumentException("x must be < GIF width");
if (y >= mMetaData[1])
throw new IllegalArgumentException("y must be < GIF height");
final int[] colors = mColors;
if (colors == null)
throw new IllegalArgumentException("GifDrawable is recycled");
return colors[mMetaData[1] * y + x];
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mApplyTransformation = true;
}
/**
* Reads and renders new frame if needed then draws last rendered frame.
*
* @param canvas canvas to draw into
*/
@SuppressWarnings("deprecation")
@Override
public void draw(Canvas canvas) {
if (mApplyTransformation) {
mDstRect.set(getBounds());
mSx = (float) mDstRect.width() / mMetaData[0];
mSy = (float) mDstRect.height() / mMetaData[1];
mApplyTransformation = false;
}
if (mPaint.getShader() == null) {
if (mIsRunning) {
if (renderFrame(mColors, mGifInfoPtr, mMetaData))
for (AnimationListener listener : mListeners)
listener.onAnimationCompleted();
} else
mMetaData[4] = -1;
canvas.scale(mSx, mSy);
final int[] colors = mColors;
if (colors != null)
canvas.drawBitmap(colors, 0, mMetaData[0], 0f, 0f, mMetaData[0], mMetaData[1], true, mPaint);
if (mMetaData[4] >= 0 && mMetaData[2] > 1)
scheduleSelf(mInvalidateTask, mMetaData[4]);//TODO don't post if message for given frame was already posted
//invalidateSelf();
} else
canvas.drawRect(mDstRect, mPaint);
}
/**
* @return the paint used to render this drawable
*/
public final Paint getPaint() {
return mPaint;
}
@Override
public int getAlpha() {
return mPaint.getAlpha();
}
@Override
public void setFilterBitmap(boolean filter) {
mPaint.setFilterBitmap(filter);
invalidateSelf();
}
@Override
public void setDither(boolean dither) {
mPaint.setDither(dither);
invalidateSelf();
}
@Override
public int getMinimumHeight() {
return mMetaData[1];
}
@Override
public int getMinimumWidth() {
return mMetaData[0];
}
/**
* Adds a new animation listener
*
* @param listener animation listener to be added, not null
* @throws java.lang.NullPointerException if listener is null
*/
public void addAnimationListener(AnimationListener listener) {
mListeners.add(listener);
}
/**
* Removes an animation listener
*
* @param listener animation listener to be removed
* @return true if listener collection has been modified
*/
public boolean removeAnimationListener(AnimationListener listener) {
return mListeners.remove(listener);
}
@Override
public ColorFilter getColorFilter() {
return mPaint.getColorFilter();
}
}