Home | History | Annotate | Download | only in rastermill
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.support.rastermill;
     18 
     19 import android.graphics.Bitmap;
     20 import android.graphics.BitmapShader;
     21 import android.graphics.Canvas;
     22 import android.graphics.ColorFilter;
     23 import android.graphics.Paint;
     24 import android.graphics.PixelFormat;
     25 import android.graphics.Rect;
     26 import android.graphics.RectF;
     27 import android.graphics.Shader;
     28 import android.graphics.drawable.Animatable;
     29 import android.graphics.drawable.Drawable;
     30 import android.os.Handler;
     31 import android.os.HandlerThread;
     32 import android.os.Process;
     33 import android.os.SystemClock;
     34 import android.util.Log;
     35 
     36 public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable {
     37     private static final String TAG = "FrameSequence";
     38     /**
     39      * These constants are chosen to imitate common browser behavior for WebP/GIF.
     40      * If other decoders are added, this behavior should be moved into the WebP/GIF decoders.
     41      *
     42      * Note that 0 delay is undefined behavior in the GIF standard.
     43      */
     44     private static final long MIN_DELAY_MS = 20;
     45     private static final long DEFAULT_DELAY_MS = 100;
     46 
     47     private static final Object sLock = new Object();
     48     private static HandlerThread sDecodingThread;
     49     private static Handler sDecodingThreadHandler;
     50     private static void initializeDecodingThread() {
     51         synchronized (sLock) {
     52             if (sDecodingThread != null) return;
     53 
     54             sDecodingThread = new HandlerThread("FrameSequence decoding thread",
     55                     Process.THREAD_PRIORITY_BACKGROUND);
     56             sDecodingThread.start();
     57             sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
     58         }
     59     }
     60 
     61     public static interface OnFinishedListener {
     62         /**
     63          * Called when a FrameSequenceDrawable has finished looping.
     64          *
     65          * Note that this is will not be called if the drawable is explicitly
     66          * stopped, or marked invisible.
     67          */
     68         public abstract void onFinished(FrameSequenceDrawable drawable);
     69     }
     70 
     71     public static interface BitmapProvider {
     72         /**
     73          * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions.
     74          */
     75         public abstract Bitmap acquireBitmap(int minWidth, int minHeight);
     76 
     77         /**
     78          * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap
     79          * will no longer be used at all by the drawable, so it is safe to reuse elsewhere.
     80          *
     81          * This method may be called by FrameSequenceDrawable on any thread.
     82          */
     83         public abstract void releaseBitmap(Bitmap bitmap);
     84     }
     85 
     86     private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() {
     87         @Override
     88         public Bitmap acquireBitmap(int minWidth, int minHeight) {
     89             return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888);
     90         }
     91 
     92         @Override
     93         public void releaseBitmap(Bitmap bitmap) {}
     94     };
     95 
     96     /**
     97      * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
     98      *
     99      * @see #setLoopBehavior(int)
    100      */
    101     public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
    102         mOnFinishedListener = onFinishedListener;
    103     }
    104 
    105     /**
    106      * Loop a finite number of times, which can be set using setLoopCount. Default to loop once.
    107      */
    108     public static final int LOOP_FINITE = 1;
    109 
    110     /**
    111      * Loop continuously. The OnFinishedListener will never be called.
    112      */
    113     public static final int LOOP_INF = 2;
    114 
    115     /**
    116      * Use loop count stored in source data, or LOOP_ONCE if not present.
    117      */
    118     public static final int LOOP_DEFAULT = 3;
    119 
    120     /**
    121      * Loop only once.
    122      *
    123      * @deprecated Use LOOP_FINITE instead.
    124      */
    125     @Deprecated
    126     public static final int LOOP_ONCE = LOOP_FINITE;
    127 
    128     /**
    129      * Define looping behavior of frame sequence.
    130      *
    131      * Must be one of LOOP_ONCE, LOOP_INF, LOOP_DEFAULT, or LOOP_FINITE.
    132      */
    133     public void setLoopBehavior(int loopBehavior) {
    134         mLoopBehavior = loopBehavior;
    135     }
    136 
    137     /**
    138      * Set the number of loops in LOOP_FINITE mode. The number must be a postive integer.
    139      */
    140     public void setLoopCount(int loopCount) {
    141         mLoopCount = loopCount;
    142     }
    143 
    144     private final FrameSequence mFrameSequence;
    145     private final FrameSequence.State mFrameSequenceState;
    146 
    147     private final Paint mPaint;
    148     private BitmapShader mFrontBitmapShader;
    149     private BitmapShader mBackBitmapShader;
    150     private final Rect mSrcRect;
    151     private boolean mCircleMaskEnabled;
    152 
    153     //Protects the fields below
    154     private final Object mLock = new Object();
    155 
    156     private final BitmapProvider mBitmapProvider;
    157     private boolean mDestroyed = false;
    158     private Bitmap mFrontBitmap;
    159     private Bitmap mBackBitmap;
    160 
    161     private static final int STATE_SCHEDULED = 1;
    162     private static final int STATE_DECODING = 2;
    163     private static final int STATE_WAITING_TO_SWAP = 3;
    164     private static final int STATE_READY_TO_SWAP = 4;
    165 
    166     private int mState;
    167     private int mCurrentLoop;
    168     private int mLoopBehavior = LOOP_DEFAULT;
    169     private int mLoopCount = 1;
    170 
    171     private long mLastSwap;
    172     private long mNextSwap;
    173     private int mNextFrameToDecode;
    174     private OnFinishedListener mOnFinishedListener;
    175 
    176     private RectF mTempRectF = new RectF();
    177 
    178     /**
    179      * Runs on decoding thread, only modifies mBackBitmap's pixels
    180      */
    181     private Runnable mDecodeRunnable = new Runnable() {
    182         @Override
    183         public void run() {
    184             int nextFrame;
    185             Bitmap bitmap;
    186             synchronized (mLock) {
    187                 if (mDestroyed) return;
    188 
    189                 nextFrame = mNextFrameToDecode;
    190                 if (nextFrame < 0) {
    191                     return;
    192                 }
    193                 bitmap = mBackBitmap;
    194                 mState = STATE_DECODING;
    195             }
    196             int lastFrame = nextFrame - 2;
    197             boolean exceptionDuringDecode = false;
    198             long invalidateTimeMs = 0;
    199             try {
    200                 invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
    201             } catch(Exception e) {
    202                 // Exception during decode: continue, but delay next frame indefinitely.
    203                 Log.e(TAG, "exception during decode: " + e);
    204                 exceptionDuringDecode = true;
    205             }
    206 
    207             if (invalidateTimeMs < MIN_DELAY_MS) {
    208                 invalidateTimeMs = DEFAULT_DELAY_MS;
    209             }
    210 
    211             boolean schedule = false;
    212             Bitmap bitmapToRelease = null;
    213             synchronized (mLock) {
    214                 if (mDestroyed) {
    215                     bitmapToRelease = mBackBitmap;
    216                     mBackBitmap = null;
    217                 } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
    218                     schedule = true;
    219                     mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
    220                     mState = STATE_WAITING_TO_SWAP;
    221                 }
    222             }
    223             if (schedule) {
    224                 scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
    225             }
    226             if (bitmapToRelease != null) {
    227                 // destroy the bitmap here, since there's no safe way to get back to
    228                 // drawable thread - drawable is likely detached, so schedule is noop.
    229                 mBitmapProvider.releaseBitmap(bitmapToRelease);
    230             }
    231         }
    232     };
    233 
    234     private Runnable mFinishedCallbackRunnable = new Runnable() {
    235         @Override
    236         public void run() {
    237             synchronized (mLock) {
    238                 mNextFrameToDecode = -1;
    239                 mState = 0;
    240             }
    241             if (mOnFinishedListener != null) {
    242                 mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
    243             }
    244         }
    245     };
    246 
    247     private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
    248             int minWidth, int minHeight) {
    249         Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
    250 
    251         if (bitmap.getWidth() < minWidth
    252                 || bitmap.getHeight() < minHeight
    253                 || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
    254             throw new IllegalArgumentException("Invalid bitmap provided");
    255         }
    256 
    257         return bitmap;
    258     }
    259 
    260     public FrameSequenceDrawable(FrameSequence frameSequence) {
    261         this(frameSequence, sAllocatingBitmapProvider);
    262     }
    263 
    264     public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
    265         if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
    266 
    267         mFrameSequence = frameSequence;
    268         mFrameSequenceState = frameSequence.createState();
    269         final int width = frameSequence.getWidth();
    270         final int height = frameSequence.getHeight();
    271 
    272         mBitmapProvider = bitmapProvider;
    273         mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    274         mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    275         mSrcRect = new Rect(0, 0, width, height);
    276         mPaint = new Paint();
    277         mPaint.setFilterBitmap(true);
    278 
    279         mFrontBitmapShader
    280             = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    281         mBackBitmapShader
    282             = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    283 
    284         mLastSwap = 0;
    285 
    286         mNextFrameToDecode = -1;
    287         mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
    288         initializeDecodingThread();
    289     }
    290 
    291     /**
    292      * Pass true to mask the shape of the animated drawing content to a circle.
    293      *
    294      * <p> The masking circle will be the largest circle contained in the Drawable's bounds.
    295      * Masking is done with BitmapShader, incurring minimal additional draw cost.
    296      */
    297     public final void setCircleMaskEnabled(boolean circleMaskEnabled) {
    298         if (mCircleMaskEnabled != circleMaskEnabled) {
    299             mCircleMaskEnabled = circleMaskEnabled;
    300             // Anti alias only necessary when using circular mask
    301             mPaint.setAntiAlias(circleMaskEnabled);
    302             invalidateSelf();
    303         }
    304     }
    305 
    306     public final boolean getCircleMaskEnabled() {
    307         return mCircleMaskEnabled;
    308     }
    309 
    310     private void checkDestroyedLocked() {
    311         if (mDestroyed) {
    312             throw new IllegalStateException("Cannot perform operation on recycled drawable");
    313         }
    314     }
    315 
    316     public boolean isDestroyed() {
    317         synchronized (mLock) {
    318             return mDestroyed;
    319         }
    320     }
    321 
    322     /**
    323      * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
    324      * Bitmaps drawable to its BitmapProvider, if attached.
    325      *
    326      * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
    327      */
    328     public void destroy() {
    329         if (mBitmapProvider == null) {
    330             throw new IllegalStateException("BitmapProvider must be non-null");
    331         }
    332 
    333         Bitmap bitmapToReleaseA;
    334         Bitmap bitmapToReleaseB = null;
    335         synchronized (mLock) {
    336             checkDestroyedLocked();
    337 
    338             bitmapToReleaseA = mFrontBitmap;
    339             mFrontBitmap = null;
    340 
    341             if (mState != STATE_DECODING) {
    342                 bitmapToReleaseB = mBackBitmap;
    343                 mBackBitmap = null;
    344             }
    345 
    346             mDestroyed = true;
    347         }
    348 
    349         // For simplicity and safety, we don't destroy the state object here
    350         mBitmapProvider.releaseBitmap(bitmapToReleaseA);
    351         if (bitmapToReleaseB != null) {
    352             mBitmapProvider.releaseBitmap(bitmapToReleaseB);
    353         }
    354     }
    355 
    356     @Override
    357     protected void finalize() throws Throwable {
    358         try {
    359             mFrameSequenceState.destroy();
    360         } finally {
    361             super.finalize();
    362         }
    363     }
    364 
    365     @Override
    366     public void draw(Canvas canvas) {
    367         synchronized (mLock) {
    368             checkDestroyedLocked();
    369             if (mState == STATE_WAITING_TO_SWAP) {
    370                 // may have failed to schedule mark ready runnable,
    371                 // so go ahead and swap if swapping is due
    372                 if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
    373                     mState = STATE_READY_TO_SWAP;
    374                 }
    375             }
    376 
    377             if (isRunning() && mState == STATE_READY_TO_SWAP) {
    378                 // Because draw has occurred, the view system is guaranteed to no longer hold a
    379                 // reference to the old mFrontBitmap, so we now use it to produce the next frame
    380                 Bitmap tmp = mBackBitmap;
    381                 mBackBitmap = mFrontBitmap;
    382                 mFrontBitmap = tmp;
    383 
    384                 BitmapShader tmpShader = mBackBitmapShader;
    385                 mBackBitmapShader = mFrontBitmapShader;
    386                 mFrontBitmapShader = tmpShader;
    387 
    388                 mLastSwap = SystemClock.uptimeMillis();
    389 
    390                 boolean continueLooping = true;
    391                 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
    392                     mCurrentLoop++;
    393                     if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
    394                             (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
    395                         continueLooping = false;
    396                     }
    397                 }
    398 
    399                 if (continueLooping) {
    400                     scheduleDecodeLocked();
    401                 } else {
    402                     scheduleSelf(mFinishedCallbackRunnable, 0);
    403                 }
    404             }
    405         }
    406 
    407         if (mCircleMaskEnabled) {
    408             final Rect bounds = getBounds();
    409             final int bitmapWidth = getIntrinsicWidth();
    410             final int bitmapHeight = getIntrinsicHeight();
    411             final float scaleX = 1.0f * bounds.width() / bitmapWidth;
    412             final float scaleY = 1.0f * bounds.height() / bitmapHeight;
    413 
    414             canvas.save();
    415             // scale and translate to account for bounds, so we can operate in intrinsic
    416             // width/height (so it's valid to use an unscaled bitmap shader)
    417             canvas.translate(bounds.left, bounds.top);
    418             canvas.scale(scaleX, scaleY);
    419 
    420             final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
    421             final float scaledDiameterX = unscaledCircleDiameter / scaleX;
    422             final float scaledDiameterY = unscaledCircleDiameter / scaleY;
    423 
    424             // Want to draw a circle, but we have to compensate for canvas scale
    425             mTempRectF.set(
    426                     (bitmapWidth - scaledDiameterX) / 2.0f,
    427                     (bitmapHeight - scaledDiameterY) / 2.0f,
    428                     (bitmapWidth + scaledDiameterX) / 2.0f,
    429                     (bitmapHeight + scaledDiameterY) / 2.0f);
    430             mPaint.setShader(mFrontBitmapShader);
    431             canvas.drawOval(mTempRectF, mPaint);
    432             canvas.restore();
    433         } else {
    434             mPaint.setShader(null);
    435             canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
    436         }
    437     }
    438 
    439     private void scheduleDecodeLocked() {
    440         mState = STATE_SCHEDULED;
    441         mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
    442         sDecodingThreadHandler.post(mDecodeRunnable);
    443     }
    444 
    445     @Override
    446     public void run() {
    447         // set ready to swap as necessary
    448         boolean invalidate = false;
    449         synchronized (mLock) {
    450             if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
    451                 mState = STATE_READY_TO_SWAP;
    452                 invalidate = true;
    453             }
    454         }
    455         if (invalidate) {
    456             invalidateSelf();
    457         }
    458     }
    459 
    460     @Override
    461     public void start() {
    462         if (!isRunning()) {
    463             synchronized (mLock) {
    464                 checkDestroyedLocked();
    465                 if (mState == STATE_SCHEDULED) return; // already scheduled
    466                 mCurrentLoop = 0;
    467                 scheduleDecodeLocked();
    468             }
    469         }
    470     }
    471 
    472     @Override
    473     public void stop() {
    474         if (isRunning()) {
    475             unscheduleSelf(this);
    476         }
    477     }
    478 
    479     @Override
    480     public boolean isRunning() {
    481         synchronized (mLock) {
    482             return mNextFrameToDecode > -1 && !mDestroyed;
    483         }
    484     }
    485 
    486     @Override
    487     public void unscheduleSelf(Runnable what) {
    488         synchronized (mLock) {
    489             mNextFrameToDecode = -1;
    490             mState = 0;
    491         }
    492         super.unscheduleSelf(what);
    493     }
    494 
    495     @Override
    496     public boolean setVisible(boolean visible, boolean restart) {
    497         boolean changed = super.setVisible(visible, restart);
    498 
    499         if (!visible) {
    500             stop();
    501         } else if (restart || changed) {
    502             stop();
    503             start();
    504         }
    505 
    506         return changed;
    507     }
    508 
    509     // drawing properties
    510 
    511     @Override
    512     public void setFilterBitmap(boolean filter) {
    513         mPaint.setFilterBitmap(filter);
    514     }
    515 
    516     @Override
    517     public void setAlpha(int alpha) {
    518         mPaint.setAlpha(alpha);
    519     }
    520 
    521     @Override
    522     public void setColorFilter(ColorFilter colorFilter) {
    523         mPaint.setColorFilter(colorFilter);
    524     }
    525 
    526     @Override
    527     public int getIntrinsicWidth() {
    528         return mFrameSequence.getWidth();
    529     }
    530 
    531     @Override
    532     public int getIntrinsicHeight() {
    533         return mFrameSequence.getHeight();
    534     }
    535 
    536     @Override
    537     public int getOpacity() {
    538         return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
    539     }
    540 }
    541