package pl.droidsonroids.gif; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.StrictMode; import android.os.SystemClock; import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RawRes; 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; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import pl.droidsonroids.gif.transforms.CornerRadiusTransform; import pl.droidsonroids.gif.transforms.Transform; import static pl.droidsonroids.gif.InvalidationHandler.MSG_TYPE_INVALIDATION; /** * A {@link Drawable} which can be used to hold GIF images, especially animations. * Basic GIF metadata can also be examined. * * @author koral-- */ public class GifDrawable extends Drawable implements Animatable, MediaPlayerControl { final ScheduledThreadPoolExecutor mExecutor; volatile boolean mIsRunning = true; long mNextFrameRenderTime = Long.MIN_VALUE; 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. */ protected final Bitmap mBuffer; final GifInfoHandle mNativeInfoHandle; final ConcurrentLinkedQueue<AnimationListener> mListeners = new ConcurrentLinkedQueue<>(); private ColorStateList mTint; private PorterDuffColorFilter mTintFilter; private PorterDuff.Mode mTintMode; final boolean mIsRenderingTriggeredOnDraw; final InvalidationHandler mInvalidationHandler; private final RenderTask mRenderTask = new RenderTask(this); private final Rect mSrcRect; ScheduledFuture<?> mRenderTaskSchedule; private int mScaledWidth; private int mScaledHeight; private Transform mTransform; /** * Creates drawable from resource. * * @param res Resources to read from * @param id resource id (raw or drawable) * @throws NotFoundException if the given ID does not exist. * @throws IOException when opening failed * @throws NullPointerException if res is null */ public GifDrawable(@NonNull Resources res, @RawRes @DrawableRes int id) throws NotFoundException, IOException { this(res.openRawResourceFd(id)); final float densityScale = GifViewUtils.getDensityScale(res, id); mScaledHeight = (int) (mNativeInfoHandle.getHeight() * densityScale); mScaledWidth = (int) (mNativeInfoHandle.getWidth() * densityScale); } /** * 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(@NonNull AssetManager assets, @NonNull 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(@NonNull String filePath) throws IOException { this(new GifInfoHandle(filePath), null, null, true); } /** * Equivalent to {@code} GifDrawable(file.getPath())} * * @param file the GIF file * @throws IOException when opening failed * @throws NullPointerException if file is null */ public GifDrawable(@NonNull File file) throws IOException { this(file.getPath()); } /** * 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(@NonNull InputStream stream) throws IOException { this(new GifInfoHandle(stream), null, null, true); } /** * 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(@NonNull AssetFileDescriptor afd) throws IOException { this(new GifInfoHandle(afd), null, null, true); } /** * Creates drawable from FileDescriptor * * @param fd source * @throws IOException when opening failed * @throws NullPointerException if fd is null */ public GifDrawable(@NonNull FileDescriptor fd) throws IOException { this(new GifInfoHandle(fd), null, null, true); } /** * 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(@NonNull byte[] bytes) throws IOException { this(new GifInfoHandle(bytes), null, null, true); } /** * 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 or is indirect * @throws NullPointerException if buffer is null */ public GifDrawable(@NonNull ByteBuffer buffer) throws IOException { this(new GifInfoHandle(buffer), null, null, true); } /** * 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 used to query {@code uri}, can be null for file:// scheme Uris * @throws IOException if resolution fails or destination is not a GIF. */ public GifDrawable(@Nullable ContentResolver resolver, @NonNull Uri uri) throws IOException { this(GifInfoHandle.openUri(resolver, uri), null, null, true); } GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor, boolean isRenderingTriggeredOnDraw) { mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw; mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance(); mNativeInfoHandle = gifInfoHandle; Bitmap oldBitmap = null; if (oldDrawable != null) { synchronized (oldDrawable.mNativeInfoHandle) { if (!oldDrawable.mNativeInfoHandle.isRecycled() && oldDrawable.mNativeInfoHandle.getHeight() >= mNativeInfoHandle.getHeight() && oldDrawable.mNativeInfoHandle.getWidth() >= mNativeInfoHandle.getWidth()) { oldDrawable.shutdown(); oldBitmap = oldDrawable.mBuffer; oldBitmap.eraseColor(Color.TRANSPARENT); } } } if (oldBitmap == null) { mBuffer = makeBitmap(mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), Bitmap.Config.ARGB_8888); } else { mBuffer = oldBitmap; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { mBuffer.setHasAlpha(!gifInfoHandle.isOpaque()); } mSrcRect = new Rect(0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight()); mInvalidationHandler = new InvalidationHandler(this); mRenderTask.doWork(); mScaledWidth = mNativeInfoHandle.getWidth(); mScaledHeight = mNativeInfoHandle.getHeight(); } protected Bitmap makeBitmap(int width, int height, Bitmap.Config config) { return Bitmap.createBitmap(width, height, config); } /** * 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() { shutdown(); recycleBitmap(); } protected void recycleBitmap() { if (mBuffer != null) { mBuffer.recycle(); } } private void shutdown() { mIsRunning = false; mInvalidationHandler.removeMessages(MSG_TYPE_INVALIDATION); mNativeInfoHandle.recycle(); } /** * @return true if drawable is recycled */ public boolean isRecycled() { return mNativeInfoHandle.isRecycled(); } @Override public int getIntrinsicHeight() { return mScaledHeight; } @Override public int getIntrinsicWidth() { return mScaledWidth; } @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { mPaint.setAlpha(alpha); } @Override public void setColorFilter(@Nullable ColorFilter cf) { mPaint.setColorFilter(cf); } /** * See {@link Drawable#getOpacity()} * * @return either {@link PixelFormat#TRANSPARENT} or {@link PixelFormat#OPAQUE} * depending on current {@link Paint} and {@link GifOptions#setInIsOpaque(boolean)} used to construct this Drawable */ @Override public int getOpacity() { if (!mNativeInfoHandle.isOpaque() || mPaint.getAlpha() < 255) { return PixelFormat.TRANSPARENT; } return PixelFormat.OPAQUE; } /** * Starts the animation. Does nothing if GIF is not animated. * This method is thread-safe. */ @Override public void start() { synchronized (this) { if (mIsRunning) { return; } mIsRunning = true; } final long lastFrameRemainder = mNativeInfoHandle.restoreRemainder(); startAnimation(lastFrameRemainder); } void startAnimation(long lastFrameRemainder) { if (mIsRenderingTriggeredOnDraw) { mNextFrameRenderTime = 0; mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); } else { cancelPendingRenderTask(); mRenderTaskSchedule = mExecutor.schedule(mRenderTask, Math.max(lastFrameRemainder, 0), TimeUnit.MILLISECONDS); } } /** * Causes the animation to start over. * If rewinding input source fails then state is not affected. * This method is thread-safe. */ public void reset() { mExecutor.execute(new SafeRunnable(this) { @Override public void doWork() { if (mNativeInfoHandle.reset()) { start(); } } }); } /** * Stops the animation. Does nothing if GIF is not animated. * This method is thread-safe. */ @Override public void stop() { synchronized (this) { if (!mIsRunning) { return; } mIsRunning = false; } cancelPendingRenderTask(); mNativeInfoHandle.saveRemainder(); } private void cancelPendingRenderTask() { if (mRenderTaskSchedule != null) { mRenderTaskSchedule.cancel(false); } mInvalidationHandler.removeMessages(MSG_TYPE_INVALIDATION); } @Override public boolean isRunning() { return mIsRunning; } /** * Returns GIF comment * * @return comment or null if there is no one defined in file */ @Nullable public String getComment() { return mNativeInfoHandle.getComment(); } /** * Returns loop count previously read from GIF's application extension block. * Defaults to 1 if there is no such extension. * * @return loop count, 0 means that animation is infinite */ public int getLoopCount() { return mNativeInfoHandle.getLoopCount(); } /** * Sets loop count of the animation. Loop count must be in range {@code <0 ,65535>} * * @param loopCount loop count, 0 means infinity */ public void setLoopCount(@IntRange(from = 0, to = Character.MAX_VALUE) final int loopCount) { mNativeInfoHandle.setLoopCount(loopCount); } /** * @return basic description of the GIF including size and number of frames */ @Override public String toString() { return String.format(Locale.ENGLISH, "GIF: size: %dx%d, frames: %d, error: %d", mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), mNativeInfoHandle.getNumberOfFrames(), mNativeInfoHandle.getNativeErrorCode()); } /** * @return number of frames in GIF, at least one */ public int getNumberOfFrames() { return mNativeInfoHandle.getNumberOfFrames(); } /** * 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 or drawable is recycled */ @NonNull public GifError getError() { return GifError.fromCode(mNativeInfoHandle.getNativeErrorCode()); } /** * 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 */ @Nullable public static GifDrawable createFromResource(@NonNull Resources res, @RawRes @DrawableRes int resourceId) { try { return new GifDrawable(res, resourceId); } catch (IOException 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(@FloatRange(from = 0, fromInclusive = false) final float factor) { mNativeInfoHandle.setSpeedFactor(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 mNativeInfoHandle.getDuration(); } /** * Retrieves elapsed time from the beginning of a current loop of animation. * If there is only 1 frame or drawable is recycled 0 is returned. * * @return elapsed time from the beginning of a loop in ms */ @Override public int getCurrentPosition() { return mNativeInfoHandle.getCurrentPosition(); } /** * Seeks animation to given absolute position (within given loop) and refreshes the canvas.<br> * If <code>position</code> 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, no exception will be thrown.<br> * NOTE: all frames from current (or first one if seeking backward) to desired one must be rendered sequentially to perform seeking. * It may take a lot of time if number of such frames is large. * Method is thread-safe. Decoding is performed in background thread and drawable is invalidated automatically * afterwards. * * @param position position to seek to in milliseconds * @throws IllegalArgumentException if <code>position</code><0 */ @Override public void seekTo(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position) { if (position < 0) { throw new IllegalArgumentException("Position is not positive"); } mExecutor.execute(new SafeRunnable(this) { @Override public void doWork() { mNativeInfoHandle.seekToTime(position, mBuffer); mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); } }); } /** * Like {@link #seekTo(int)} but uses index of the frame instead of time. * If <code>frameIndex</code> exceeds number of frames, seek stops at the end, no exception is thrown. * * @param frameIndex index of the frame to seek to (zero based) * @throws IllegalArgumentException if <code>frameIndex</code><0 */ public void seekToFrame(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex) { if (frameIndex < 0) { throw new IndexOutOfBoundsException("Frame index is not positive"); } mExecutor.execute(new SafeRunnable(this) { @Override public void doWork() { mNativeInfoHandle.seekToFrame(frameIndex, mBuffer); mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); } }); } /** * Like {@link #seekToFrame(int)} but performs operation synchronously and returns that frame. * * @param frameIndex index of the frame to seek to (zero based) * @return frame at desired index * @throws IndexOutOfBoundsException if frameIndex<0 */ public Bitmap seekToFrameAndGet(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex) { if (frameIndex < 0) { throw new IndexOutOfBoundsException("Frame index is not positive"); } final Bitmap bitmap; synchronized (mNativeInfoHandle) { mNativeInfoHandle.seekToFrame(frameIndex, mBuffer); bitmap = getCurrentFrame(); } mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); return bitmap; } /** * Like {@link #seekTo(int)} but performs operation synchronously and returns that frame. * * @param position position to seek to in milliseconds * @return frame at desired position * @throws IndexOutOfBoundsException if position<0 */ public Bitmap seekToPositionAndGet(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position) { if (position < 0) { throw new IllegalArgumentException("Position is not positive"); } final Bitmap bitmap; synchronized (mNativeInfoHandle) { mNativeInfoHandle.seekToTime(position, mBuffer); bitmap = getCurrentFrame(); } mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); return bitmap; } /** * 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. * * @return true if GIF has at least 2 frames */ @Override public boolean canSeekBackward() { return getNumberOfFrames() > 1; } /** * 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. * <p>This method should not be used to calculate the memory usage of the bitmap. * Instead see {@link #getAllocationByteCount()}. * * @return the minimum number of bytes that can be used to store pixels of the single frame */ public int getFrameByteCount() { return mBuffer.getRowBytes() * mBuffer.getHeight(); } /** * Returns size of the memory needed to store pixels of this object. It counts possible length of all frame buffers. * Returned value may be lower than amount of actually allocated memory if GIF uses dispose to previous method but frame requiring it * has never been needed yet. Returned value does not change during runtime. * * @return possible size of the memory needed to store pixels of this object */ public long getAllocationByteCount() { long byteCount = mNativeInfoHandle.getAllocationByteCount(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { byteCount += mBuffer.getAllocationByteCount(); } else { byteCount += getFrameByteCount(); } return byteCount; } /** * Returns the maximum possible size of the allocated memory used to store pixels and metadata of this object. * It counts length of all frame buffers. Returned value does not change over time. * * @return maximum possible size of the allocated memory needed to store metadata of this object */ public long getMetadataAllocationByteCount() { return mNativeInfoHandle.getMetadataByteCount(); } /** * Returns length of the input source obtained at the opening time or -1 if * length cannot be determined. Returned value does not change during runtime. * If GifDrawable is constructed from {@link InputStream} -1 is always returned. * In case of byte array and {@link ByteBuffer} length is always known. * In other cases length -1 can be returned if length cannot be determined. * * @return number of bytes backed by input source or -1 if it is unknown */ public long getInputSourceByteCount() { return mNativeInfoHandle.getSourceLength(); } /** * 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(@NonNull int[] pixels) { mBuffer.getPixels(pixels, 0, mNativeInfoHandle.getWidth(), 0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight()); } /** * 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 * @throws IllegalStateException if drawable is recycled */ public int getPixel(int x, int y) { if (x >= mNativeInfoHandle.getWidth()) { //need to check explicitly because reused bitmap may be larger throw new IllegalArgumentException("x must be < width"); } if (y >= mNativeInfoHandle.getHeight()) { throw new IllegalArgumentException("y must be < height"); } return mBuffer.getPixel(x, y); } @Override protected void onBoundsChange(Rect bounds) { mDstRect.set(bounds); if (mTransform != null) { mTransform.onBoundsChange(bounds); } } /** * Reads and renders new frame if needed then draws last rendered frame. * * @param canvas canvas to draw into */ @Override public void draw(@NonNull Canvas canvas) { final boolean clearColorFilter; if (mTintFilter != null && mPaint.getColorFilter() == null) { mPaint.setColorFilter(mTintFilter); clearColorFilter = true; } else { clearColorFilter = false; } if (mTransform == null) { canvas.drawBitmap(mBuffer, mSrcRect, mDstRect, mPaint); } else { mTransform.onDraw(canvas, mPaint, mBuffer); } if (clearColorFilter) { mPaint.setColorFilter(null); } if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) { final long renderDelay = Math.max(0, mNextFrameRenderTime - SystemClock.uptimeMillis()); mNextFrameRenderTime = Long.MIN_VALUE; mExecutor.remove(mRenderTask); mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS); } } /** * @return the paint used to render this drawable */ @NonNull public final Paint getPaint() { return mPaint; } @Override public int getAlpha() { return mPaint.getAlpha(); } @Override public void setFilterBitmap(boolean filter) { mPaint.setFilterBitmap(filter); invalidateSelf(); } @SuppressWarnings("deprecation") @Override @Deprecated public void setDither(boolean dither) { mPaint.setDither(dither); invalidateSelf(); } /** * 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(@NonNull 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(); } /** * Retrieves a copy of currently buffered frame. * * @return current frame */ public Bitmap getCurrentFrame() { final Bitmap copy = mBuffer.copy(mBuffer.getConfig(), mBuffer.isMutable()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { copy.setHasAlpha(mBuffer.hasAlpha()); } return copy; } private PorterDuffColorFilter updateTintFilter(ColorStateList tint, PorterDuff.Mode tintMode) { if (tint == null || tintMode == null) { return null; } final int color = tint.getColorForState(getState(), Color.TRANSPARENT); return new PorterDuffColorFilter(color, tintMode); } @Override public void setTintList(ColorStateList tint) { mTint = tint; mTintFilter = updateTintFilter(tint, mTintMode); invalidateSelf(); } @Override public void setTintMode(@NonNull PorterDuff.Mode tintMode) { mTintMode = tintMode; mTintFilter = updateTintFilter(mTint, tintMode); invalidateSelf(); } @Override protected boolean onStateChange(int[] stateSet) { if (mTint != null && mTintMode != null) { mTintFilter = updateTintFilter(mTint, mTintMode); return true; } return false; } @Override public boolean isStateful() { return super.isStateful() || (mTint != null && mTint.isStateful()); } /** * Sets whether this drawable is visible. If rendering of next frame is scheduled on draw current one (the default) then this method * only calls through to the super class's implementation.<br> * Otherwise (if {@link GifDrawableBuilder#setRenderingTriggeredOnDraw(boolean)} was used with <code>true</code>) * when the drawable becomes invisible, it will pause its animation. A * subsequent change to visible with <code>restart</code> set to true will * restart the animation from the first frame. If <code>restart</code> is * false, the animation will resume from the most recent frame. * * @param visible true if visible, false otherwise * @param restart when visible and rendering is triggered on draw, true to force the animation to restart * from the first frame * @return true if the new visibility is different than its previous state */ @Override public boolean setVisible(boolean visible, boolean restart) { final boolean changed = super.setVisible(visible, restart); if (!mIsRenderingTriggeredOnDraw) { if (visible) { if (restart) { reset(); } if (changed) { start(); } } else if (changed) { stop(); } } return changed; } /** * Returns zero-based index of recently rendered frame in given loop or -1 when drawable is recycled. * * @return index of recently rendered frame or -1 when drawable is recycled */ public int getCurrentFrameIndex() { return mNativeInfoHandle.getCurrentFrameIndex(); } /** * Returns zero-based index of currently played animation loop. If animation is infinite or * drawable is recycled 0 is returned. * * @return index of currently played animation loop */ public int getCurrentLoop() { final int currentLoop = mNativeInfoHandle.getCurrentLoop(); if (currentLoop == 0 || currentLoop < mNativeInfoHandle.getLoopCount()) { return currentLoop; } else { return currentLoop - 1; } } /** * Returns whether all animation loops has ended. If drawable is recycled false is returned. * * @return true if all animation loops has ended */ public boolean isAnimationCompleted() { return mNativeInfoHandle.isAnimationCompleted(); } /** * Returns duration of the given frame (in milliseconds). If there is no data (no Graphics * Control Extension blocks or drawable is recycled) 0 is returned. * * @param index index of the frame * @return duration of the given frame in milliseconds * @throws IndexOutOfBoundsException if index < 0 or index >= number of frames */ public int getFrameDuration(@IntRange(from = 0) final int index) { return mNativeInfoHandle.getFrameDuration(index); } /** * Sets the corner radius to be applied when drawing the bitmap. * Note that changing corner radius will cause replacing current {@link Paint} shader by {@link BitmapShader}. * Transform set by {@link #setTransform(Transform)} will also be replaced. * * @param cornerRadius corner radius or 0 to remove rounding */ public void setCornerRadius(@FloatRange(from = 0) final float cornerRadius) { mTransform = new CornerRadiusTransform(cornerRadius); } /** * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded. */ @FloatRange(from = 0) public float getCornerRadius() { if (mTransform instanceof CornerRadiusTransform) { return ((CornerRadiusTransform) mTransform).getCornerRadius(); } return 0; } /** * Specify a {@link Transform} implementation to customize how the GIF's current Bitmap is drawn. * * @param transform new {@link Transform} or null to remove current one */ public void setTransform(@Nullable Transform transform) { mTransform = transform; } /** * @return The current {@link Transform} implementation that customizes * how the GIF's current Bitmap is drawn or null if nothing has been set. */ @Nullable public Transform getTransform() { return mTransform; } }