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