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