1package com.bumptech.glide.load.resource.gif;
2
3import android.annotation.TargetApi;
4import android.content.Context;
5import android.content.res.Resources;
6import android.graphics.Bitmap;
7import android.graphics.Canvas;
8import android.graphics.ColorFilter;
9import android.graphics.Paint;
10import android.graphics.PixelFormat;
11import android.graphics.Rect;
12import android.graphics.drawable.Drawable;
13import android.os.Build;
14import android.view.Gravity;
15
16import com.bumptech.glide.gifdecoder.GifDecoder;
17import com.bumptech.glide.gifdecoder.GifHeader;
18import com.bumptech.glide.load.Transformation;
19import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
20import com.bumptech.glide.load.resource.drawable.GlideDrawable;
21
22/**
23 * An animated {@link android.graphics.drawable.Drawable} that plays the frames of an animated GIF.
24 */
25public class GifDrawable extends GlideDrawable implements GifFrameManager.FrameCallback {
26    private final Paint paint = new Paint();
27    private final Rect destRect = new Rect();
28    private final GifFrameManager frameManager;
29    private final GifState state;
30    private final GifDecoder decoder;
31
32    /** True if the drawable is currently animating. */
33    private boolean isRunning;
34    /** True if the drawable should animate while visible. */
35    private boolean isStarted;
36    /** True if the drawable's resources have been recycled. */
37    private boolean isRecycled;
38    /**
39     * True if the drawable is currently visible. Default to true because on certain platforms (at least 4.1.1),
40     * setVisible is not called on {@link android.graphics.drawable.Drawable Drawables} during
41     * {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. See issue #130.
42     */
43    private boolean isVisible = true;
44    /** The number of times we've looped over all the frames in the gif. */
45    private int loopCount;
46    /** The number of times to loop through the gif animation. */
47    private int maxLoopCount = LOOP_FOREVER;
48
49    private boolean applyGravity;
50
51    /**
52     * Constructor for GifDrawable.
53     *
54     * @see #setFrameTransformation(com.bumptech.glide.load.Transformation, android.graphics.Bitmap)
55     *
56     * @param context A context.
57     * @param bitmapProvider An {@link com.bumptech.glide.gifdecoder.GifDecoder.BitmapProvider} that can be used to
58     *                       retrieve re-usable {@link android.graphics.Bitmap}s.
59     * @param bitmapPool A {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} that can be used to return
60     *                   the first frame when this drawable is recycled.
61     * @param frameTransformation An {@link com.bumptech.glide.load.Transformation} that can be applied to each frame.
62     * @param targetFrameWidth The desired width of the frames displayed by this drawable (the width of the view or
63     *                         {@link com.bumptech.glide.request.target.Target} this drawable is being loaded into).
64     * @param targetFrameHeight The desired height of the frames displayed by this drawable (the height of the view or
65     *                          {@link com.bumptech.glide.request.target.Target} this drawable is being loaded into).
66     * @param gifHeader The header data for this gif.
67     * @param data The full bytes of the gif.
68     * @param firstFrame The decoded and transformed first frame of this gif.
69     */
70    public GifDrawable(Context context, GifDecoder.BitmapProvider bitmapProvider, BitmapPool bitmapPool,
71            Transformation<Bitmap> frameTransformation, int targetFrameWidth, int targetFrameHeight,
72            GifHeader gifHeader, byte[] data, Bitmap firstFrame) {
73        this(new GifState(gifHeader, data, context, frameTransformation, targetFrameWidth, targetFrameHeight,
74                bitmapProvider, bitmapPool, firstFrame));
75    }
76
77    GifDrawable(GifState state) {
78        if (state == null) {
79            throw new NullPointerException("GifState must not be null");
80        }
81
82        this.state = state;
83        this.decoder = new GifDecoder(state.bitmapProvider);
84        decoder.setData(state.gifHeader, state.data);
85        frameManager = new GifFrameManager(state.context, decoder, state.targetWidth, state.targetHeight);
86        frameManager.setFrameTransformation(state.frameTransformation);
87    }
88
89    // Visible for testing.
90    GifDrawable(GifDecoder decoder, GifFrameManager frameManager, Bitmap firstFrame, BitmapPool bitmapPool) {
91        this.decoder = decoder;
92        this.frameManager = frameManager;
93        this.state = new GifState(null);
94        state.bitmapPool = bitmapPool;
95        state.firstFrame = firstFrame;
96    }
97
98    public Bitmap getFirstFrame() {
99        return state.firstFrame;
100    }
101
102    public void setFrameTransformation(Transformation<Bitmap> frameTransformation, Bitmap firstFrame) {
103        if (firstFrame == null) {
104            throw new NullPointerException("The first frame of the GIF must not be null");
105        }
106        if (frameTransformation == null) {
107            throw new NullPointerException("The frame transformation must not be null");
108        }
109        state.frameTransformation = frameTransformation;
110        state.firstFrame = firstFrame;
111        frameManager.setFrameTransformation(frameTransformation);
112    }
113
114    public GifDecoder getDecoder() {
115        return decoder;
116    }
117
118    public Transformation<Bitmap> getFrameTransformation() {
119        return state.frameTransformation;
120    }
121
122    public byte[] getData() {
123        return state.data;
124    }
125
126    public int getFrameCount() {
127        return decoder.getFrameCount();
128    }
129
130    private void resetLoopCount() {
131        loopCount = 0;
132    }
133
134    @Override
135    public void start() {
136        isStarted = true;
137        resetLoopCount();
138        if (isVisible) {
139            startRunning();
140        }
141    }
142
143    @Override
144    public void stop() {
145        isStarted = false;
146        stopRunning();
147
148        // On APIs > honeycomb we know our drawable is not being displayed anymore when it's callback is cleared and so
149        // we can use the absence of a callback as an indication that it's ok to clear our temporary data. Prior to
150        // honeycomb we can't tell if our callback is null and instead eagerly reset to avoid holding on to resources we
151        // no longer need.
152        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
153            reset();
154        }
155    }
156
157    /**
158     * Clears temporary data and resets the drawable back to the first frame.
159     */
160    private void reset() {
161        frameManager.clear();
162        invalidateSelf();
163    }
164
165    private void startRunning() {
166        // If we have only a single frame, we don't want to decode it endlessly.
167        if (decoder.getFrameCount() == 1) {
168            invalidateSelf();
169        }  else if (!isRunning) {
170            isRunning = true;
171            frameManager.getNextFrame(this);
172            invalidateSelf();
173        }
174    }
175
176    private void stopRunning() {
177        isRunning = false;
178    }
179
180    @Override
181    public boolean setVisible(boolean visible, boolean restart) {
182        isVisible = visible;
183        if (!visible) {
184            stopRunning();
185        } else if (isStarted) {
186            startRunning();
187        }
188        return super.setVisible(visible, restart);
189    }
190
191    @Override
192    public int getIntrinsicWidth() {
193        return state.firstFrame.getWidth();
194    }
195
196    @Override
197    public int getIntrinsicHeight() {
198        return state.firstFrame.getHeight();
199    }
200
201    @Override
202    public boolean isRunning() {
203        return isRunning;
204    }
205
206    // For testing.
207    void setIsRunning(boolean isRunning) {
208        this.isRunning = isRunning;
209    }
210
211    @Override
212    protected void onBoundsChange(Rect bounds) {
213        super.onBoundsChange(bounds);
214        applyGravity = true;
215    }
216
217    @Override
218    public void draw(Canvas canvas) {
219        if (isRecycled) {
220            return;
221        }
222
223        if (applyGravity) {
224            Gravity.apply(GifState.GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), destRect);
225            applyGravity = false;
226        }
227
228        Bitmap currentFrame = frameManager.getCurrentFrame();
229        Bitmap toDraw = currentFrame != null ? currentFrame : state.firstFrame;
230        canvas.drawBitmap(toDraw, null, destRect, paint);
231    }
232
233    @Override
234    public void setAlpha(int i) {
235        paint.setAlpha(i);
236    }
237
238    @Override
239    public void setColorFilter(ColorFilter colorFilter) {
240        paint.setColorFilter(colorFilter);
241    }
242
243    @Override
244    public int getOpacity() {
245        // We can't tell, so default to transparent to be safe.
246        return PixelFormat.TRANSPARENT;
247    }
248
249    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
250    @Override
251    public void onFrameRead(int frameIndex) {
252        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && getCallback() == null) {
253            stop();
254            reset();
255            return;
256        }
257        if (!isRunning) {
258            return;
259        }
260
261        invalidateSelf();
262
263        if (frameIndex == decoder.getFrameCount() - 1) {
264            loopCount++;
265        }
266
267        if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
268            stop();
269        } else {
270            frameManager.getNextFrame(this);
271        }
272    }
273
274    @Override
275    public ConstantState getConstantState() {
276        return state;
277    }
278
279    /**
280     * Clears any resources for loading frames that are currently held on to by this object.
281     */
282    public void recycle() {
283        isRecycled = true;
284        state.bitmapPool.put(state.firstFrame);
285        frameManager.clear();
286    }
287
288    // For testing.
289    boolean isRecycled() {
290        return isRecycled;
291    }
292
293    @Override
294    public boolean isAnimated() {
295        return true;
296    }
297
298    @Override
299    public void setLoopCount(int loopCount) {
300        if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
301            throw new IllegalArgumentException("Loop count must be greater than 0, or equal to "
302                    + "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC");
303        }
304
305        if (loopCount == LOOP_INTRINSIC) {
306            maxLoopCount = decoder.getLoopCount();
307        } else {
308            maxLoopCount = loopCount;
309        }
310    }
311
312    static class GifState extends ConstantState {
313        private static final int GRAVITY = Gravity.FILL;
314        GifHeader gifHeader;
315        byte[] data;
316        Context context;
317        Transformation<Bitmap> frameTransformation;
318        int targetWidth;
319        int targetHeight;
320        GifDecoder.BitmapProvider bitmapProvider;
321        BitmapPool bitmapPool;
322        Bitmap firstFrame;
323
324        public GifState(GifHeader header, byte[] data, Context context,
325                Transformation<Bitmap> frameTransformation, int targetWidth, int targetHeight,
326                GifDecoder.BitmapProvider provider, BitmapPool bitmapPool, Bitmap firstFrame) {
327            if (firstFrame == null) {
328                throw new NullPointerException("The first frame of the GIF must not be null");
329            }
330            gifHeader = header;
331            this.data = data;
332            this.bitmapPool = bitmapPool;
333            this.firstFrame = firstFrame;
334            this.context = context.getApplicationContext();
335            this.frameTransformation = frameTransformation;
336            this.targetWidth = targetWidth;
337            this.targetHeight = targetHeight;
338            bitmapProvider = provider;
339        }
340
341        public GifState(GifState original) {
342            if (original != null) {
343                gifHeader = original.gifHeader;
344                data = original.data;
345                context = original.context;
346                frameTransformation = original.frameTransformation;
347                targetWidth = original.targetWidth;
348                targetHeight = original.targetHeight;
349                bitmapProvider = original.bitmapProvider;
350                bitmapPool = original.bitmapPool;
351                firstFrame = original.firstFrame;
352            }
353        }
354
355        @Override
356        public Drawable newDrawable(Resources res) {
357            return newDrawable();
358        }
359
360        @Override
361        public Drawable newDrawable() {
362            return new GifDrawable(this);
363        }
364
365        @Override
366        public int getChangingConfigurations() {
367            return 0;
368        }
369    }
370}
371