FrameSequenceDrawable.java revision 769d2c1df936a328f324a953f3cdf5901dd99078
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
17package android.support.rastermill;
18
19import android.graphics.Bitmap;
20import android.graphics.BitmapShader;
21import android.graphics.Canvas;
22import android.graphics.ColorFilter;
23import android.graphics.Paint;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.Shader;
28import android.graphics.drawable.Animatable;
29import android.graphics.drawable.Drawable;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.Process;
33import android.os.SystemClock;
34import android.util.Log;
35
36public 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 only once.
107     */
108    public static final int LOOP_ONCE = 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     * Define looping behavior of frame sequence.
122     *
123     * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT
124     */
125    public void setLoopBehavior(int loopBehavior) {
126        mLoopBehavior = loopBehavior;
127    }
128
129    private final FrameSequence mFrameSequence;
130    private final FrameSequence.State mFrameSequenceState;
131
132    private final Paint mPaint;
133    private BitmapShader mFrontBitmapShader;
134    private BitmapShader mBackBitmapShader;
135     private final Rect mSrcRect;
136    private boolean mCircleMaskEnabled;
137
138    //Protects the fields below
139    private final Object mLock = new Object();
140
141    private final BitmapProvider mBitmapProvider;
142    private boolean mDestroyed = false;
143    private Bitmap mFrontBitmap;
144    private Bitmap mBackBitmap;
145
146    private static final int STATE_SCHEDULED = 1;
147    private static final int STATE_DECODING = 2;
148    private static final int STATE_WAITING_TO_SWAP = 3;
149    private static final int STATE_READY_TO_SWAP = 4;
150
151    private int mState;
152    private int mCurrentLoop;
153    private int mLoopBehavior = LOOP_DEFAULT;
154
155    private long mLastSwap;
156    private long mNextSwap;
157    private int mNextFrameToDecode;
158    private OnFinishedListener mOnFinishedListener;
159
160    /**
161     * Runs on decoding thread, only modifies mBackBitmap's pixels
162     */
163    private Runnable mDecodeRunnable = new Runnable() {
164        @Override
165        public void run() {
166            int nextFrame;
167            Bitmap bitmap;
168            synchronized (mLock) {
169                if (mDestroyed) return;
170
171                nextFrame = mNextFrameToDecode;
172                if (nextFrame < 0) {
173                    return;
174                }
175                bitmap = mBackBitmap;
176                mState = STATE_DECODING;
177            }
178            int lastFrame = nextFrame - 2;
179            boolean exceptionDuringDecode = false;
180            long invalidateTimeMs = 0;
181            try {
182                invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
183            } catch(Exception e) {
184                // Exception during decode: continue, but delay next frame indefinitely.
185                Log.e(TAG, "exception during decode: " + e);
186                exceptionDuringDecode = true;
187            }
188
189            if (invalidateTimeMs < MIN_DELAY_MS) {
190                invalidateTimeMs = DEFAULT_DELAY_MS;
191            }
192
193            boolean schedule = false;
194            Bitmap bitmapToRelease = null;
195            synchronized (mLock) {
196                if (mDestroyed) {
197                    bitmapToRelease = mBackBitmap;
198                    mBackBitmap = null;
199                } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
200                    schedule = true;
201                    mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
202                    mState = STATE_WAITING_TO_SWAP;
203                }
204            }
205            if (schedule) {
206                scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
207            }
208            if (bitmapToRelease != null) {
209                // destroy the bitmap here, since there's no safe way to get back to
210                // drawable thread - drawable is likely detached, so schedule is noop.
211                mBitmapProvider.releaseBitmap(bitmapToRelease);
212            }
213        }
214    };
215
216    private Runnable mCallbackRunnable = new Runnable() {
217        @Override
218        public void run() {
219            if (mOnFinishedListener != null) {
220                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
221            }
222        }
223    };
224
225    private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
226            int minWidth, int minHeight) {
227        Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
228
229        if (bitmap.getWidth() < minWidth
230                || bitmap.getHeight() < minHeight
231                || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
232            throw new IllegalArgumentException("Invalid bitmap provided");
233        }
234
235        return bitmap;
236    }
237
238    public FrameSequenceDrawable(FrameSequence frameSequence) {
239        this(frameSequence, sAllocatingBitmapProvider);
240    }
241
242    public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
243        if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
244
245        mFrameSequence = frameSequence;
246        mFrameSequenceState = frameSequence.createState();
247        final int width = frameSequence.getWidth();
248        final int height = frameSequence.getHeight();
249
250        mBitmapProvider = bitmapProvider;
251        mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
252        mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
253        mSrcRect = new Rect(0, 0, width, height);
254        mPaint = new Paint();
255        mPaint.setFilterBitmap(true);
256
257        mFrontBitmapShader
258            = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
259        mBackBitmapShader
260            = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
261
262        mLastSwap = 0;
263
264        mNextFrameToDecode = -1;
265        mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
266        initializeDecodingThread();
267    }
268
269    /**
270     * Pass true to mask the shape of the animated drawing content to a circle.
271     *
272     * <p> The masking circle will be the largest circle contained in the Drawable's bounds.
273     * Masking is done with BitmapShader, incurring minimal additional draw cost.
274     */
275    public final void setCircleMaskEnabled(boolean circleMaskEnabled) {
276        mCircleMaskEnabled = circleMaskEnabled;
277        // Anti alias only necessary when using circular mask
278        mPaint.setAntiAlias(circleMaskEnabled);
279    }
280
281    private void checkDestroyedLocked() {
282        if (mDestroyed) {
283            throw new IllegalStateException("Cannot perform operation on recycled drawable");
284        }
285    }
286
287    public boolean isDestroyed() {
288        synchronized (mLock) {
289            return mDestroyed;
290        }
291    }
292
293    /**
294     * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
295     * Bitmaps drawable to its BitmapProvider, if attached.
296     *
297     * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
298     */
299    public void destroy() {
300        if (mBitmapProvider == null) {
301            throw new IllegalStateException("BitmapProvider must be non-null");
302        }
303
304        Bitmap bitmapToReleaseA;
305        Bitmap bitmapToReleaseB = null;
306        synchronized (mLock) {
307            checkDestroyedLocked();
308
309            bitmapToReleaseA = mFrontBitmap;
310            mFrontBitmap = null;
311
312            if (mState != STATE_DECODING) {
313                bitmapToReleaseB = mBackBitmap;
314                mBackBitmap = null;
315            }
316
317            mDestroyed = true;
318        }
319
320        // For simplicity and safety, we don't destroy the state object here
321        mBitmapProvider.releaseBitmap(bitmapToReleaseA);
322        if (bitmapToReleaseB != null) {
323            mBitmapProvider.releaseBitmap(bitmapToReleaseB);
324        }
325    }
326
327    @Override
328    protected void finalize() throws Throwable {
329        try {
330            mFrameSequenceState.destroy();
331        } finally {
332            super.finalize();
333        }
334    }
335
336    @Override
337    public void draw(Canvas canvas) {
338        synchronized (mLock) {
339            checkDestroyedLocked();
340            if (mState == STATE_WAITING_TO_SWAP) {
341                // may have failed to schedule mark ready runnable,
342                // so go ahead and swap if swapping is due
343                if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
344                    mState = STATE_READY_TO_SWAP;
345                }
346            }
347
348            if (isRunning() && mState == STATE_READY_TO_SWAP) {
349                // Because draw has occurred, the view system is guaranteed to no longer hold a
350                // reference to the old mFrontBitmap, so we now use it to produce the next frame
351                Bitmap tmp = mBackBitmap;
352                mBackBitmap = mFrontBitmap;
353                mFrontBitmap = tmp;
354
355                BitmapShader tmpShader = mBackBitmapShader;
356                mBackBitmapShader = mFrontBitmapShader;
357                mFrontBitmapShader = tmpShader;
358
359                mLastSwap = SystemClock.uptimeMillis();
360
361                boolean continueLooping = true;
362                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
363                    mCurrentLoop++;
364                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
365                            (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
366                        continueLooping = false;
367                    }
368                }
369
370                if (continueLooping) {
371                    scheduleDecodeLocked();
372                } else {
373                    scheduleSelf(mCallbackRunnable, 0);
374                }
375            }
376        }
377
378        if (mCircleMaskEnabled) {
379            Rect bounds = getBounds();
380            mPaint.setShader(mFrontBitmapShader);
381            float width = bounds.width();
382            float height = bounds.height();
383            float circleRadius = (Math.min(width, height)) / 2f;
384            canvas.drawCircle(width / 2f, height / 2f, circleRadius, mPaint);
385        } else {
386            mPaint.setShader(null);
387            canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
388        }
389    }
390
391    private void scheduleDecodeLocked() {
392        mState = STATE_SCHEDULED;
393        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
394        sDecodingThreadHandler.post(mDecodeRunnable);
395    }
396
397    @Override
398    public void run() {
399        // set ready to swap as necessary
400        boolean invalidate = false;
401        synchronized (mLock) {
402            if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
403                mState = STATE_READY_TO_SWAP;
404                invalidate = true;
405            }
406        }
407        if (invalidate) {
408            invalidateSelf();
409        }
410    }
411
412    @Override
413    public void start() {
414        if (!isRunning()) {
415            synchronized (mLock) {
416                checkDestroyedLocked();
417                if (mState == STATE_SCHEDULED) return; // already scheduled
418                mCurrentLoop = 0;
419                scheduleDecodeLocked();
420            }
421        }
422    }
423
424    @Override
425    public void stop() {
426        if (isRunning()) {
427            unscheduleSelf(this);
428        }
429    }
430
431    @Override
432    public boolean isRunning() {
433        synchronized (mLock) {
434            return mNextFrameToDecode > -1 && !mDestroyed;
435        }
436    }
437
438    @Override
439    public void unscheduleSelf(Runnable what) {
440        synchronized (mLock) {
441            mNextFrameToDecode = -1;
442            mState = 0;
443        }
444        super.unscheduleSelf(what);
445    }
446
447    @Override
448    public boolean setVisible(boolean visible, boolean restart) {
449        boolean changed = super.setVisible(visible, restart);
450
451        if (!visible) {
452            stop();
453        } else if (restart || changed) {
454            stop();
455            start();
456        }
457
458        return changed;
459    }
460
461    // drawing properties
462
463    @Override
464    public void setFilterBitmap(boolean filter) {
465        mPaint.setFilterBitmap(filter);
466    }
467
468    @Override
469    public void setAlpha(int alpha) {
470        mPaint.setAlpha(alpha);
471    }
472
473    @Override
474    public void setColorFilter(ColorFilter colorFilter) {
475        mPaint.setColorFilter(colorFilter);
476    }
477
478    @Override
479    public int getIntrinsicWidth() {
480        return mFrameSequence.getWidth();
481    }
482
483    @Override
484    public int getIntrinsicHeight() {
485        return mFrameSequence.getHeight();
486    }
487
488    @Override
489    public int getOpacity() {
490        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
491    }
492}
493