• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
initializeDecodingThread()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          */
onFinished(FrameSequenceDrawable drawable)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          */
acquireBitmap(int minWidth, int minHeight)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          */
releaseBitmap(Bitmap bitmap)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      */
setOnFinishedListener(OnFinishedListener onFinishedListener)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      */
setLoopBehavior(int loopBehavior)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 
acquireAndValidateBitmap(BitmapProvider bitmapProvider, int minWidth, int minHeight)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 
FrameSequenceDrawable(FrameSequence frameSequence)228     public FrameSequenceDrawable(FrameSequence frameSequence) {
229         this(frameSequence, sAllocatingBitmapProvider);
230     }
231 
FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider)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      */
setCircleMaskEnabled(boolean circleMaskEnabled)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 
checkDestroyedLocked()271     private void checkDestroyedLocked() {
272         if (mDestroyed) {
273             throw new IllegalStateException("Cannot perform operation on recycled drawable");
274         }
275     }
276 
isDestroyed()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      */
destroy()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
finalize()318     protected void finalize() throws Throwable {
319         try {
320             mFrameSequenceState.destroy();
321         } finally {
322             super.finalize();
323         }
324     }
325 
326     @Override
draw(Canvas canvas)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 
scheduleDecodeLocked()381     private void scheduleDecodeLocked() {
382         mState = STATE_SCHEDULED;
383         mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
384         sDecodingThreadHandler.post(mDecodeRunnable);
385     }
386 
387     @Override
run()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
start()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
stop()415     public void stop() {
416         if (isRunning()) {
417             unscheduleSelf(this);
418         }
419     }
420 
421     @Override
isRunning()422     public boolean isRunning() {
423         synchronized (mLock) {
424             return mNextFrameToDecode > -1 && !mDestroyed;
425         }
426     }
427 
428     @Override
unscheduleSelf(Runnable what)429     public void unscheduleSelf(Runnable what) {
430         synchronized (mLock) {
431             mNextFrameToDecode = -1;
432             mState = 0;
433         }
434         super.unscheduleSelf(what);
435     }
436 
437     @Override
setVisible(boolean visible, boolean restart)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
setFilterBitmap(boolean filter)454     public void setFilterBitmap(boolean filter) {
455         mPaint.setFilterBitmap(filter);
456     }
457 
458     @Override
setAlpha(int alpha)459     public void setAlpha(int alpha) {
460         mPaint.setAlpha(alpha);
461     }
462 
463     @Override
setColorFilter(ColorFilter colorFilter)464     public void setColorFilter(ColorFilter colorFilter) {
465         mPaint.setColorFilter(colorFilter);
466     }
467 
468     @Override
getIntrinsicWidth()469     public int getIntrinsicWidth() {
470         return mFrameSequence.getWidth();
471     }
472 
473     @Override
getIntrinsicHeight()474     public int getIntrinsicHeight() {
475         return mFrameSequence.getHeight();
476     }
477 
478     @Override
getOpacity()479     public int getOpacity() {
480         return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
481     }
482 }
483