package python.gif; 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 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.os.Handler; import android.os.Looper; import android.os.StrictMode; import android.widget.MediaController.MediaPlayerControl; /** * 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"); } private static native void renderFrame(int[] pixels, int gifFileInPtr, int[] metaData); private static native int openFd(int[] metaData, FileDescriptor fd, long offset); private static native int openByteArray(int[] metaData, byte[] bytes); private static native int openDirectByteBuffer(int[] metaData, ByteBuffer buffer); private static native int openStream(int[] metaData, InputStream stream); private static native int openFile(int[] metaData, String filePath); private static native void free(int gifFileInPtr); private static native boolean reset(int gifFileInPtr); private static native void setSpeedFactor(int gifFileInPtr, float factor); private static native String getComment(int gifFileInPtr); private static native int getLoopCount(int gifFileInPtr); private static native int getDuration(int gifFileInPtr); private static native int getCurrentPosition(int gifFileInPtr); private static native int seekToTime(int gifFileInPtr, int pos, int[] pixels); private static native int seekToFrame(int gifFileInPtr, int frameNr, int[] pixels); private static native int saveRemainder(int gifFileInPtr); private static native int restoreRemainder(int gifFileInPtr); private static native long getAllocationByteCount(int gifFileInPtr); private static final Handler UI_HANDLER = new Handler( Looper.getMainLooper()); private volatile int 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. */ protected final int[] mColors; 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 static void runOnUiThread(Runnable task) { if (Looper.myLooper() == UI_HANDLER.getLooper()) task.run(); else UI_HANDLER.post(task); } /** * 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); 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()); mColors = new int[mMetaData[0] * mMetaData[1]]; } /** * Creates drawable from InputStream. InputStream must support marking, * GifIOException 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); 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(); mGifInfoPtr = openFd(mMetaData, fd, afd.getStartOffset()); 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); 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); 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); mColors = new int[mMetaData[0] * mMetaData[1]]; mInputSourceLength = buffer.capacity(); } /** * 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; int tmpPtr = mGifInfoPtr; mGifInfoPtr = 0; 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 infinite loop, 1 means one repetition * (animation is played twice) etc. */ 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, "Size: %dx%d, %d frames, 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 e) { // 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(int, 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 */ 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() { return getAllocationByteCount(mGifInfoPtr) + mColors.length * 4L; } /** * 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}. * * @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) { if (pixels.length < mColors.length) throw new ArrayIndexOutOfBoundsException( "Pixels array is too small. Required length: " + mColors.length); System.arraycopy(mColors, 0, pixels, 0, mColors.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 bitmap's bounds */ 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"); return mColors[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 */ @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) renderFrame(mColors, mGifInfoPtr, mMetaData); else mMetaData[4] = -1; canvas.scale(mSx, mSy); canvas.drawBitmap(mColors, 0, mMetaData[0], 0f, 0f, mMetaData[0], mMetaData[1], true, mPaint); if (mMetaData[4] >= 0 && mMetaData[2] > 1) UI_HANDLER.postDelayed(mInvalidateTask, mMetaData[4]);// TODO // don't // post // if // message // for // given // frame // was // already // posted } else canvas.drawRect(mDstRect, mPaint); } /** * @return the paint used to render this drawable */ public final Paint getPaint() { return mPaint; } 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]; } }