/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.rastermill; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; import android.os.SystemClock; public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable { /** * These constants are chosen to imitate common browser behavior for WebP/GIF. * If other decoders are added, this behavior should be moved into the WebP/GIF decoders. * * Note that 0 delay is undefined behavior in the GIF standard. */ private static final long MIN_DELAY_MS = 20; private static final long DEFAULT_DELAY_MS = 100; private static final Object sLock = new Object(); private static HandlerThread sDecodingThread; private static Handler sDecodingThreadHandler; private static void initializeDecodingThread() { synchronized (sLock) { if (sDecodingThread != null) return; sDecodingThread = new HandlerThread("FrameSequence decoding thread", Process.THREAD_PRIORITY_BACKGROUND); sDecodingThread.start(); sDecodingThreadHandler = new Handler(sDecodingThread.getLooper()); } } public static interface OnFinishedListener { /** * Called when a FrameSequenceDrawable has finished looping. * * Note that this is will not be called if the drawable is explicitly * stopped, or marked invisible. */ public abstract void onFinished(FrameSequenceDrawable drawable); } public static interface BitmapProvider { /** * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions. */ public abstract Bitmap acquireBitmap(int minWidth, int minHeight); /** * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap * will no longer be used at all by the drawable, so it is safe to reuse elsewhere. * * This method may be called by FrameSequenceDrawable on any thread. */ public abstract void releaseBitmap(Bitmap bitmap); } private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() { @Override public Bitmap acquireBitmap(int minWidth, int minHeight) { return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888); } @Override public void releaseBitmap(Bitmap bitmap) {} }; /** * Register a callback to be invoked when a FrameSequenceDrawable finishes looping. * * @see #setLoopBehavior(int) */ public void setOnFinishedListener(OnFinishedListener onFinishedListener) { mOnFinishedListener = onFinishedListener; } /** * Loop only once. */ public static final int LOOP_ONCE = 1; /** * Loop continuously. The OnFinishedListener will never be called. */ public static final int LOOP_INF = 2; /** * Use loop count stored in source data, or LOOP_ONCE if not present. */ public static final int LOOP_DEFAULT = 3; /** * Define looping behavior of frame sequence. * * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT */ public void setLoopBehavior(int loopBehavior) { mLoopBehavior = loopBehavior; } private final FrameSequence mFrameSequence; private final FrameSequence.State mFrameSequenceState; private final Paint mPaint; private BitmapShader mFrontBitmapShader; private BitmapShader mBackBitmapShader; private final Rect mSrcRect; private boolean mCircleMaskEnabled; //Protects the fields below private final Object mLock = new Object(); private final BitmapProvider mBitmapProvider; private boolean mDestroyed = false; private Bitmap mFrontBitmap; private Bitmap mBackBitmap; private static final int STATE_SCHEDULED = 1; private static final int STATE_DECODING = 2; private static final int STATE_WAITING_TO_SWAP = 3; private static final int STATE_READY_TO_SWAP = 4; private int mState; private int mCurrentLoop; private int mLoopBehavior = LOOP_DEFAULT; private long mLastSwap; private long mNextSwap; private int mNextFrameToDecode; private OnFinishedListener mOnFinishedListener; /** * Runs on decoding thread, only modifies mBackBitmap's pixels */ private Runnable mDecodeRunnable = new Runnable() { @Override public void run() { int nextFrame; Bitmap bitmap; synchronized (mLock) { if (mDestroyed) return; nextFrame = mNextFrameToDecode; if (nextFrame < 0) { return; } bitmap = mBackBitmap; mState = STATE_DECODING; } int lastFrame = nextFrame - 2; long invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame); if (invalidateTimeMs < MIN_DELAY_MS) { invalidateTimeMs = DEFAULT_DELAY_MS; } boolean schedule = false; Bitmap bitmapToRelease = null; synchronized (mLock) { if (mDestroyed) { bitmapToRelease = mBackBitmap; mBackBitmap = null; } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) { schedule = true; mNextSwap = invalidateTimeMs + mLastSwap; mState = STATE_WAITING_TO_SWAP; } } if (schedule) { scheduleSelf(FrameSequenceDrawable.this, mNextSwap); } if (bitmapToRelease != null) { // destroy the bitmap here, since there's no safe way to get back to // drawable thread - drawable is likely detached, so schedule is noop. mBitmapProvider.releaseBitmap(bitmapToRelease); } } }; private Runnable mCallbackRunnable = new Runnable() { @Override public void run() { if (mOnFinishedListener != null) { mOnFinishedListener.onFinished(FrameSequenceDrawable.this); } } }; private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider, int minWidth, int minHeight) { Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight); if (bitmap.getWidth() < minWidth || bitmap.getHeight() < minHeight || bitmap.getConfig() != Bitmap.Config.ARGB_8888) { throw new IllegalArgumentException("Invalid bitmap provided"); } return bitmap; } public FrameSequenceDrawable(FrameSequence frameSequence) { this(frameSequence, sAllocatingBitmapProvider); } public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) { if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException(); mFrameSequence = frameSequence; mFrameSequenceState = frameSequence.createState(); final int width = frameSequence.getWidth(); final int height = frameSequence.getHeight(); mBitmapProvider = bitmapProvider; mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); mSrcRect = new Rect(0, 0, width, height); mPaint = new Paint(); mPaint.setFilterBitmap(true); mFrontBitmapShader = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mBackBitmapShader = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mLastSwap = 0; mNextFrameToDecode = -1; mFrameSequenceState.getFrame(0, mFrontBitmap, -1); initializeDecodingThread(); } /** * Pass true to mask the shape of the animated drawing content to a circle. * *

