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