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