FrameSequenceDrawable.java revision e36c5d675c8c2f900ef186a55edf71ce36ca9fa0
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.SystemClock;
30
31public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable {
32    private static final Object sLock = new Object();
33    private static HandlerThread sDecodingThread;
34    private static Handler sDecodingThreadHandler;
35    private static void initializeDecodingThread() {
36        synchronized (sLock) {
37            if (sDecodingThread != null) return;
38
39            sDecodingThread = new HandlerThread("FrameSequence decoding thread");
40            sDecodingThread.start();
41            sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
42        }
43    }
44
45    public static interface OnFinishedListener {
46        /**
47         * Called when a FrameSequenceDrawable has finished looping.
48         *
49         * Note that this is will not be called if the drawable is explicitly
50         * stopped, or marked invisible.
51         */
52        public abstract void onFinished(FrameSequenceDrawable drawable);
53    }
54
55    /**
56     * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
57     *
58     * @see setLoopBehavior()
59     */
60    public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
61        mOnFinishedListener = onFinishedListener;
62    }
63
64    /**
65     * Loop only once.
66     */
67    public static final int LOOP_ONCE = 1;
68
69    /**
70     * Loop continuously. The OnFinishedListener will never be called.
71     */
72    public static final int LOOP_INF = 2;
73
74    /**
75     * Use loop count stored in source data, or LOOP_ONCE if not present.
76     */
77    public static final int LOOP_DEFAULT = 3;
78
79    /**
80     * Define looping behavior of frame sequence.
81     *
82     * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT
83     */
84    public void setLoopBehavior(int loopBehavior) {
85        mLoopBehavior = loopBehavior;
86    }
87
88    private final FrameSequence mFrameSequence;
89    private final FrameSequence.State mFrameSequenceState;
90
91    private final Paint mPaint;
92    private final Rect mSrcRect;
93
94    //Protects the fields below
95    private final Object mLock = new Object();
96
97    private Bitmap mFrontBitmap;
98    private Bitmap mBackBitmap;
99
100    private static final int STATE_SCHEDULED = 1;
101    private static final int STATE_DECODING = 2;
102    private static final int STATE_WAITING_TO_SWAP = 3;
103    private static final int STATE_READY_TO_SWAP = 4;
104
105    private int mState;
106    private int mCurrentLoop;
107    private int mLoopBehavior = LOOP_DEFAULT;
108
109    private long mLastSwap;
110    private int mNextFrameToDecode;
111    private OnFinishedListener mOnFinishedListener;
112
113    /**
114     * Runs on decoding thread, only modifies mBackBitmap's pixels
115     */
116    private Runnable mDecodeRunnable = new Runnable() {
117        @Override
118        public void run() {
119            int nextFrame;
120            Bitmap bitmap;
121            synchronized (mLock) {
122                nextFrame = mNextFrameToDecode;
123                if (nextFrame < 0) {
124                    return;
125                }
126                bitmap = mBackBitmap;
127                mState = STATE_DECODING;
128            }
129            int lastFrame = nextFrame - 2;
130            long invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
131
132            synchronized (mLock) {
133                if (mNextFrameToDecode < 0 || mState != STATE_DECODING) return;
134                invalidateTimeMs += mLastSwap;
135
136                mState = STATE_WAITING_TO_SWAP;
137            }
138            scheduleSelf(FrameSequenceDrawable.this, invalidateTimeMs);
139        }
140    };
141
142    private Runnable mCallbackRunnable = new Runnable() {
143        @Override
144        public void run() {
145            if (mOnFinishedListener != null) {
146                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
147            }
148        }
149    };
150
151    public FrameSequenceDrawable(FrameSequence frameSequence) {
152        if (frameSequence == null) throw new IllegalArgumentException();
153
154        mFrameSequence = frameSequence;
155        mFrameSequenceState = frameSequence.createState();
156        // TODO: add callback for requesting bitmaps, to allow for reuse
157        final int width = frameSequence.getWidth();
158        final int height = frameSequence.getHeight();
159
160        mFrontBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
161        mBackBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
162        mSrcRect = new Rect(0, 0, width, height);
163        mPaint = new Paint();
164        mPaint.setFilterBitmap(true);
165
166        mLastSwap = 0;
167
168        mNextFrameToDecode = -1;
169        mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
170        initializeDecodingThread();
171    }
172
173    @Override
174    protected void finalize() throws Throwable {
175        try {
176            mFrontBitmap.recycle();
177            mBackBitmap.recycle();
178            mFrameSequenceState.recycle();
179        } finally {
180            super.finalize();
181        }
182    }
183
184    @Override
185    public void draw(Canvas canvas) {
186        synchronized (mLock) {
187            if (isRunning() && mState == STATE_READY_TO_SWAP) {
188                // Because draw has occurred, the view system is guaranteed to no longer hold a
189                // reference to the old mFrontBitmap, so we now use it to produce the next frame
190                Bitmap tmp = mBackBitmap;
191                mBackBitmap = mFrontBitmap;
192                mFrontBitmap = tmp;
193
194                mLastSwap = SystemClock.uptimeMillis();
195
196                boolean continueLooping = true;
197                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
198                    mCurrentLoop++;
199                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
200                        (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
201                        continueLooping = false;
202                    }
203                }
204
205                if (continueLooping) {
206                    scheduleDecodeLocked();
207                } else {
208                    scheduleSelf(mCallbackRunnable, 0);
209                }
210            }
211        }
212
213        canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
214    }
215
216    private void scheduleDecodeLocked() {
217        mState = STATE_SCHEDULED;
218        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
219        sDecodingThreadHandler.post(mDecodeRunnable);
220    }
221
222    @Override
223    public void run() {
224        // set ready to swap
225        synchronized (mLock) {
226            if (mState != STATE_WAITING_TO_SWAP || mNextFrameToDecode < 0) return;
227            mState = STATE_READY_TO_SWAP;
228        }
229        invalidateSelf();
230    }
231
232    @Override
233    public void start() {
234        if (!isRunning()) {
235            synchronized (mLock) {
236                if (mState == STATE_SCHEDULED) return; // already scheduled
237                mCurrentLoop = 0;
238                scheduleDecodeLocked();
239            }
240        }
241    }
242
243    @Override
244    public void stop() {
245        if (isRunning()) {
246            unscheduleSelf(this);
247        }
248    }
249
250    @Override
251    public boolean isRunning() {
252        synchronized (mLock) {
253            return mNextFrameToDecode > -1;
254        }
255    }
256
257    @Override
258    public void scheduleSelf(Runnable what, long when) {
259        super.scheduleSelf(what, when);
260    }
261
262    @Override
263    public void unscheduleSelf(Runnable what) {
264        synchronized (mLock) {
265            mNextFrameToDecode = -1;
266        }
267        super.unscheduleSelf(what);
268    }
269
270    @Override
271    public boolean setVisible(boolean visible, boolean restart) {
272        boolean changed = super.setVisible(visible, restart);
273
274        if (!visible) {
275            stop();
276        } else if (restart || changed) {
277            stop();
278            start();
279        }
280
281        return changed;
282    }
283
284    // drawing properties
285
286    @Override
287    public void setFilterBitmap(boolean filter) {
288        mPaint.setFilterBitmap(filter);
289    }
290
291    @Override
292    public void setAlpha(int alpha) {
293        mPaint.setAlpha(alpha);
294    }
295
296    @Override
297    public void setColorFilter(ColorFilter colorFilter) {
298        mPaint.setColorFilter(colorFilter);
299    }
300
301    @Override
302    public int getIntrinsicWidth() {
303        return mFrameSequence.getWidth();
304    }
305
306    @Override
307    public int getIntrinsicHeight() {
308        return mFrameSequence.getHeight();
309    }
310
311    @Override
312    public int getOpacity() {
313        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
314    }
315}
316