FrameSequenceDrawable.java revision 4eb541aff092a057b27b917f09d33aba226dffed
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            if (!mDestroyed) {
274                destroy();
275            }
276        } finally {
277            super.finalize();
278        }
279    }
280
281    @Override
282    public void draw(Canvas canvas) {
283        synchronized (mLock) {
284            checkDestroyedLocked();
285            if (mState == STATE_WAITING_TO_SWAP) {
286                // may have failed to schedule mark ready runnable,
287                // so go ahead and swap if swapping is due
288                if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
289                    mState = STATE_READY_TO_SWAP;
290                }
291            }
292
293            if (isRunning() && mState == STATE_READY_TO_SWAP) {
294                // Because draw has occurred, the view system is guaranteed to no longer hold a
295                // reference to the old mFrontBitmap, so we now use it to produce the next frame
296                Bitmap tmp = mBackBitmap;
297                mBackBitmap = mFrontBitmap;
298                mFrontBitmap = tmp;
299
300                mLastSwap = SystemClock.uptimeMillis();
301
302                boolean continueLooping = true;
303                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
304                    mCurrentLoop++;
305                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
306                            (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
307                        continueLooping = false;
308                    }
309                }
310
311                if (continueLooping) {
312                    scheduleDecodeLocked();
313                } else {
314                    scheduleSelf(mCallbackRunnable, 0);
315                }
316            }
317        }
318
319        canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
320    }
321
322    private void scheduleDecodeLocked() {
323        mState = STATE_SCHEDULED;
324        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
325        sDecodingThreadHandler.post(mDecodeRunnable);
326    }
327
328    @Override
329    public void run() {
330        // set ready to swap
331        synchronized (mLock) {
332            if (mState != STATE_WAITING_TO_SWAP || mNextFrameToDecode < 0) return;
333            mState = STATE_READY_TO_SWAP;
334        }
335        invalidateSelf();
336    }
337
338    @Override
339    public void start() {
340        if (!isRunning()) {
341            synchronized (mLock) {
342                checkDestroyedLocked();
343                if (mState == STATE_SCHEDULED) return; // already scheduled
344                mCurrentLoop = 0;
345                scheduleDecodeLocked();
346            }
347        }
348    }
349
350    @Override
351    public void stop() {
352        if (isRunning()) {
353            unscheduleSelf(this);
354        }
355    }
356
357    @Override
358    public boolean isRunning() {
359        synchronized (mLock) {
360            return mNextFrameToDecode > -1 && !mDestroyed;
361        }
362    }
363
364    @Override
365    public void unscheduleSelf(Runnable what) {
366        synchronized (mLock) {
367            mNextFrameToDecode = -1;
368        }
369        super.unscheduleSelf(what);
370    }
371
372    @Override
373    public boolean setVisible(boolean visible, boolean restart) {
374        boolean changed = super.setVisible(visible, restart);
375
376        if (!visible) {
377            stop();
378        } else if (restart || changed) {
379            stop();
380            start();
381        }
382
383        return changed;
384    }
385
386    // drawing properties
387
388    @Override
389    public void setFilterBitmap(boolean filter) {
390        mPaint.setFilterBitmap(filter);
391    }
392
393    @Override
394    public void setAlpha(int alpha) {
395        mPaint.setAlpha(alpha);
396    }
397
398    @Override
399    public void setColorFilter(ColorFilter colorFilter) {
400        mPaint.setColorFilter(colorFilter);
401    }
402
403    @Override
404    public int getIntrinsicWidth() {
405        return mFrameSequence.getWidth();
406    }
407
408    @Override
409    public int getIntrinsicHeight() {
410        return mFrameSequence.getHeight();
411    }
412
413    @Override
414    public int getOpacity() {
415        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
416    }
417}
418