• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.bumptech.glide.load.resource.gif;
2 
3 import android.annotation.TargetApi;
4 import android.content.Context;
5 import android.content.res.Resources;
6 import android.graphics.Bitmap;
7 import android.graphics.Canvas;
8 import android.graphics.ColorFilter;
9 import android.graphics.Paint;
10 import android.graphics.PixelFormat;
11 import android.graphics.Rect;
12 import android.graphics.drawable.Drawable;
13 import android.os.Build;
14 import android.view.Gravity;
15 
16 import com.bumptech.glide.gifdecoder.GifDecoder;
17 import com.bumptech.glide.gifdecoder.GifHeader;
18 import com.bumptech.glide.load.Transformation;
19 import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
20 import 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  */
25 public 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      */
GifDrawable(Context context, GifDecoder.BitmapProvider bitmapProvider, BitmapPool bitmapPool, Transformation<Bitmap> frameTransformation, int targetFrameWidth, int targetFrameHeight, GifHeader gifHeader, byte[] data, Bitmap firstFrame)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 
GifDrawable(GifState state)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.
GifDrawable(GifDecoder decoder, GifFrameManager frameManager, Bitmap firstFrame, BitmapPool bitmapPool)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 
getFirstFrame()98     public Bitmap getFirstFrame() {
99         return state.firstFrame;
100     }
101 
setFrameTransformation(Transformation<Bitmap> frameTransformation, Bitmap firstFrame)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 
getDecoder()114     public GifDecoder getDecoder() {
115         return decoder;
116     }
117 
getFrameTransformation()118     public Transformation<Bitmap> getFrameTransformation() {
119         return state.frameTransformation;
120     }
121 
getData()122     public byte[] getData() {
123         return state.data;
124     }
125 
getFrameCount()126     public int getFrameCount() {
127         return decoder.getFrameCount();
128     }
129 
resetLoopCount()130     private void resetLoopCount() {
131         loopCount = 0;
132     }
133 
134     @Override
start()135     public void start() {
136         isStarted = true;
137         resetLoopCount();
138         if (isVisible) {
139             startRunning();
140         }
141     }
142 
143     @Override
stop()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      */
reset()160     private void reset() {
161         frameManager.clear();
162         invalidateSelf();
163     }
164 
startRunning()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 
stopRunning()176     private void stopRunning() {
177         isRunning = false;
178     }
179 
180     @Override
setVisible(boolean visible, boolean restart)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
getIntrinsicWidth()192     public int getIntrinsicWidth() {
193         return state.firstFrame.getWidth();
194     }
195 
196     @Override
getIntrinsicHeight()197     public int getIntrinsicHeight() {
198         return state.firstFrame.getHeight();
199     }
200 
201     @Override
isRunning()202     public boolean isRunning() {
203         return isRunning;
204     }
205 
206     // For testing.
setIsRunning(boolean isRunning)207     void setIsRunning(boolean isRunning) {
208         this.isRunning = isRunning;
209     }
210 
211     @Override
onBoundsChange(Rect bounds)212     protected void onBoundsChange(Rect bounds) {
213         super.onBoundsChange(bounds);
214         applyGravity = true;
215     }
216 
217     @Override
draw(Canvas canvas)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
setAlpha(int i)234     public void setAlpha(int i) {
235         paint.setAlpha(i);
236     }
237 
238     @Override
setColorFilter(ColorFilter colorFilter)239     public void setColorFilter(ColorFilter colorFilter) {
240         paint.setColorFilter(colorFilter);
241     }
242 
243     @Override
getOpacity()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
onFrameRead(int frameIndex)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
getConstantState()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      */
recycle()282     public void recycle() {
283         isRecycled = true;
284         state.bitmapPool.put(state.firstFrame);
285         frameManager.clear();
286     }
287 
288     // For testing.
isRecycled()289     boolean isRecycled() {
290         return isRecycled;
291     }
292 
293     @Override
isAnimated()294     public boolean isAnimated() {
295         return true;
296     }
297 
298     @Override
setLoopCount(int loopCount)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 
GifState(GifHeader header, byte[] data, Context context, Transformation<Bitmap> frameTransformation, int targetWidth, int targetHeight, GifDecoder.BitmapProvider provider, BitmapPool bitmapPool, Bitmap firstFrame)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 
GifState(GifState original)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
newDrawable(Resources res)356         public Drawable newDrawable(Resources res) {
357             return newDrawable();
358         }
359 
360         @Override
newDrawable()361         public Drawable newDrawable() {
362             return new GifDrawable(this);
363         }
364 
365         @Override
getChangingConfigurations()366         public int getChangingConfigurations() {
367             return 0;
368         }
369     }
370 }
371