From ea01ddd754126290fc18c3dc8f6095ce0216768a Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Fri, 6 Jun 2014 17:45:36 -0700 Subject: [PATCH] Add GifDecoder and GifResource* --- .../resource/drawable/DrawableResource.java | 29 + .../load/resource/drawable/GifBitmap.java | 43 + .../resource/drawable/GifBitmapResource.java | 25 + .../drawable/GifBitmapResourceDecoder.java | 58 ++ .../drawable/GifBitmapResourceEncoder.java | 35 + .../GifBitmapStreamResourceDecoder.java | 26 + .../glide/load/resource/gif/GifDecoder.java | 854 ++++++++++++++++++ .../glide/load/resource/gif/GifDrawable.java | 113 +++ .../load/resource/gif/GifFrameLoader.java | 43 + .../load/resource/gif/GifFrameManager.java | 99 ++ .../resource/gif/GifFrameResourceDecoder.java | 20 + .../glide/load/resource/gif/GifResource.java | 34 + .../load/resource/gif/GifResourceDecoder.java | 36 + .../load/resource/gif/GifResourceEncoder.java | 30 + .../GifBitmapDrawableTranscoder.java | 40 + .../drawable/DrawableResourceTest.java | 47 + .../GifBitmapResourceDecoderTest.java | 87 ++ .../GifBitmapResourceEncoderTest.java | 93 ++ .../GifBitmapStreamResourceDecoderTest.java | 63 ++ .../load/resource/gif/GifDrawableTest.java | 166 ++++ .../gif/GifFrameResourceDecoderTest.java | 40 + .../resource/gif/GifResourceEncoderTest.java | 64 ++ .../load/resource/gif/GifResourceTest.java | 53 ++ .../GifBitmapDrawableTranscoderTest.java | 104 +++ .../res/layout/flickr_search_activity.xml | 5 + .../samples/flickr/FlickrSearchActivity.java | 13 + 26 files changed, 2220 insertions(+) create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableResource.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmap.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResource.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifDecoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawable.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameManager.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifResource.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceDecoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceEncoder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoder.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/drawable/DrawableResourceTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoderTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoderTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoderTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoderTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceEncoderTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceTest.java create mode 100644 library/src/test/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoderTest.java diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableResource.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableResource.java new file mode 100644 index 000000000..b10d1a15f --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableResource.java @@ -0,0 +1,29 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.graphics.drawable.Drawable; +import com.bumptech.glide.Resource; + +public class DrawableResource extends Resource { + private final Drawable drawable; + private final Resource wrapped; + + public DrawableResource(Drawable drawable, Resource wrapped) { + this.drawable = drawable; + this.wrapped = wrapped; + } + + @Override + public Drawable get() { + return drawable; + } + + @Override + public int getSize() { + return wrapped.getSize(); + } + + @Override + protected void recycleInternal() { + wrapped.recycle(); + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmap.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmap.java new file mode 100644 index 000000000..bffdddf7f --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmap.java @@ -0,0 +1,43 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.resource.gif.GifDrawable; + +public class GifBitmap { + private Resource gifResource; + private Resource bitmapResource; + private Resources resources; + + public GifBitmap(Resources resources, Resource bitmapResource) { + this.resources = resources; + this.bitmapResource = bitmapResource; + } + + public GifBitmap(Resource gifResource) { + this.gifResource = gifResource; + } + + public int getSize() { + return 0; + } + + public Resource getBitmapResource() { + return bitmapResource; + } + + public Resource getGifResource() { + return gifResource; + } + + public Drawable getDrawable() { + if (gifResource != null) { + return gifResource.get(); + } else { + return new BitmapDrawable(resources, bitmapResource.get()); + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResource.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResource.java new file mode 100644 index 000000000..b074e4b8d --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResource.java @@ -0,0 +1,25 @@ +package com.bumptech.glide.load.resource.drawable; + +import com.bumptech.glide.Resource; + +public class GifBitmapResource extends Resource { + private GifBitmap data; + + public GifBitmapResource(GifBitmap data) { + this.data = data; + } + + @Override + public GifBitmap get() { + return data; + } + + @Override + public int getSize() { + return data.getSize(); + } + + @Override + protected void recycleInternal() { + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoder.java new file mode 100644 index 000000000..7b366e82b --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoder.java @@ -0,0 +1,58 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.content.Context; +import android.graphics.Bitmap; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.model.ImageVideoWrapper; +import com.bumptech.glide.load.resource.bitmap.ImageHeaderParser; +import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; +import com.bumptech.glide.load.resource.gif.GifDrawable; +import com.bumptech.glide.util.ByteArrayPool; + +import java.io.IOException; +import java.io.InputStream; + +public class GifBitmapResourceDecoder implements ResourceDecoder { + private Context context; + private final ResourceDecoder bitmapDecoder; + private final ResourceDecoder gifDecoder; + + public GifBitmapResourceDecoder(Context context, ResourceDecoder bitmapDecoder, + ResourceDecoder gifDecoder) { + this.context = context; + this.bitmapDecoder = bitmapDecoder; + this.gifDecoder = gifDecoder; + } + + @Override + public Resource decode(ImageVideoWrapper source, int width, int height) throws IOException { + ByteArrayPool pool = ByteArrayPool.get(); + InputStream is = source.getStream(); + GifBitmap result = null; + if (is != null) { + byte[] tempBytes = pool.getBytes(); + RecyclableBufferedInputStream bis = new RecyclableBufferedInputStream(is, tempBytes); + bis.mark(1024); + ImageHeaderParser.ImageType type = new ImageHeaderParser(bis).getType(); + bis.reset(); + + if (type == ImageHeaderParser.ImageType.GIF) { + Resource gifResource = gifDecoder.decode(is, width, height); + result = new GifBitmap(gifResource); + } + pool.releaseBytes(tempBytes); + } + + if (result == null) { + Resource bitmapResource = bitmapDecoder.decode(source, width, height); + result = new GifBitmap(context.getResources(), bitmapResource); + } + return new GifBitmapResource(result); + } + + @Override + public String getId() { + return "GifBitmapResourceDecoder.com.bumptech.glide.load.resource.drawable"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoder.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoder.java new file mode 100644 index 000000000..5852b5c23 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoder.java @@ -0,0 +1,35 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.graphics.Bitmap; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceEncoder; +import com.bumptech.glide.load.resource.gif.GifDrawable; + +import java.io.OutputStream; + +public class GifBitmapResourceEncoder implements ResourceEncoder { + private final ResourceEncoder bitmapEncoder; + private final ResourceEncoder gifEncoder; + + public GifBitmapResourceEncoder(ResourceEncoder bitmapEncoder, ResourceEncoder gifEncoder) { + this.bitmapEncoder = bitmapEncoder; + this.gifEncoder = gifEncoder; + } + + @Override + public boolean encode(Resource resource, OutputStream os) { + final GifBitmap gifBitmap = resource.get(); + final Resource bitmapResource = gifBitmap.getBitmapResource(); + + if (bitmapResource != null) { + return bitmapEncoder.encode(bitmapResource, os); + } else { + return gifEncoder.encode(gifBitmap.getGifResource(), os); + } + } + + @Override + public String getId() { + return "GifBitmapResourceEncoder.com.bumptech.glide.load.resource.drawable"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoder.java new file mode 100644 index 000000000..b217c56b1 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoder.java @@ -0,0 +1,26 @@ +package com.bumptech.glide.load.resource.drawable; + +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.model.ImageVideoWrapper; + +import java.io.IOException; +import java.io.InputStream; + +public class GifBitmapStreamResourceDecoder implements ResourceDecoder{ + private ResourceDecoder gifBitmapDecoder; + + public GifBitmapStreamResourceDecoder(ResourceDecoder gifBitmapDecoder) { + this.gifBitmapDecoder = gifBitmapDecoder; + } + + @Override + public Resource decode(InputStream source, int width, int height) throws IOException { + return gifBitmapDecoder.decode(new ImageVideoWrapper(source, null), width, height); + } + + @Override + public String getId() { + return "GifBitmapStreamResourceDecoder.com.bumptech.glide.resource.drawable"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDecoder.java new file mode 100644 index 000000000..8a809b4c2 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDecoder.java @@ -0,0 +1,854 @@ +package com.bumptech.glide.load.resource.gif; + + +/** + * Copyright (c) 2013 Xcellent Creations, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import android.graphics.Bitmap; +import android.os.SystemClock; +import android.util.Log; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapResource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +/** + * Reads frame data from a GIF image source and decodes it into individual frames + * for animation purposes. Image data can be read from either and InputStream source + * or a byte[]. + * + * This class is optimized for running animations with the frames, there + * are no methods to get individual frame images, only to decode the next frame in the + * animation sequence. Instead, it lowers its memory footprint by only housing the minimum + * data necessary to decode the next frame in the animation sequence. + * + * The animation must be manually moved forward using {@link #advance()} before requesting the next + * frame. This method must also be called before you request the first frame or an error will + * occur. + * + * Implementation adapted from sample code published in Lyons. (2004). Java for Programmers, + * republished under the MIT Open Source License + */ +public class GifDecoder { + private static final String TAG = GifDecoder.class.getSimpleName(); + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + /** + * File read status: Error decoding file (may be partially decoded) + */ + public static final int STATUS_FORMAT_ERROR = 1; + /** + * File read status: Unable to open source. + */ + public static final int STATUS_OPEN_ERROR = 2; + /** + * max decoder pixel stack size + */ + protected static final int MAX_STACK_SIZE = 4096; + + /** + * GIF Disposal Method meaning take no action + */ + private static final int DISPOSAL_UNSPECIFIED = 0; + /** + * GIF Disposal Method meaning leave canvas from previous frame + */ + private static final int DISPOSAL_NONE = 1; + /** + * GIF Disposal Method meaning clear canvas to background color + */ + private static final int DISPOSAL_BACKGROUND = 2; + /** + * GIF Disposal Method meaning clear canvas to frame before last + */ + private static final int DISPOSAL_PREVIOUS = 3; + /** + * Global status code of GIF data parsing + */ + protected int status; + + //Global File Header values and parsing flags + protected int width; // full image width + protected int height; // full image height + protected boolean gctFlag; // global color table used + protected int gctSize; // size of global color table + protected int loopCount = 1; // iterations; 0 = repeat forever + protected int[] gct; // global color table + protected int[] act; // active color table + protected int bgIndex; // background color index + protected int bgColor; // background color + protected int pixelAspect; // pixel aspect ratio + protected boolean lctFlag; // local color table flag + protected int lctSize; // local color table size + private boolean isTransparent; + + // Raw GIF data from input source + protected ByteBuffer rawData; + + // Raw data read working array + protected byte[] block = new byte[256]; // current data block + protected int blockSize = 0; // block size last graphic control extension info + + // LZW decoder working arrays + protected short[] prefix; + protected byte[] suffix; + protected byte[] pixelStack; + protected byte[] mainPixels; + protected int[] mainScratch; + + protected ArrayList frames; // frames read from current file + protected GifFrame currentFrame; + + protected int framePointer; + protected int frameCount; + private BitmapPool bitmapPool; + private Bitmap currentImage; + private byte[] data; + + /** + * Inner model class housing metadata for each frame + */ + private static class GifFrame { + public int ix, iy, iw, ih; + /* Control Flags */ + public boolean interlace; + public boolean transparency; + /* Disposal Method */ + public int dispose; + /* Transparency Index */ + public int transIndex; + /* Delay, in ms, to next frame */ + public int delay; + /* Index in the raw buffer where we need to start reading to decode */ + public int bufferFrameStart; + /* Local Color Table */ + public int[] lct; + } + + public GifDecoder(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public boolean isTransparent() { + return isTransparent; + } + + public int getGifByteSize() { + return data.length; + } + + public byte[] getData() { + return data; + } + + public int getDecodedFrameByteSize() { + // 4 == ARGB_8888, 2 == RGB_565 + return frameCount * width * height * (isTransparent ? 4 : 2); + } + + /** + * Move the animation frame counter forward + */ + public void advance() { + framePointer = (framePointer + 1) % frameCount; + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + public int getDelay(int n) { + int delay = -1; + if ((n >= 0) && (n < frameCount)) { + delay = frames.get(n).delay; + } + return delay; + } + + /** + * Gets display duration for the upcoming frame + */ + public int getNextDelay() { + if (frameCount <= 0 || framePointer < 0) { + return -1; + } + + return getDelay(framePointer); + } + + /** + * Gets the number of frames read from file. + * + * @return frame count + */ + public int getFrameCount() { + return frameCount; + } + + /** + * Gets the current index of the animation frame, or -1 if animation hasn't not yet started + * + * @return frame index + */ + public int getCurrentFrameIndex() { + return framePointer; + } + + /** + * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return loopCount; + } + + /** + * Get the next frame in the animation sequence. + * + * @return Bitmap representation of frame + */ + public Resource getNextFrame() { + if (frameCount <= 0 || framePointer < 0 ) { + return null; + } + + long startTime = SystemClock.currentThreadTimeMillis(); + + GifFrame frame = frames.get(framePointer); + + //Set the appropriate color table + if (frame.lct == null) { + act = gct; + } else { + act = frame.lct; + if (bgIndex == frame.transIndex) { + bgColor = 0; + } + } + + int save = 0; + if (frame.transparency) { + save = act[frame.transIndex]; + act[frame.transIndex] = 0; // set transparent color if specified + } + if (act == null) { + Log.w(TAG, "No Valid Color Table"); + status = STATUS_FORMAT_ERROR; // no color table defined + return null; + } + + Bitmap result = setPixels(framePointer); // transfer pixel data to image + currentImage = result; + + // Reset the transparent pixel in the color table + if (frame.transparency) { + act[frame.transIndex] = save; + } + + return new BitmapResource(result, bitmapPool); + } + + /** + * Reads GIF image from stream + * + * @param is containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(InputStream is, int contentLength) { + if (is != null) { + try { + int capacity = (contentLength > 0) ? (contentLength + 4096) : 16384; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity); + int nRead; + byte[] data = new byte[16384]; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + + read(buffer.toByteArray()); + } catch (IOException e) { + Log.w(TAG, "Error reading data from stream", e); + } + } else { + status = STATUS_OPEN_ERROR; + } + + try { + is.close(); + } catch (Exception e) { + Log.w(TAG, "Error closing stream", e); + } + + return status; + } + + /** + * Reads GIF image from byte array + * + * @param data containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(byte[] data) { + this.data = data; + init(); + if (data != null) { + //Initialize the raw data buffer + rawData = ByteBuffer.wrap(data); + rawData.rewind(); + rawData.order(ByteOrder.LITTLE_ENDIAN); + + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + /** + * Creates new frame image from current data (and previous frames as specified by their disposition codes). + */ + protected Bitmap setPixels(int frameIndex) { + GifFrame currentFrame = frames.get(frameIndex); + GifFrame previousFrame = null; + int previousIndex = frameIndex - 1; + if (previousIndex >= 0) { + previousFrame = frames.get(previousIndex); + } + + // final location of blended pixels + final int[] dest = mainScratch; + + // fill in starting image contents based on last image's dispose code + long disposeStartTime = System.currentTimeMillis(); + if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { + if (previousFrame.dispose == DISPOSAL_NONE && currentImage != null) { + // Start with the current image + currentImage.getPixels(dest, 0, width, 0, 0, width, height); + } + if (previousFrame.dispose == DISPOSAL_BACKGROUND) { + // Start with a canvas filled with the background color + int c = 0; + if (!currentFrame.transparency) { + c = bgColor; + } + for (int i = 0; i < previousFrame.ih; i++) { + int n1 = (previousFrame.iy + i) * width + previousFrame.ix; + int n2 = n1 + previousFrame.iw; + for (int k = n1; k < n2; k++) { + dest[k] = c; + } + } + } + } else { + int c = 0; + if (!currentFrame.transparency) { + c = bgColor; + } + for (int i = 0; i < dest.length; i++) { + dest[i] = c; + } + } + + //Decode pixels for this frame into the global pixels[] scratch + long start = System.currentTimeMillis(); + decodeBitmapData(currentFrame, mainPixels); // decode pixel data + + // copy each source line to the appropriate place in the destination + long startTime = System.currentTimeMillis(); + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < currentFrame.ih; i++) { + int line = i; + if (currentFrame.interlace) { + if (iline >= currentFrame.ih) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + break; + default: + break; + } + } + line = iline; + iline += inc; + } + line += currentFrame.iy; + if (line < height) { + int k = line * width; + int dx = k + currentFrame.ix; // start of line in dest + int dlim = dx + currentFrame.iw; // end of dest line + if ((k + width) < dlim) { + dlim = k + width; // past dest edge + } + int sx = i * currentFrame.iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = ((int) mainPixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + + //Set pixels for current image + long startSetPixelBitmap = System.currentTimeMillis(); + Bitmap result = getNextBitmap(); + result.setPixels(dest, 0, width, 0, 0, width, height); + return result; + } + + /** + * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. + */ + protected void decodeBitmapData(GifFrame frame, byte[] dstPixels) { + if (frame != null) { + //Jump to the frame start position + rawData.position(frame.bufferFrameStart); + } + + int nullCode = -1; + int npix = (frame == null) ? width * height : frame.iw * frame.ih; + int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi; + + if (dstPixels == null || dstPixels.length < npix) { + dstPixels = new byte[npix]; // allocate new pixel array + } + if (prefix == null) { + prefix = new short[MAX_STACK_SIZE]; + } + if (suffix == null) { + suffix = new byte[MAX_STACK_SIZE]; + } + if (pixelStack == null) { + pixelStack = new byte[MAX_STACK_SIZE + 1]; + } + + // Initialize GIF data stream decoder. + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = nullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + long start = System.currentTimeMillis(); + for (code = 0; code < clear; code++) { + prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException + suffix[code] = (byte) code; + } + + start = System.currentTimeMillis(); + // Decode GIF pixel stream. + datum = bits = count = first = top = pi = bi = 0; + int iterations = 0; + for (i = 0; i < npix; ) { + iterations++; + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) { + break; + } + bi = 0; + } + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + // Get the next code. + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + // Interpret the code + if ((code > available) || (code == end_of_information)) { + break; + } + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = nullCode; + continue; + } + if (old_code == nullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte) first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + // Add a new string to the string table, + if (available >= MAX_STACK_SIZE) { + break; + } + pixelStack[top++] = (byte) first; + prefix[available] = (short) old_code; + suffix[available] = (byte) first; + available++; + if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + // Pop a pixel off the pixel stack. + top--; + dstPixels[pi++] = pixelStack[top]; + i++; + } + start = System.currentTimeMillis(); + + for (i = pi; i < npix; i++) { + dstPixels[i] = 0; // clear missing pixels + } + } + + /** + * Returns true if an error was encountered during reading/decoding + */ + protected boolean err() { + return status != STATUS_OK; + } + + /** + * Initializes or re-initializes reader + */ + protected void init() { + status = STATUS_OK; + frameCount = 0; + framePointer = -1; + frames = new ArrayList(); + gct = null; + } + + /** + * Reads a single byte from the input stream. + */ + protected int read() { + int curByte = 0; + try { + curByte = (rawData.get() & 0xFF); + } catch (Exception e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + protected int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count; + while (n < blockSize) { + count = blockSize - n; + rawData.get(block, n, count); + + n += count; + } + } catch (Exception e) { + Log.w(TAG, "Error Reading Block", e); + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads color table as 256 RGB integer values + * + * @param ncolors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + protected int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + + try { + rawData.get(c); + + tab = new int[256]; // max size to avoid bounds checks + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int) c[j++]) & 0xff; + int g = ((int) c[j++]) & 0xff; + int b = ((int) c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } catch (BufferUnderflowException e) { + Log.w(TAG, "Format Error Reading Color Table", e); + status = STATUS_FORMAT_ERROR; + } + + return tab; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + protected void readContents() { + // read GIF file content blocks + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + case 0x2C: // image separator + readBitmap(); + break; + case 0x21: // extension + code = read(); + switch (code) { + case 0xf9: // graphics control extension + //Start a new frame + currentFrame = new GifFrame(); + readGraphicControlExt(); + break; + case 0xff: // application extension + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } else { + skip(); // don't care + } + break; + case 0xfe:// comment extension + skip(); + break; + case 0x01:// plain text extension + skip(); + break; + default: // uninteresting extension + skip(); + } + break; + case 0x3b: // terminator + done = true; + break; + case 0x00: // bad byte, but keep going and see what happens break; + default: + status = STATUS_FORMAT_ERROR; + } + } + } + + /** + * Reads GIF file header information. + */ + protected void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + /** + * Reads Graphics Control Extension values + */ + protected void readGraphicControlExt() { + read(); // block size + int packed = read(); // packed fields + currentFrame.dispose = (packed & 0x1c) >> 2; // disposal method + if (currentFrame.dispose == 0) { + currentFrame.dispose = 1; // elect to keep old image if discretionary + } + currentFrame.transparency = (packed & 1) != 0; + isTransparent |= currentFrame.transparency; + currentFrame.delay = readShort() * 10; // delay in milliseconds + currentFrame.transIndex = read(); // transparent color index + read(); // block terminator + } + + /** + * Reads next frame image + */ + protected void readBitmap() { + currentFrame.ix = readShort(); // (sub)image position & size + currentFrame.iy = readShort(); + currentFrame.iw = readShort(); + currentFrame.ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace + lctSize = (int) Math.pow(2, (packed & 0x07) + 1); + // 3 - sort flag + // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color + // table size + currentFrame.interlace = (packed & 0x40) != 0; + if (lctFlag) { + currentFrame.lct = readColorTable(lctSize); // read table + } else { + currentFrame.lct = null; //No local color table + } + + currentFrame.bufferFrameStart = rawData.position(); //Save this as the decoding position pointer + + decodeBitmapData(null, mainPixels); // false decode pixel data to advance buffer + skip(); + if (err()) { + return; + } + + frameCount++; + frames.add(currentFrame); // add image to frame + } + + /** + * Reads Logical Screen Descriptor + */ + protected void readLSD() { + // logical screen size + width = readShort(); + height = readShort(); + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + + //Now that we know the size, init scratch arrays + mainPixels = new byte[width * height]; + mainScratch = new int[width * height]; + } + + private Bitmap getNextBitmap() { + Bitmap.Config targetConfig = isTransparent ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + Bitmap result = bitmapPool.get(width, height, targetConfig); + if (result == null) { + result = Bitmap.createBitmap(width, height, targetConfig); + } + return result; + } + + /** + * Reads Netscape extenstion to obtain iteration count + */ + protected void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // loop count sub-block + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + /** + * Reads next 16-bit value, LSB first + */ + protected int readShort() { + // read 16-bit value + return rawData.getShort(); + } + + /** + * Skips variable length blocks up to and including next zero length block. + */ + protected void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } + +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawable.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawable.java new file mode 100644 index 000000000..694666533 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawable.java @@ -0,0 +1,113 @@ +package com.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; + +public class GifDrawable extends Drawable implements Animatable, GifFrameManager.FrameCallback { + + private final Paint paint; + private final GifFrameManager frameManager; + private GifDecoder decoder; + private boolean isRunning; + private Bitmap currentFrame; + + public GifDrawable(GifDecoder decoder, GifFrameManager frameManager) { + this.decoder = decoder; + this.frameManager = frameManager; + + paint = new Paint(); + } + + GifDecoder getDecoder() { + return decoder; + } + + @Override + public void start() { + if (!isRunning) { + isRunning = true; + frameManager.getNextFrame(decoder, this); + invalidateSelf(); + } + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + if (!visible) { + stop(); + } else { + start(); + } + return super.setVisible(visible, restart); + } + + @Override + public int getIntrinsicWidth() { + return decoder.getWidth(); + } + + @Override + public int getIntrinsicHeight() { + return decoder.getHeight(); + } + + @Override + public void stop() { + isRunning = false; + } + + @Override + public boolean isRunning() { + return isRunning; + } + + // For testing. + void setIsRunning(boolean isRunning) { + this.isRunning = isRunning; + } + + @Override + public void draw(Canvas canvas) { + if (currentFrame != null) { + canvas.drawBitmap(currentFrame, 0, 0, paint); + } + } + + @Override + public void setAlpha(int i) { + paint.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return decoder.isTransparent() ? PixelFormat.TRANSPARENT : PixelFormat.OPAQUE; + } + + @Override + public void onFrameRead(Bitmap frame) { + if (!isRunning) { + return; + } + + if (frame != null) { + currentFrame = frame; + invalidateSelf(); + } + + frameManager.getNextFrame(decoder, this); + } + + public void recycle() { + frameManager.clear(); + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java new file mode 100644 index 000000000..c92f0efbc --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java @@ -0,0 +1,43 @@ +package com.bumptech.glide.load.resource.gif; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.ModelLoader; + +public class GifFrameLoader implements ModelLoader { + private String gifId; + + //TODO: this should be in the model. + public GifFrameLoader(String gifId) { + this.gifId = gifId; + } + + @Override + public DataFetcher getResourceFetcher(GifDecoder model, int width, int height) { + return new GifFrameDataFetcher(model); + } + + @Override + public String getId(GifDecoder model) { + return gifId + model.getCurrentFrameIndex(); + } + + private static class GifFrameDataFetcher implements DataFetcher { + private GifDecoder decoder; + + public GifFrameDataFetcher(GifDecoder decoder) { + this.decoder = decoder; + } + + @Override + public GifDecoder loadData(Priority priority) throws Exception { + return decoder; + } + + @Override + public void cleanup() { } + + @Override + public void cancel() { } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameManager.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameManager.java new file mode 100644 index 000000000..d8a709ac4 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameManager.java @@ -0,0 +1,99 @@ +package com.bumptech.glide.load.resource.gif; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.engine.cache.MemorySizeCalculator; +import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; +import com.bumptech.glide.request.target.SimpleTarget; + +import java.util.UUID; + +class GifFrameManager { + static final long MIN_FRAME_DELAY = 16; + private final String id; + private final MemorySizeCalculator calculator; + private BitmapPool bitmapPool; + private final Handler mainHandler; + private Context context; + private DelayTarget current; + private DelayTarget next; + + public interface FrameCallback { + public void onFrameRead(Bitmap frame); + } + + public GifFrameManager(Context context, BitmapPool bitmapPool) { + this(context, UUID.randomUUID().toString(), bitmapPool, new Handler(Looper.getMainLooper())); + } + + public GifFrameManager(Context context, String id, BitmapPool bitmapPool, Handler mainHandler) { + this.context = context; + this.id = id; + this.bitmapPool = bitmapPool; + this.mainHandler = mainHandler; + calculator = new MemorySizeCalculator(context); + } + + public void getNextFrame(final GifDecoder decoder, FrameCallback cb) { + decoder.advance(); + boolean skipCache = decoder.getDecodedFrameByteSize() > calculator.getMemoryCacheSize() / 2; + + long targetTime = SystemClock.uptimeMillis() + (Math.min(MIN_FRAME_DELAY, decoder.getNextDelay())); + next = new DelayTarget(decoder, cb, targetTime, mainHandler); + Glide.with(context) + .using(new GifFrameLoader(id), GifDecoder.class) + .load(decoder) + .as(Bitmap.class) + .decoder(new GifFrameResourceDecoder()) + .cacheDecoder(new StreamBitmapDecoder(bitmapPool)) + .skipMemoryCache(skipCache) + .skipDiskCache(true) + .into(next); + } + + public void clear() { + if (current != null) { + Glide.clear(current); + mainHandler.removeCallbacks(current); + } + if (next != null) { + Glide.clear(next); + mainHandler.removeCallbacks(next); + } + } + + class DelayTarget extends SimpleTarget implements Runnable { + private FrameCallback cb; + private long targetTime; + private Handler mainHandler; + private Bitmap resource; + + public DelayTarget(GifDecoder decoder, FrameCallback cb, long targetTime, Handler mainHandler) { + super(decoder.getWidth(), decoder.getHeight()); + this.cb = cb; + this.targetTime = targetTime; + this.mainHandler = mainHandler; + } + + @Override + public void onResourceReady(final Bitmap resource) { + this.resource = resource; + mainHandler.postAtTime(this, targetTime); + if (current != null) { + Glide.clear(current); + } + current = next; + next = null; + } + + @Override + public void run() { + cb.onFrameRead(resource); + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java new file mode 100644 index 000000000..4d5b538a7 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java @@ -0,0 +1,20 @@ +package com.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceDecoder; + +import java.io.IOException; + +class GifFrameResourceDecoder implements ResourceDecoder { + + @Override + public Resource decode(GifDecoder source, int width, int height) throws IOException { + return source.getNextFrame(); + } + + @Override + public String getId() { + return "GifFrameResourceDecoder.com.bumptech.glide.load.resource.gif"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResource.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResource.java new file mode 100644 index 000000000..f01f4d01d --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResource.java @@ -0,0 +1,34 @@ +package com.bumptech.glide.load.resource.gif; + +import com.bumptech.glide.Resource; + +//TODO: make this safe for multiple consumers. +public class GifResource extends Resource { + private final GifDrawable drawable; + private final GifDecoder decoder; + + public GifResource(GifDecoder decoder, GifFrameManager frameManager) { + this(decoder, new GifDrawable(decoder, frameManager)); + } + + GifResource(GifDecoder gifDecoder, GifDrawable gifDrawable) { + decoder = gifDecoder; + drawable = gifDrawable; + } + + @Override + public GifDrawable get() { + return drawable; + } + + @Override + public int getSize() { + return decoder.getGifByteSize(); + } + + @Override + protected void recycleInternal() { + drawable.stop(); + drawable.recycle(); + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceDecoder.java new file mode 100644 index 000000000..8ffbb80d8 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceDecoder.java @@ -0,0 +1,36 @@ +package com.bumptech.glide.load.resource.gif; + +import android.content.Context; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + +import java.io.IOException; +import java.io.InputStream; + +public class GifResourceDecoder implements ResourceDecoder { + private Context context; + private BitmapPool bitmapPool; + + public GifResourceDecoder(Context context) { + this(context, Glide.get(context).getBitmapPool()); + } + + public GifResourceDecoder(Context context, BitmapPool bitmapPool) { + this.context = context; + this.bitmapPool = bitmapPool; + } + + @Override + public GifResource decode(InputStream source, int width, int height) throws IOException { + GifDecoder gifDecoder = new GifDecoder(bitmapPool); + gifDecoder.read(source, 0); + GifFrameManager frameManager = new GifFrameManager(context, bitmapPool); + return new GifResource(gifDecoder, frameManager); + } + + @Override + public String getId() { + return "GifResourceDecoder.com.bumptech.glide.load.gif"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceEncoder.java b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceEncoder.java new file mode 100644 index 000000000..98c94a922 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/gif/GifResourceEncoder.java @@ -0,0 +1,30 @@ +package com.bumptech.glide.load.resource.gif; + +import android.util.Log; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceEncoder; + +import java.io.IOException; +import java.io.OutputStream; + +public class GifResourceEncoder implements ResourceEncoder { + private static final String TAG = "GifEncoder"; + @Override + public boolean encode(Resource resource, OutputStream os) { + boolean result = true; + try { + os.write(resource.get().getDecoder().getData()); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to encode gif", e); + } + result = false; + } + return result; + } + + @Override + public String getId() { + return "GifEncoder.com.bumptech.glide.load.resource.gif"; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoder.java b/library/src/main/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoder.java new file mode 100644 index 000000000..32c73fd7f --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoder.java @@ -0,0 +1,40 @@ +package com.bumptech.glide.load.resource.transcode; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.resource.drawable.DrawableResource; +import com.bumptech.glide.load.resource.drawable.GifBitmap; + +public class GifBitmapDrawableTranscoder implements ResourceTranscoder { + private final Resources resources; + + public GifBitmapDrawableTranscoder(Context context) { + resources = context.getResources(); + } + + @Override + public Resource transcode(Resource toTranscode) { + GifBitmap gifBitmap = toTranscode.get(); + Resource bitmapResource = gifBitmap.getBitmapResource(); + + final Resource resource; + final Drawable drawable; + if (bitmapResource != null) { + resource = bitmapResource; + drawable = new BitmapDrawable(resources, bitmapResource.get()); + } else { + resource = gifBitmap.getGifResource(); + drawable = gifBitmap.getGifResource().get(); + } + return new DrawableResource(drawable, resource); + } + + @Override + public String getId() { + return "GifBitmapDrawableTranscoder.com.bumptech.glide.load.resource.transcode"; + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/drawable/DrawableResourceTest.java b/library/src/test/java/com/bumptech/glide/load/resource/drawable/DrawableResourceTest.java new file mode 100644 index 000000000..7814b366c --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/drawable/DrawableResourceTest.java @@ -0,0 +1,47 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import com.bumptech.glide.Resource; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class DrawableResourceTest { + private ColorDrawable drawable; + private DrawableResource resource; + private Resource wrapped; + + @Before + public void setUp() { + drawable = new ColorDrawable(Color.RED); + wrapped = mock(Resource.class); + resource = new DrawableResource(drawable, wrapped); + } + + @Test + public void testReturnsGivenSize() { + final int size = 100; + when(wrapped.getSize()).thenReturn(size); + assertEquals(size, resource.getSize()); + } + + @Test + public void testGetReturnsGivenDrawable() { + assertEquals(drawable, resource.get()); + } + + @Test + public void testRecyclesWrappedWhenRecycled() { + resource.recycleInternal(); + + verify(wrapped).recycle(); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoderTest.java new file mode 100644 index 000000000..e56039092 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceDecoderTest.java @@ -0,0 +1,87 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.os.ParcelFileDescriptor; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.model.ImageVideoWrapper; +import com.bumptech.glide.load.resource.gif.GifDrawable; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifBitmapResourceDecoderTest { + private ResourceDecoder bitmapDecoder; + private ResourceDecoder gifDecoder; + private GifBitmapResourceDecoder decoder; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + bitmapDecoder = mock(ResourceDecoder.class); + gifDecoder = mock(ResourceDecoder.class); + decoder = new GifBitmapResourceDecoder(Robolectric.application, bitmapDecoder, gifDecoder); + } + + @Test + public void testDecoderUsesGifDecoderResultIfGif() throws IOException { + GifDrawable expected = mock(GifDrawable.class); + Resource gifDrawableResource = mock(Resource.class); + when(gifDrawableResource.get()).thenReturn(expected); + when(gifDecoder.decode(any(InputStream.class), anyInt(), anyInt())).thenReturn(gifDrawableResource); + + byte[] data = new byte[] { 'G', 'I', 'F'}; + ImageVideoWrapper wrapper = new ImageVideoWrapper(new ByteArrayInputStream(data), null); + + Resource result = decoder.decode(wrapper, 100, 100); + + assertEquals(expected, result.get().getDrawable()); + } + + @Test + public void testDecoderUsesBitmapDecoderIfStreamIsNotGif() throws IOException { + Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Resource bitmapResource = mock(Resource.class); + when(bitmapResource.get()).thenReturn(expected); + when(bitmapDecoder.decode(any(ImageVideoWrapper.class), anyInt(), anyInt())).thenReturn(bitmapResource); + + byte[] data = new byte[] { 'A', 'I', 'F'}; + ImageVideoWrapper wrapper = new ImageVideoWrapper(new ByteArrayInputStream(data), null); + + Resource result = decoder.decode(wrapper, 100, 100); + + BitmapDrawable bitmapDrawable = (BitmapDrawable) result.get().getDrawable(); + + assertEquals(expected, bitmapDrawable.getBitmap()); + } + + @Test + public void testDecoderUsesBitmapDecoderIfIsFileDescriptor() throws IOException { + Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Resource bitmapResource = mock(Resource.class); + when(bitmapResource.get()).thenReturn(expected); + when(bitmapDecoder.decode(any(ImageVideoWrapper.class), anyInt(), anyInt())).thenReturn(bitmapResource); + + ImageVideoWrapper wrapper = new ImageVideoWrapper(null, mock(ParcelFileDescriptor.class)); + + Resource result = decoder.decode(wrapper, 100, 100); + + BitmapDrawable bitmapDrawable = (BitmapDrawable) result.get().getDrawable(); + + assertEquals(expected, bitmapDrawable.getBitmap()); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoderTest.java new file mode 100644 index 000000000..dae3864b3 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapResourceEncoderTest.java @@ -0,0 +1,93 @@ +package com.bumptech.glide.load.resource.drawable; + +import android.graphics.Bitmap; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceEncoder; +import com.bumptech.glide.load.resource.gif.GifDrawable; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifBitmapResourceEncoderTest { + private ResourceEncoder bitmapEncoder; + private ResourceEncoder gifEncoder; + private GifBitmapResourceEncoder encoder; + private Resource resource; + private GifBitmap gifBitmap; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + bitmapEncoder = mock(ResourceEncoder.class); + gifEncoder = mock(ResourceEncoder.class); + encoder = new GifBitmapResourceEncoder(bitmapEncoder, gifEncoder); + resource = mock(Resource.class); + gifBitmap = mock(GifBitmap.class); + when(resource.get()).thenReturn(gifBitmap); + } + + @Test + public void testEncodesWithBitmapEncoderIfHasBitmapResource() { + Resource bitmapResource = mock(Resource.class); + when(gifBitmap.getBitmapResource()).thenReturn(bitmapResource); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + encoder.encode(resource, os); + + verify(bitmapEncoder).encode(eq(bitmapResource), eq(os)); + } + + @Test + public void testReturnsBitmapEncoderResultIfHasBitmapResource() { + Resource bitmapResource = mock(Resource.class); + when(gifBitmap.getBitmapResource()).thenReturn(bitmapResource); + + when(bitmapEncoder.encode(any(Resource.class), any(OutputStream.class))).thenReturn(true); + assertTrue(encoder.encode(resource, new ByteArrayOutputStream())); + + when(bitmapEncoder.encode(any(Resource.class), any(OutputStream.class))).thenReturn(false); + assertFalse(encoder.encode(resource, new ByteArrayOutputStream())); + } + + @Test + public void testEncodesWithGifEncoderIfHasGif() { + Resource gifResource = mock(Resource.class); + when(gifBitmap.getGifResource()).thenReturn(gifResource); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + encoder.encode(resource, os); + + verify(gifEncoder).encode(eq(gifResource), eq(os)); + } + + @Test + public void testReturnsGifEncoderResultIfHasGifResource() { + Resource gifResource = mock(Resource.class); + when(gifBitmap.getGifResource()).thenReturn(gifResource); + + when(gifEncoder.encode(any(Resource.class), any(OutputStream.class))).thenReturn(true); + assertTrue(encoder.encode(resource, new ByteArrayOutputStream())); + + when(gifEncoder.encode(any(Resource.class), any(OutputStream.class))).thenReturn(false); + assertFalse(encoder.encode(resource, new ByteArrayOutputStream())); + } + + @Test + public void testReturnsNonNullId() { + assertNotNull(encoder.getId()); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoderTest.java new file mode 100644 index 000000000..754c8847c --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/drawable/GifBitmapStreamResourceDecoderTest.java @@ -0,0 +1,63 @@ +package com.bumptech.glide.load.resource.drawable; + +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.model.ImageVideoWrapper; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GifBitmapStreamResourceDecoderTest { + ResourceDecoder gifBitmapDecoder; + private GifBitmapStreamResourceDecoder decoder; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + gifBitmapDecoder = mock(ResourceDecoder.class); + decoder = new GifBitmapStreamResourceDecoder(gifBitmapDecoder); + } + + @Test + public void testReturnsWrappedDecoderResult() throws IOException { + int width = 100; + int height = 110; + Resource expected = mock(Resource.class); + when(gifBitmapDecoder.decode(any(ImageVideoWrapper.class), eq(width), eq(height))).thenReturn(expected); + + assertEquals(expected, decoder.decode(new ByteArrayInputStream(new byte[0]), width, height)); + } + + @Test + public void testPassesGivenInputStreamWrappedAsImageVideoWrapper() throws IOException { + final InputStream expected = new ByteArrayInputStream(new byte[0]); + when(gifBitmapDecoder.decode(any(ImageVideoWrapper.class), anyInt(), anyInt())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ImageVideoWrapper wrapper = (ImageVideoWrapper) invocation.getArguments()[0]; + assertEquals(expected, wrapper.getStream()); + return null; + } + }); + + decoder.decode(expected, 1, 2); + } + + @Test + public void testReturnsNonNullId() { + assertNotNull(decoder.getId()); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java new file mode 100644 index 000000000..12927a986 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java @@ -0,0 +1,166 @@ +package com.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifDrawableTest { + private GifDecoder gifDecoder; + private GifDrawable drawable; + private GifFrameManager frameManager = mock(GifFrameManager.class); + private Drawable.Callback cb = mock(Drawable.Callback.class); + + @Before + public void setUp() { + gifDecoder = mock(GifDecoder.class); + drawable = new GifDrawable(gifDecoder, frameManager); + drawable.setCallback(cb); + } + + @Test + public void testReturnsDecoderWidth() { + int width = 123; + when(gifDecoder.getWidth()).thenReturn(width); + + assertEquals(width, drawable.getIntrinsicWidth()); + } + + @Test + public void testReturnsDecoderHeight() { + int height = 321; + when(gifDecoder.getHeight()).thenReturn(height); + + assertEquals(height, drawable.getIntrinsicHeight()); + } + + @Test + public void testShouldNotDrawNullBitmap() { + Canvas canvas = mock(Canvas.class); + drawable.draw(canvas); + + verify(canvas, never()).drawBitmap((Bitmap) isNull(), anyInt(), anyInt(), any(Paint.class)); + } + + @Test + public void testRequestsNextFrameOnStart() { + drawable.start(); + + verify(frameManager).getNextFrame(eq(gifDecoder), eq(drawable)); + } + + @Test + public void testShouldInvalidateSelfOnRun() { + drawable.start(); + + verify(cb).invalidateDrawable(eq(drawable)); + } + + @Test + public void testShouldNotScheduleItselfIfAlreadyRunning() { + drawable.start(); + drawable.start(); + + verify(frameManager, times(1)).getNextFrame(eq(gifDecoder), eq(drawable)); + } + + @Test + public void testReturnsFalseFromIsRunningWhenNotRunning() { + assertFalse(drawable.isRunning()); + } + + @Test + public void testReturnsTrueFromIsRunningWhenRunning() { + drawable.start(); + + assertTrue(drawable.isRunning()); + } + + @Test + public void testStartsLoadingNextFrameWhenCurrentFinishes() { + drawable.setIsRunning(true); + drawable.onFrameRead(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + + verify(frameManager).getNextFrame(eq(gifDecoder), eq(drawable)); + } + + @Test + public void testInvalidatesSelfWhenFrameReady() { + drawable.setIsRunning(true); + drawable.onFrameRead(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)); + + verify(cb).invalidateDrawable(eq(drawable)); + } + + @Test + public void testDoesNotStartLoadingNextFrameWhenCurrentFinishesIfNotRunn() { + drawable.setIsRunning(false); + drawable.onFrameRead(Bitmap.createBitmap(10, 100, Bitmap.Config.ARGB_8888)); + + verify(frameManager, never()).getNextFrame(eq(gifDecoder), eq(drawable)); + } + + @Test + public void testSetsIsRunningFalseOnStop() { + drawable.start(); + drawable.stop(); + + assertFalse(drawable.isRunning()); + } + + @Test + public void testStopsOnSetVisibleFalse() { + drawable.start(); + + drawable.setVisible(false, true); + + assertFalse(drawable.isRunning()); + } + + @Test + public void testStartsOnSetVisibleTrue() { + drawable.setVisible(true, true); + + assertTrue(drawable.isRunning()); + } + + @Test + public void testGetOpacityReturnsTransparentfDecoderHasTransparency() { + when(gifDecoder.isTransparent()).thenReturn(true); + + assertEquals(PixelFormat.TRANSPARENT, drawable.getOpacity()); + } + + @Test + public void testGetOpacityReturnsOpaqueIfDecoderDoesNotHaveTransparency() { + when(gifDecoder.isTransparent()).thenReturn(false); + + assertEquals(PixelFormat.OPAQUE, drawable.getOpacity()); + } + + @Test + public void testRecycleCallsClearOnFrameManager() { + drawable.recycle(); + + verify(frameManager).clear(); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoderTest.java new file mode 100644 index 000000000..f6d6e53b8 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoderTest.java @@ -0,0 +1,40 @@ +package com.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; +import com.bumptech.glide.Resource; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifFrameResourceDecoderTest { + private GifDecoder gifDecoder; + private GifFrameResourceDecoder resourceDecoder; + + @Before + public void setUp() { + gifDecoder = mock(GifDecoder.class); + resourceDecoder = new GifFrameResourceDecoder(); + } + + @Test + public void testReturnsNonNullId() { + assertNotNull(resourceDecoder.getId()); + } + + @Test + public void testReturnsFrameFromGifDecoder() throws IOException { + Resource resource = mock(Resource.class); + when(gifDecoder.getNextFrame()).thenReturn(resource); + + assertEquals(resource, resourceDecoder.decode(gifDecoder, 100, 100)); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceEncoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceEncoderTest.java new file mode 100644 index 000000000..36a053ad4 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceEncoderTest.java @@ -0,0 +1,64 @@ +package com.bumptech.glide.load.resource.gif; + +import com.bumptech.glide.Resource; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifResourceEncoderTest { + private Resource resource; + private byte[] data; + private GifResourceEncoder encoder; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + resource = mock(Resource.class); + data = new byte[]{ 2, 3, 5, 8 }; + GifDecoder decoder = mock(GifDecoder.class); + GifDrawable drawable = mock(GifDrawable.class); + when(drawable.getDecoder()).thenReturn(decoder); + when(resource.get()).thenReturn(drawable); + when(decoder.getData()).thenReturn(data); + encoder = new GifResourceEncoder(); + } + + @Test + public void testWritesDataToOutputStream() { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + encoder.encode(resource, os); + + assertTrue(Arrays.equals(data, os.toByteArray())); + } + + @Test + public void testReturnsTrueIfWriteCompletes() { + assertTrue(encoder.encode(resource, new ByteArrayOutputStream())); + } + + @Test + public void testReturnsFalseIfWriteFails() { + OutputStream os = new ByteArrayOutputStream() { + @Override + public void write(byte[] buffer) throws IOException { + super.write(buffer); //To change body of overridden methods use File | Settings | File Templates. + throw new IOException("Test"); + } + }; + + assertFalse(encoder.encode(resource, os)); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceTest.java b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceTest.java new file mode 100644 index 000000000..4225707ce --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/gif/GifResourceTest.java @@ -0,0 +1,53 @@ +package com.bumptech.glide.load.resource.gif; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifResourceTest { + private GifDecoder decoder; + private GifDrawable gifDrawable; + private GifResource resource; + + @Before + public void setUp() { + decoder = mock(GifDecoder.class); + gifDrawable = mock(GifDrawable.class); + resource = new GifResource(decoder, gifDrawable); + } + + @Test + public void testSizeReturnsGifDecoderGifByteSize() { + int size = 1234; + when(decoder.getGifByteSize()).thenReturn(size); + + assertEquals(size, resource.getSize()); + } + + @Test + public void testReturnsNonNullDrawable() { + assertNotNull(resource.get()); + } + + @Test + public void testStopsGifDrawableOnRecycle() { + resource.recycle(); + + verify(gifDrawable).stop(); + } + + @Test + public void testRecyclesGifDrawableOnRecycle() { + resource.recycle(); + + verify(gifDrawable).recycle(); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoderTest.java new file mode 100644 index 000000000..fd93d7acc --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/transcode/GifBitmapDrawableTranscoderTest.java @@ -0,0 +1,104 @@ +package com.bumptech.glide.load.resource.transcode; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import com.bumptech.glide.Resource; +import com.bumptech.glide.load.resource.drawable.GifBitmap; +import com.bumptech.glide.load.resource.gif.GifDrawable; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class GifBitmapDrawableTranscoderTest { + private GifBitmapDrawableTranscoder transcoder; + + @Before + public void setUp() { + transcoder = new GifBitmapDrawableTranscoder(Robolectric.application); + } + + @Test + public void testReturnsBitmapDrawableIfGifBitmapHasBitmap() { + GifBitmapWithBitmapHarness harness = new GifBitmapWithBitmapHarness(); + + BitmapDrawable transcoded = (BitmapDrawable) transcoder.transcode(harness.gifBitmapResource).get(); + + assertEquals(harness.expected, transcoded.getBitmap()); + } + + @Test + public void testReturnedResourceHasBitmapSizeIfGifBitmapHasBitmap() { + final int size = 100; + GifBitmapWithBitmapHarness harness = new GifBitmapWithBitmapHarness(); + when(harness.bitmapResource.getSize()).thenReturn(size); + + Resource transcoded = transcoder.transcode(harness.gifBitmapResource); + + assertEquals(size, transcoded.getSize()); + } + + @Test + public void testReturnsGifDrawableIfGifBitmapHasGif() { + GifBitmapWithGifHarness harness = new GifBitmapWithGifHarness(); + + Drawable transcoded = transcoder.transcode(harness.gifBitmapResource).get(); + + assertEquals(harness.expected, transcoded); + } + + @Test + public void testReturnedResourceHasGifDrawableSizeIfGifBitmapHasGif() { + final int size = 200; + GifBitmapWithGifHarness harness = new GifBitmapWithGifHarness(); + when(harness.gifResource.getSize()).thenReturn(size); + + Resource transcoded = transcoder.transcode(harness.gifBitmapResource); + + assertEquals(size, transcoded.getSize()); + } + + @Test + public void testHasNonNullId() { + assertNotNull(transcoder.getId()); + } + + private static class TranscoderHarness { + Resource gifBitmapResource = mock(Resource.class); + GifBitmap gifBitmap = mock(GifBitmap.class); + + public TranscoderHarness() { + when(gifBitmapResource.get()).thenReturn(gifBitmap); + } + } + + private static class GifBitmapWithBitmapHarness extends TranscoderHarness { + Bitmap expected = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); + Resource bitmapResource = mock(Resource.class); + + public GifBitmapWithBitmapHarness() { + super(); + when(bitmapResource.get()).thenReturn(expected); + when(gifBitmap.getBitmapResource()).thenReturn(bitmapResource); + } + } + + private static class GifBitmapWithGifHarness extends TranscoderHarness { + GifDrawable expected = mock(GifDrawable.class); + Resource gifResource = mock(Resource.class); + + public GifBitmapWithGifHarness() { + super(); + when(gifResource.get()).thenReturn(expected); + when(gifBitmap.getGifResource()).thenReturn(gifResource); + } + } +} diff --git a/samples/flickr/res/layout/flickr_search_activity.xml b/samples/flickr/res/layout/flickr_search_activity.xml index fe172fddc..85b241446 100644 --- a/samples/flickr/res/layout/flickr_search_activity.xml +++ b/samples/flickr/res/layout/flickr_search_activity.xml @@ -4,6 +4,11 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> +