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 
     35 public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable {
     36     /**
     37      * These constants are chosen to imitate common browser behavior for WebP/GIF.
     38      * If other decoders are added, this behavior should be moved into the WebP/GIF decoders.
     39      *
     40      * Note that 0 delay is undefined behavior in the GIF standard.
     41      */
     42     private static final long MIN_DELAY_MS = 20;
     43     private static final long DEFAULT_DELAY_MS = 100;
     44 
     45     private static final Object sLock = new Object();
     46     private static HandlerThread sDecodingThread;
     47     private static Handler sDecodingThreadHandler;
     48     private static void initializeDecodingThread() {
     49         synchronized (sLock) {
     50             if (sDecodingThread != null) return;
     51 
     52             sDecodingThread = new HandlerThread("FrameSequence decoding thread",
     53                     Process.THREAD_PRIORITY_BACKGROUND);
     54             sDecodingThread.start();
     55             sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
     56         }
     57     }
     58 
     59     public static interface OnFinishedListener {
     60         /**
     61          * Called when a FrameSequenceDrawable has finished looping.
     62          *
     63          * Note that this is will not be called if the drawable is explicitly
     64          * stopped, or marked invisible.
     65          */
     66         public abstract void onFinished(FrameSequenceDrawable drawable);
     67     }
     68 
     69     public static interface BitmapProvider {
     70         /**
     71          * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions.
     72          */
     73         public abstract Bitmap acquireBitmap(int minWidth, int minHeight);
     74 
     75         /**
     76          * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap
     77          * will no longer be used at all by the drawable, so it is safe to reuse elsewhere.
     78          *
     79          * This method may be called by FrameSequenceDrawable on any thread.
     80          */
     81         public abstract void releaseBitmap(Bitmap bitmap);
     82     }
     83 
     84     private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() {
     85         @Override
     86         public Bitmap acquireBitmap(int minWidth, int minHeight) {
     87             return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888);
     88         }
     89 
     90         @Override
     91         public void releaseBitmap(Bitmap bitmap) {}
     92     };
     93 
     94     /**
     95      * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
     96      *
     97      * @see #setLoopBehavior(int)
     98      */
     99     public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
    100         mOnFinishedListener = onFinishedListener;
    101     }
    102 
    103     /**
    104      * Loop only once.
    105      */
    106     public static final int LOOP_ONCE = 1;
    107 
    108     /**
    109      * Loop continuously. The OnFinishedListener will never be called.
    110      */
    111     public static final int LOOP_INF = 2;
    112 
    113     /**
    114      * Use loop count stored in source data, or LOOP_ONCE if not present.
    115      */
    116     public static final int LOOP_DEFAULT = 3;
    117 
    118     /**
    119      * Define looping behavior of frame sequence.
    120      *
    121      * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT
    122      */
    123     public void setLoopBehavior(int loopBehavior) {
    124         mLoopBehavior = loopBehavior;
    125     }
    126 
    127     private final FrameSequence mFrameSequence;
    128     private final FrameSequence.State mFrameSequenceState;
    129 
    130     private final Paint mPaint;
    131     private BitmapShader mFrontBitmapShader;
    132     private BitmapShader mBackBitmapShader;
    133      private final Rect mSrcRect;
    134     private boolean mCircleMaskEnabled;
    135 
    136     //Protects the fields below
    137     private final Object mLock = new Object();
    138 
    139     private final BitmapProvider mBitmapProvider;
    140     private boolean mDestroyed = false;
    141     private Bitmap mFrontBitmap;
    142     private Bitmap mBackBitmap;
    143 
    144     private static final int STATE_SCHEDULED = 1;
    145     private static final int STATE_DECODING = 2;
    146     private static final int STATE_WAITING_TO_SWAP = 3;
    147     private static final int STATE_READY_TO_SWAP = 4;
    148 
    149     private int mState;
    150     private int mCurrentLoop;
    151     private int mLoopBehavior = LOOP_DEFAULT;
    152 
    153     private long mLastSwap;
    154     private long mNextSwap;
    155     private int mNextFrameToDecode;
    156     private OnFinishedListener mOnFinishedListener;
    157 
    158     /**
    159      * Runs on decoding thread, only modifies mBackBitmap's pixels
    160      */
    161     private Runnable mDecodeRunnable = new Runnable() {
    162         @Override
    163         public void run() {
    164             int nextFrame;
    165             Bitmap bitmap;
    166             synchronized (mLock) {
    167                 if (mDestroyed) return;
    168 
    169                 nextFrame = mNextFrameToDecode;
    170                 if (nextFrame < 0) {
    171                     return;
    172                 }
    173                 bitmap = mBackBitmap;
    174                 mState = STATE_DECODING;
    175             }
    176             int lastFrame = nextFrame - 2;
    177             long invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
    178 
    179             if (invalidateTimeMs < MIN_DELAY_MS) {
    180                 invalidateTimeMs = DEFAULT_DELAY_MS;
    181             }
    182 
    183             boolean schedule = false;
    184             Bitmap bitmapToRelease = null;
    185             synchronized (mLock) {
    186                 if (mDestroyed) {
    187                     bitmapToRelease = mBackBitmap;
    188                     mBackBitmap = null;
    189                 } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
    190                     schedule = true;
    191                     mNextSwap = invalidateTimeMs + mLastSwap;
    192                     mState = STATE_WAITING_TO_SWAP;
    193                 }
    194             }
    195             if (schedule) {
    196                 scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
    197             }
    198             if (bitmapToRelease != null) {
    199                 // destroy the bitmap here, since there's no safe way to get back to
    200                 // drawable thread - drawable is likely detached, so schedule is noop.
    201                 mBitmapProvider.releaseBitmap(bitmapToRelease);
    202             }
    203         }
    204     };
    205 
    206     private Runnable mCallbackRunnable = new Runnable() {
    207         @Override
    208         public void run() {
    209             if (mOnFinishedListener != null) {
    210                 mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
    211             }
    212         }
    213     };
    214 
    215     private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
    216             int minWidth, int minHeight) {
    217         Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
    218 
    219         if (bitmap.getWidth() < minWidth
    220                 || bitmap.getHeight() < minHeight
    221                 || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
    222             throw new IllegalArgumentException("Invalid bitmap provided");
    223         }
    224 
    225         return bitmap;
    226     }
    227 
    228     public FrameSequenceDrawable(FrameSequence frameSequence) {
    229         this(frameSequence, sAllocatingBitmapProvider);
    230     }
    231 
    232     public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
    233         if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
    234 
    235         mFrameSequence = frameSequence;
    236         mFrameSequenceState = frameSequence.createState();
    237         final int width = frameSequence.getWidth();
    238         final int height = frameSequence.getHeight();
    239 
    240         mBitmapProvider = bitmapProvider;
    241         mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    242         mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    243         mSrcRect = new Rect(0, 0, width, height);
    244         mPaint = new Paint();
    245         mPaint.setFilterBitmap(true);
    246 
    247         mFrontBitmapShader
    248             = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    249         mBackBitmapShader
    250             = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    251 
    252         mLastSwap = 0;
    253 
    254         mNextFrameToDecode = -1;
    255         mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
    256         initializeDecodingThread();
    257     }
    258 
    259     /**
    260      * Pass true to mask the shape of the animated drawing content to a circle.
    261      *
    262      * <p> The masking circle will be the largest circle contained in the Drawable's bounds.
    263      * Masking is done with BitmapShader, incurring minimal additional draw cost.
    264      */
    265     public final void setCircleMaskEnabled(boolean circleMaskEnabled) {
    266         mCircleMaskEnabled = circleMaskEnabled;
    267         // Anti alias only necessary when using circular mask
    268         mPaint.setAntiAlias(circleMaskEnabled);
    269     }
    270 
    271     private void checkDestroyedLocked() {
    272         if (mDestroyed) {
    273             throw new IllegalStateException("Cannot perform operation on recycled drawable");
    274         }
    275     }
    276 
    277     public boolean isDestroyed() {
    278         synchronized (mLock) {
    279             return mDestroyed;
    280         }
    281     }
    282 
    283     /**
    284      * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
    285      * Bitmaps drawable to its BitmapProvider, if attached.
    286      *
    287      * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
    288      */
    289     public void destroy() {
    290         if (mBitmapProvider == null) {
    291             throw new IllegalStateException("BitmapProvider must be non-null");
    292         }
    293 
    294         Bitmap bitmapToReleaseA;
    295         Bitmap bitmapToReleaseB = null;
    296         synchronized (mLock) {
    297             checkDestroyedLocked();
    298 
    299             bitmapToReleaseA = mFrontBitmap;
    300             mFrontBitmap = null;
    301 
    302             if (mState != STATE_DECODING) {
    303                 bitmapToReleaseB = mBackBitmap;
    304                 mBackBitmap = null;
    305             }
    306 
    307             mDestroyed = true;
    308         }
    309 
    310         // For simplicity and safety, we don't destroy the state object here
    311         mBitmapProvider.releaseBitmap(bitmapToReleaseA);
    312         if (bitmapToReleaseB != null) {
    313             mBitmapProvider.releaseBitmap(bitmapToReleaseB);
    314         }
    315     }
    316 
    317     @Override
    318     protected void finalize() throws Throwable {
    319         try {
    320             mFrameSequenceState.destroy();
    321         } finally {
    322             super.finalize();
    323         }
    324     }
    325 
    326     @Override
    327     public void draw(Canvas canvas) {
    328         synchronized (mLock) {
    329             checkDestroyedLocked();
    330             if (mState == STATE_WAITING_TO_SWAP) {
    331                 // may have failed to schedule mark ready runnable,
    332                 // so go ahead and swap if swapping is due
    333                 if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
    334                     mState = STATE_READY_TO_SWAP;
    335                 }
    336             }
    337 
    338             if (isRunning() && mState == STATE_READY_TO_SWAP) {
    339                 // Because draw has occurred, the view system is guaranteed to no longer hold a
    340                 // reference to the old mFrontBitmap, so we now use it to produce the next frame
    341                 Bitmap tmp = mBackBitmap;
    342                 mBackBitmap = mFrontBitmap;
    343                 mFrontBitmap = tmp;
    344 
    345                 BitmapShader tmpShader = mBackBitmapShader;
    346                 mBackBitmapShader = mFrontBitmapShader;
    347                 mFrontBitmapShader = tmpShader;
    348 
    349                 mLastSwap = SystemClock.uptimeMillis();
    350 
    351                 boolean continueLooping = true;
    352                 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
    353                     mCurrentLoop++;
    354                     if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
    355                             (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
    356                         continueLooping = false;
    357                     }
    358                 }
    359 
    360                 if (continueLooping) {
    361                     scheduleDecodeLocked();
    362                 } else {
    363                     scheduleSelf(mCallbackRunnable, 0);
    364                 }
    365             }
    366         }
    367 
    368         if (mCircleMaskEnabled) {
    369             Rect bounds = getBounds();
    370             mPaint.setShader(mFrontBitmapShader);
    371             float width = bounds.width();
    372             float height = bounds.height();
    373             float circleRadius = (Math.min(width, height)) / 2f;
    374             canvas.drawCircle(width / 2f, height / 2f, circleRadius, mPaint);
    375         } else {
    376             mPaint.setShader(null);
    377             canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
    378         }
    379     }
    380 
    381     private void scheduleDecodeLocked() {
    382         mState = STATE_SCHEDULED;
    383         mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
    384         sDecodingThreadHandler.post(mDecodeRunnable);
    385     }
    386 
    387     @Override
    388     public void run() {
    389         // set ready to swap as necessary
    390         boolean invalidate = false;
    391         synchronized (mLock) {
    392             if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
    393                 mState = STATE_READY_TO_SWAP;
    394                 invalidate = true;
    395             }
    396         }
    397         if (invalidate) {
    398             invalidateSelf();
    399         }
    400     }
    401 
    402     @Override
    403     public void start() {
    404         if (!isRunning()) {
    405             synchronized (mLock) {
    406                 checkDestroyedLocked();
    407                 if (mState == STATE_SCHEDULED) return; // already scheduled
    408                 mCurrentLoop = 0;
    409                 scheduleDecodeLocked();
    410             }
    411         }
    412     }
    413 
    414     @Override
    415     public void stop() {
    416         if (isRunning()) {
    417             unscheduleSelf(this);
    418         }
    419     }
    420 
    421     @Override
    422     public boolean isRunning() {
    423         synchronized (mLock) {
    424             return mNextFrameToDecode > -1 && !mDestroyed;
    425         }
    426     }
    427 
    428     @Override
    429     public void unscheduleSelf(Runnable what) {
    430         synchronized (mLock) {
    431             mNextFrameToDecode = -1;
    432             mState = 0;
    433         }
    434         super.unscheduleSelf(what);
    435     }
    436 
    437     @Override
    438     public boolean setVisible(boolean visible, boolean restart) {
    439         boolean changed = super.setVisible(visible, restart);
    440 
    441         if (!visible) {
    442             stop();
    443         } else if (restart || changed) {
    444             stop();
    445             start();
    446         }
    447 
    448         return changed;
    449     }
    450 
    451     // drawing properties
    452 
    453     @Override
    454     public void setFilterBitmap(boolean filter) {
    455         mPaint.setFilterBitmap(filter);
    456     }
    457 
    458     @Override
    459     public void setAlpha(int alpha) {
    460         mPaint.setAlpha(alpha);
    461     }
    462 
    463     @Override
    464     public void setColorFilter(ColorFilter colorFilter) {
    465         mPaint.setColorFilter(colorFilter);
    466     }
    467 
    468     @Override
    469     public int getIntrinsicWidth() {
    470         return mFrameSequence.getWidth();
    471     }
    472 
    473     @Override
    474     public int getIntrinsicHeight() {
    475         return mFrameSequence.getHeight();
    476     }
    477 
    478     @Override
    479     public int getOpacity() {
    480         return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
    481     }
    482 }
    483