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;
initializeDecodingThread()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          */
onFinished(FrameSequenceDrawable drawable)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          */
acquireBitmap(int minWidth, int minHeight)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          */
releaseBitmap(Bitmap bitmap)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      */
setOnFinishedListener(OnFinishedListener onFinishedListener)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      */
setLoopBehavior(int loopBehavior)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      */
setLoopCount(int loopCount)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 
acquireAndValidateBitmap(BitmapProvider bitmapProvider, int minWidth, int minHeight)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 
FrameSequenceDrawable(FrameSequence frameSequence)260     public FrameSequenceDrawable(FrameSequence frameSequence) {
261         this(frameSequence, sAllocatingBitmapProvider);
262     }
263 
FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider)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      */
setCircleMaskEnabled(boolean circleMaskEnabled)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 
getCircleMaskEnabled()306     public final boolean getCircleMaskEnabled() {
307         return mCircleMaskEnabled;
308     }
309 
checkDestroyedLocked()310     private void checkDestroyedLocked() {
311         if (mDestroyed) {
312             throw new IllegalStateException("Cannot perform operation on recycled drawable");
313         }
314     }
315 
isDestroyed()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      */
destroy()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
finalize()357     protected void finalize() throws Throwable {
358         try {
359             mFrameSequenceState.destroy();
360         } finally {
361             super.finalize();
362         }
363     }
364 
365     @Override
draw(Canvas canvas)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 
scheduleDecodeLocked()439     private void scheduleDecodeLocked() {
440         mState = STATE_SCHEDULED;
441         mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
442         sDecodingThreadHandler.post(mDecodeRunnable);
443     }
444 
445     @Override
run()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
start()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
stop()473     public void stop() {
474         if (isRunning()) {
475             unscheduleSelf(this);
476         }
477     }
478 
479     @Override
isRunning()480     public boolean isRunning() {
481         synchronized (mLock) {
482             return mNextFrameToDecode > -1 && !mDestroyed;
483         }
484     }
485 
486     @Override
unscheduleSelf(Runnable what)487     public void unscheduleSelf(Runnable what) {
488         synchronized (mLock) {
489             mNextFrameToDecode = -1;
490             mState = 0;
491         }
492         super.unscheduleSelf(what);
493     }
494 
495     @Override
setVisible(boolean visible, boolean restart)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
setFilterBitmap(boolean filter)512     public void setFilterBitmap(boolean filter) {
513         mPaint.setFilterBitmap(filter);
514     }
515 
516     @Override
setAlpha(int alpha)517     public void setAlpha(int alpha) {
518         mPaint.setAlpha(alpha);
519     }
520 
521     @Override
setColorFilter(ColorFilter colorFilter)522     public void setColorFilter(ColorFilter colorFilter) {
523         mPaint.setColorFilter(colorFilter);
524     }
525 
526     @Override
getIntrinsicWidth()527     public int getIntrinsicWidth() {
528         return mFrameSequence.getWidth();
529     }
530 
531     @Override
getIntrinsicHeight()532     public int getIntrinsicHeight() {
533         return mFrameSequence.getHeight();
534     }
535 
536     @Override
getOpacity()537     public int getOpacity() {
538         return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
539     }
540 }
541