/* * 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.Canvas; 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.HandlerThread; import android.os.Process; import android.os.SystemClock; public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable { 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) { bitmap.recycle(); } }; /** * 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 final Rect mSrcRect; //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); synchronized (mLock) { if (mNextFrameToDecode < 0 || mState != STATE_DECODING) return; mNextSwap = invalidateTimeMs + mLastSwap; mState = STATE_WAITING_TO_SWAP; } scheduleSelf(FrameSequenceDrawable.this, mNextSwap); } }; 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); mLastSwap = 0; mNextFrameToDecode = -1; mFrameSequenceState.getFrame(0, mFrontBitmap, -1); initializeDecodingThread(); } 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() { destroy(mBitmapProvider); } private void destroy(BitmapProvider bitmapProvider) { if (bitmapProvider == null) { throw new IllegalStateException("BitmapProvider must be non-null"); } Bitmap bitmapToReleaseA; Bitmap bitmapToReleaseB; synchronized (mLock) { checkDestroyedLocked(); bitmapToReleaseA = mFrontBitmap; bitmapToReleaseB = mBackBitmap; mFrontBitmap = null; mBackBitmap = null; mDestroyed = true; } // For simplicity and safety, we don't destroy the state object here bitmapProvider.releaseBitmap(bitmapToReleaseA); bitmapProvider.releaseBitmap(bitmapToReleaseB); } @Override protected void finalize() throws Throwable { try { mFrameSequenceState.destroy(); if (!mDestroyed) { 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; 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); } } } 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 synchronized (mLock) { if (mState != STATE_WAITING_TO_SWAP || mNextFrameToDecode < 0) return; mState = STATE_READY_TO_SWAP; } 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; } 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; } }