The masking circle will be the largest circle contained in the Drawable's bounds. * Masking is done with BitmapShader, incurring minimal additional draw cost. */ public final void setCircleMaskEnabled(boolean circleMaskEnabled) { mCircleMaskEnabled = circleMaskEnabled; // Anti alias only necessary when using circular mask mPaint.setAntiAlias(circleMaskEnabled); } private void checkDestroyedLocked() { if (mDestroyed) { throw new IllegalStateException("Cannot perform operation on recycled drawable"); } } public boolean isDestroyed() { synchronized (mLock) { return mDestroyed; } } /** * Marks the drawable as permanently recycled (and thus unusable), and releases any owned * Bitmaps drawable to its BitmapProvider, if attached. * * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps. */ public void destroy() { if (mBitmapProvider == null) { throw new IllegalStateException("BitmapProvider must be non-null"); } Bitmap bitmapToReleaseA; Bitmap bitmapToReleaseB = null; synchronized (mLock) { checkDestroyedLocked(); bitmapToReleaseA = mFrontBitmap; mFrontBitmap = null; if (mState != STATE_DECODING) { bitmapToReleaseB = mBackBitmap; mBackBitmap = null; } mDestroyed = true; } // For simplicity and safety, we don't destroy the state object here mBitmapProvider.releaseBitmap(bitmapToReleaseA); if (bitmapToReleaseB != null) { mBitmapProvider.releaseBitmap(bitmapToReleaseB); } } @Override protected void finalize() throws Throwable { try { mFrameSequenceState.destroy(); } finally { super.finalize(); } } @Override public void draw(Canvas canvas) { synchronized (mLock) { checkDestroyedLocked(); if (mState == STATE_WAITING_TO_SWAP) { // may have failed to schedule mark ready runnable, // so go ahead and swap if swapping is due if (mNextSwap - SystemClock.uptimeMillis() <= 0) { mState = STATE_READY_TO_SWAP; } } if (isRunning() && mState == STATE_READY_TO_SWAP) { // Because draw has occurred, the view system is guaranteed to no longer hold a // reference to the old mFrontBitmap, so we now use it to produce the next frame Bitmap tmp = mBackBitmap; mBackBitmap = mFrontBitmap; mFrontBitmap = tmp; BitmapShader tmpShader = mBackBitmapShader; mBackBitmapShader = mFrontBitmapShader; mFrontBitmapShader = tmpShader; mLastSwap = SystemClock.uptimeMillis(); boolean continueLooping = true; if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { mCurrentLoop++; if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) || (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { continueLooping = false; } } if (continueLooping) { scheduleDecodeLocked(); } else { scheduleSelf(mCallbackRunnable, 0); } } } if (mCircleMaskEnabled) { Rect bounds = getBounds(); mPaint.setShader(mFrontBitmapShader); float width = bounds.width(); float height = bounds.height(); float circleRadius = (Math.min(width, height)) / 2f; canvas.drawCircle(width / 2f, height / 2f, circleRadius, mPaint); } else { mPaint.setShader(null); canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint); } } private void scheduleDecodeLocked() { mState = STATE_SCHEDULED; mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount(); sDecodingThreadHandler.post(mDecodeRunnable); } @Override public void run() { // set ready to swap as necessary boolean invalidate = false; synchronized (mLock) { if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) { mState = STATE_READY_TO_SWAP; invalidate = true; } } if (invalidate) { invalidateSelf(); } } @Override public void start() { if (!isRunning()) { synchronized (mLock) { checkDestroyedLocked(); if (mState == STATE_SCHEDULED) return; // already scheduled mCurrentLoop = 0; scheduleDecodeLocked(); } } } @Override public void stop() { if (isRunning()) { unscheduleSelf(this); } } @Override public boolean isRunning() { synchronized (mLock) { return mNextFrameToDecode > -1 && !mDestroyed; } } @Override public void unscheduleSelf(Runnable what) { synchronized (mLock) { mNextFrameToDecode = -1; mState = 0; } super.unscheduleSelf(what); } @Override public boolean setVisible(boolean visible, boolean restart) { boolean changed = super.setVisible(visible, restart); if (!visible) { stop(); } else if (restart || changed) { stop(); start(); } return changed; } // drawing properties @Override public void setFilterBitmap(boolean filter) { mPaint.setFilterBitmap(filter); } @Override public void setAlpha(int alpha) { mPaint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); } @Override public int getIntrinsicWidth() { return mFrameSequence.getWidth(); } @Override public int getIntrinsicHeight() { return mFrameSequence.getHeight(); } @Override public int getOpacity() { return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT; } }