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