FrameSequenceDrawable.java revision 958761ceb97982b510d3059d31ba8c45700a1654
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 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     */
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     */
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 mCallbackRunnable = new Runnable() {
235        @Override
236        public void run() {
237            if (mOnFinishedListener != null) {
238                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
239            }
240        }
241    };
242
243    private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
244            int minWidth, int minHeight) {
245        Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
246
247        if (bitmap.getWidth() < minWidth
248                || bitmap.getHeight() < minHeight
249                || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
250            throw new IllegalArgumentException("Invalid bitmap provided");
251        }
252
253        return bitmap;
254    }
255
256    public FrameSequenceDrawable(FrameSequence frameSequence) {
257        this(frameSequence, sAllocatingBitmapProvider);
258    }
259
260    public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
261        if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
262
263        mFrameSequence = frameSequence;
264        mFrameSequenceState = frameSequence.createState();
265        final int width = frameSequence.getWidth();
266        final int height = frameSequence.getHeight();
267
268        mBitmapProvider = bitmapProvider;
269        mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
270        mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
271        mSrcRect = new Rect(0, 0, width, height);
272        mPaint = new Paint();
273        mPaint.setFilterBitmap(true);
274
275        mFrontBitmapShader
276            = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
277        mBackBitmapShader
278            = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
279
280        mLastSwap = 0;
281
282        mNextFrameToDecode = -1;
283        mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
284        initializeDecodingThread();
285    }
286
287    /**
288     * Pass true to mask the shape of the animated drawing content to a circle.
289     *
290     * <p> The masking circle will be the largest circle contained in the Drawable's bounds.
291     * Masking is done with BitmapShader, incurring minimal additional draw cost.
292     */
293    public final void setCircleMaskEnabled(boolean circleMaskEnabled) {
294        if (mCircleMaskEnabled != circleMaskEnabled) {
295            mCircleMaskEnabled = circleMaskEnabled;
296            // Anti alias only necessary when using circular mask
297            mPaint.setAntiAlias(circleMaskEnabled);
298            invalidateSelf();
299        }
300    }
301
302    public final boolean getCircleMaskEnabled() {
303        return mCircleMaskEnabled;
304    }
305
306    private void checkDestroyedLocked() {
307        if (mDestroyed) {
308            throw new IllegalStateException("Cannot perform operation on recycled drawable");
309        }
310    }
311
312    public boolean isDestroyed() {
313        synchronized (mLock) {
314            return mDestroyed;
315        }
316    }
317
318    /**
319     * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
320     * Bitmaps drawable to its BitmapProvider, if attached.
321     *
322     * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
323     */
324    public void destroy() {
325        if (mBitmapProvider == null) {
326            throw new IllegalStateException("BitmapProvider must be non-null");
327        }
328
329        Bitmap bitmapToReleaseA;
330        Bitmap bitmapToReleaseB = null;
331        synchronized (mLock) {
332            checkDestroyedLocked();
333
334            bitmapToReleaseA = mFrontBitmap;
335            mFrontBitmap = null;
336
337            if (mState != STATE_DECODING) {
338                bitmapToReleaseB = mBackBitmap;
339                mBackBitmap = null;
340            }
341
342            mDestroyed = true;
343        }
344
345        // For simplicity and safety, we don't destroy the state object here
346        mBitmapProvider.releaseBitmap(bitmapToReleaseA);
347        if (bitmapToReleaseB != null) {
348            mBitmapProvider.releaseBitmap(bitmapToReleaseB);
349        }
350    }
351
352    @Override
353    protected void finalize() throws Throwable {
354        try {
355            mFrameSequenceState.destroy();
356        } finally {
357            super.finalize();
358        }
359    }
360
361    @Override
362    public void draw(Canvas canvas) {
363        synchronized (mLock) {
364            checkDestroyedLocked();
365            if (mState == STATE_WAITING_TO_SWAP) {
366                // may have failed to schedule mark ready runnable,
367                // so go ahead and swap if swapping is due
368                if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
369                    mState = STATE_READY_TO_SWAP;
370                }
371            }
372
373            if (isRunning() && mState == STATE_READY_TO_SWAP) {
374                // Because draw has occurred, the view system is guaranteed to no longer hold a
375                // reference to the old mFrontBitmap, so we now use it to produce the next frame
376                Bitmap tmp = mBackBitmap;
377                mBackBitmap = mFrontBitmap;
378                mFrontBitmap = tmp;
379
380                BitmapShader tmpShader = mBackBitmapShader;
381                mBackBitmapShader = mFrontBitmapShader;
382                mFrontBitmapShader = tmpShader;
383
384                mLastSwap = SystemClock.uptimeMillis();
385
386                boolean continueLooping = true;
387                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
388                    mCurrentLoop++;
389                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == mLoopCount) ||
390                            (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
391                        continueLooping = false;
392                    }
393                }
394
395                if (continueLooping) {
396                    scheduleDecodeLocked();
397                } else {
398                    scheduleSelf(mCallbackRunnable, 0);
399                }
400            }
401        }
402
403        if (mCircleMaskEnabled) {
404            final Rect bounds = getBounds();
405            final int bitmapWidth = getIntrinsicWidth();
406            final int bitmapHeight = getIntrinsicHeight();
407            final float scaleX = 1.0f * bounds.width() / bitmapWidth;
408            final float scaleY = 1.0f * bounds.height() / bitmapHeight;
409
410            canvas.save();
411            // scale and translate to account for bounds, so we can operate in intrinsic
412            // width/height (so it's valid to use an unscaled bitmap shader)
413            canvas.translate(bounds.left, bounds.top);
414            canvas.scale(scaleX, scaleY);
415
416            final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
417            final float scaledDiameterX = unscaledCircleDiameter / scaleX;
418            final float scaledDiameterY = unscaledCircleDiameter / scaleY;
419
420            // Want to draw a circle, but we have to compensate for canvas scale
421            mTempRectF.set(
422                    (bitmapWidth - scaledDiameterX) / 2.0f,
423                    (bitmapHeight - scaledDiameterY) / 2.0f,
424                    (bitmapWidth + scaledDiameterX) / 2.0f,
425                    (bitmapHeight + scaledDiameterY) / 2.0f);
426            mPaint.setShader(mFrontBitmapShader);
427            canvas.drawOval(mTempRectF, mPaint);
428            canvas.restore();
429        } else {
430            mPaint.setShader(null);
431            canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
432        }
433    }
434
435    private void scheduleDecodeLocked() {
436        mState = STATE_SCHEDULED;
437        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
438        sDecodingThreadHandler.post(mDecodeRunnable);
439    }
440
441    @Override
442    public void run() {
443        // set ready to swap as necessary
444        boolean invalidate = false;
445        synchronized (mLock) {
446            if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
447                mState = STATE_READY_TO_SWAP;
448                invalidate = true;
449            }
450        }
451        if (invalidate) {
452            invalidateSelf();
453        }
454    }
455
456    @Override
457    public void start() {
458        if (!isRunning()) {
459            synchronized (mLock) {
460                checkDestroyedLocked();
461                if (mState == STATE_SCHEDULED) return; // already scheduled
462                mCurrentLoop = 0;
463                scheduleDecodeLocked();
464            }
465        }
466    }
467
468    @Override
469    public void stop() {
470        if (isRunning()) {
471            unscheduleSelf(this);
472        }
473    }
474
475    @Override
476    public boolean isRunning() {
477        synchronized (mLock) {
478            return mNextFrameToDecode > -1 && !mDestroyed;
479        }
480    }
481
482    @Override
483    public void unscheduleSelf(Runnable what) {
484        synchronized (mLock) {
485            mNextFrameToDecode = -1;
486            mState = 0;
487        }
488        super.unscheduleSelf(what);
489    }
490
491    @Override
492    public boolean setVisible(boolean visible, boolean restart) {
493        boolean changed = super.setVisible(visible, restart);
494
495        if (!visible) {
496            stop();
497        } else if (restart || changed) {
498            stop();
499            start();
500        }
501
502        return changed;
503    }
504
505    // drawing properties
506
507    @Override
508    public void setFilterBitmap(boolean filter) {
509        mPaint.setFilterBitmap(filter);
510    }
511
512    @Override
513    public void setAlpha(int alpha) {
514        mPaint.setAlpha(alpha);
515    }
516
517    @Override
518    public void setColorFilter(ColorFilter colorFilter) {
519        mPaint.setColorFilter(colorFilter);
520    }
521
522    @Override
523    public int getIntrinsicWidth() {
524        return mFrameSequence.getWidth();
525    }
526
527    @Override
528    public int getIntrinsicHeight() {
529        return mFrameSequence.getHeight();
530    }
531
532    @Override
533    public int getOpacity() {
534        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
535    }
536}
537