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