Animated GIFs is very popular on Web, but in Android, it become a difficult topic. Original Android SDK don't provide the way to direct display them. So, developers we find ways to use. In this post, I would like to present 3 solutions to resolve this problem. Each way has own advantages and disadvantages, through this, I hope that Android developers can find the suitable solution for your own project.
Using android.graphics.Movie
@Override protected void onDraw(Canvas canvas) { long start = android.os.SystemClock.uptimeMillis(); if (movieStart == 0) { movieStart = start; } if (gifMovie != null) { int dur = gifMovie.duration(); if (dur == 0) { dur = 1000; } int relTime = (int)((start - movieStart) % dur); gifMovie.setTime(relTime); gifMovie.draw(canvas, 0, 0); invalidate(); } }Providing a Constructor and override onMeasure(), we have full code for this custom view:
package info.devexchanges.animatedgifs; import android.content.Context; import android.graphics.Canvas; import android.graphics.Movie; import android.util.AttributeSet; import android.view.View; import java.io.InputStream; public class GifView extends View { private InputStream gifInputStream; private Movie gifMovie; private int movieWidth, movieHeight; private long movieDuration; private long movieStart; public GifView(Context context) { super(context); init(context); } public GifView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public GifView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @SuppressWarnings("ResourceType") private void init(Context context){ setFocusable(true); gifInputStream = context.getResources().openRawResource(R.mipmap.sample_gif); gifMovie = Movie.decodeStream(gifInputStream); movieWidth = gifMovie.width(); movieHeight = gifMovie.height(); movieDuration = gifMovie.duration(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(movieWidth, movieHeight); } public int getMovieWidth(){ return movieWidth; } public int getMovieHeight(){ return movieHeight; } public long getMovieDuration(){ return movieDuration; } @Override protected void onDraw(Canvas canvas) { long start = android.os.SystemClock.uptimeMillis(); if (movieStart == 0) { movieStart = start; } if (gifMovie != null) { int dur = gifMovie.duration(); if (dur == 0) { dur = 1000; } int relTime = (int)((start - movieStart) % dur); gifMovie.setTime(relTime); gifMovie.draw(canvas, 0, 0); invalidate(); } } }To use this view, in xml layout, declaring a GifView object like this:
There is nothing to do in activity/fragment programmatically code except locate this view:
gifView = (GifView) findViewById(R.id.gifview);
Using WebView
Firstly, put a WebView object in your xml file:
In programmatically code, invoke loadUrl(String url) method, WebView will auto load our gif image, setting some WebView properties for better output:
webView.loadUrl("file:///android_asset/sample_gif_2.gif"); webView.getSettings().setLoadWithOverviewMode(true); webView.getSettings().setUseWideViewPort(true);Note: WebView cannot load gif from drawable resource, It can only load from URL on Internet or from Assets. So if you use this, please put your image into src/main/assets folder first and set url string like above code.
Using ImageView
package info.devexchanges.animatedgifs; import java.io.InputStream; import java.util.Vector; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; public class GifDecoder { /** * 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; protected InputStream in; protected int status; 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[] lct; // local color table protected int[] act; // active color table protected int bgIndex; // background color index protected int bgColor; // background color protected int lastBgColor; // previous bg color protected int pixelAspect; // pixel aspect ratio protected boolean lctFlag; // local color table flag protected boolean interlace; // interlace flag protected int lctSize; // local color table size protected int ix, iy, iw, ih; // current image rectangle protected int lrx, lry, lrw, lrh; protected Bitmap image; // current frame protected Bitmap lastBitmap; // previous frame protected byte[] block = new byte[256]; // current data block protected int blockSize = 0; // block size last graphic control extension info protected int dispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev protected int lastDispose = 0; protected boolean transparency = false; // use transparent color protected int delay = 0; // delay in milliseconds protected int transIndex; // transparent color index // LZW decoder working arrays protected short[] prefix; protected byte[] suffix; protected byte[] pixelStack; protected byte[] pixels; protected Vector<GifFrame> frames; // frames read from current file protected int frameCount; private static class GifFrame { public GifFrame(Bitmap im, int del) { image = im; delay = del; } public Bitmap image; public int delay; } /** * Gets display duration for specified frame. * * @param n int index of frame * @return delay in milliseconds */ public int getDelay(int n) { delay = -1; if ((n >= 0) && (n < frameCount)) { delay = frames.elementAt(n).delay; } return delay; } /** * Gets the number of frames read from file. * * @return frame count */ public int getFrameCount() { return frameCount; } /** * Gets the first (or only) image read. * * @return BufferedBitmap containing first frame, or null if none. */ public Bitmap getBitmap() { return getFrame(0); } /** * 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; } /** * Creates new frame image from current data (and previous frames as specified by their disposition codes). */ protected void setPixels() { // expose destination image's pixels as int array int[] dest = new int[width * height]; // fill in starting image contents based on last image's dispose code if (lastDispose > 0) { if (lastDispose == 3) { // use image before last int n = frameCount - 2; if (n > 0) { lastBitmap = getFrame(n - 1); } else { lastBitmap = null; } } if (lastBitmap != null) { lastBitmap.getPixels(dest, 0, width, 0, 0, width, height); // copy pixels if (lastDispose == 2) { // fill last image rect area with background color int c = 0; if (!transparency) { c = lastBgColor; } for (int i = 0; i < lrh; i++) { int n1 = (lry + i) * width + lrx; int n2 = n1 + lrw; for (int k = n1; k < n2; k++) { dest[k] = c; } } } } } // copy each source line to the appropriate place in the destination int pass = 1; int inc = 8; int iline = 0; for (int i = 0; i < ih; i++) { int line = i; if (interlace) { if (iline >= 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 += iy; if (line < height) { int k = line * width; int dx = k + ix; // start of line in dest int dlim = dx + iw; // end of dest line if ((k + width) < dlim) { dlim = k + width; // past dest edge } int sx = i * iw; // start of line in source while (dx < dlim) { // map color and insert in destination int index = ((int) pixels[sx++]) & 0xff; int c = act[index]; if (c != 0) { dest[dx] = c; } dx++; } } } image = Bitmap.createBitmap(dest, width, height, Config.ARGB_4444); } /** * Gets the image contents of frame n. * * @return BufferedBitmap representation of frame, or null if n is invalid. */ public Bitmap getFrame(int n) { if (frameCount <= 0) return null; n = n % frameCount; return ((GifFrame) frames.elementAt(n)).image; } /** * Reads GIF image from stream * * @param is containing GIF file. * @return read status code (0 = no errors) */ public int read(InputStream is) { init(); if (is != null) { in = is; readHeader(); if (!err()) { readContents(); if (frameCount < 0) { status = STATUS_FORMAT_ERROR; } } } else { status = STATUS_OPEN_ERROR; } try { is.close(); } catch (Exception e) { } return status; } /** * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. */ protected void decodeBitmapData() { int nullCode = -1; int npix = iw * 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 ((pixels == null) || (pixels.length < npix)) { pixels = 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; for (code = 0; code < clear; code++) { prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException suffix[code] = (byte) code; } // Decode GIF pixel stream. datum = bits = count = first = top = pi = bi = 0; for (i = 0; i < npix; ) { 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--; pixels[pi++] = pixelStack[top]; i++; } for (i = pi; i < npix; i++) { pixels[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; frames = new Vector<GifFrame>(); gct = null; lct = null; } /** * Reads a single byte from the input stream. */ protected int read() { int curByte = 0; try { curByte = in.read(); } 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 = 0; while (n < blockSize) { count = in.read(block, n, blockSize - n); if (count == -1) { break; } n += count; } } catch (Exception e) { e.printStackTrace(); } if (n < blockSize) { 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]; int n = 0; try { n = in.read(c); } catch (Exception e) { e.printStackTrace(); } if (n < nbytes) { status = STATUS_FORMAT_ERROR; } else { 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; } } 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 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 Graphics Control Extension values */ protected void readGraphicControlExt() { read(); // block size int packed = read(); // packed fields dispose = (packed & 0x1c) >> 2; // disposal method if (dispose == 0) { dispose = 1; // elect to keep old image if discretionary } transparency = (packed & 1) != 0; delay = readShort() * 10; // delay in milliseconds transIndex = read(); // transparent color index read(); // block terminator } /** * 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 next frame image */ protected void readBitmap() { ix = readShort(); // (sub)image position & size iy = readShort(); iw = readShort(); 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 interlace = (packed & 0x40) != 0; if (lctFlag) { lct = readColorTable(lctSize); // read table act = lct; // make local table active } else { act = gct; // make global table active if (bgIndex == transIndex) { bgColor = 0; } } int save = 0; if (transparency) { save = act[transIndex]; act[transIndex] = 0; // set transparent color if specified } if (act == null) { status = STATUS_FORMAT_ERROR; // no color table defined } if (err()) { return; } decodeBitmapData(); // decode pixel data skip(); if (err()) { return; } frameCount++; // create new image to receive frame data image = Bitmap.createBitmap(width, height, Config.ARGB_4444); setPixels(); // transfer pixel data to image frames.addElement(new GifFrame(image, delay)); // add image to frame // list if (transparency) { act[transIndex] = save; } resetFrame(); } /** * 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 } /** * 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, LSB first return read() | (read() << 8); } /** * Resets frame state for reading next image. */ protected void resetFrame() { lastDispose = dispose; lrx = ix; lry = iy; lrw = iw; lrh = ih; lastBitmap = image; lastBgColor = bgColor; dispose = 0; transparency = false; delay = 0; lct = null; } /** * Skips variable length blocks up to and including next zero length block. */ protected void skip() { do { readBlock(); } while ((blockSize > 0) && !err()); } }In our subclass which we create now will use GifDecoder class, "joining" all decoded frames to a Bitmap object and store them. This process was doing in background thread by a Handler to post our updates to the UI-thread and a Runnable that will arrange that the Bitmap we just defined will be drawn using the Handler. Here is full code for this view:
package info.devexchanges.animatedgifs; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.widget.ImageView; import java.io.InputStream; public class GifDecoderView extends ImageView { private boolean mIsPlayingGif = false; private GifDecoder mGifDecoder; private Bitmap mTmpBitmap; final Handler mHandler = new Handler(); final Runnable mUpdateResults = new Runnable() { public void run() { if (mTmpBitmap != null && !mTmpBitmap.isRecycled()) { GifDecoderView.this.setImageBitmap(mTmpBitmap); } } }; public GifDecoderView(Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); } public void playGif(InputStream stream) { mGifDecoder = new GifDecoder(); mGifDecoder.read(stream); mIsPlayingGif = true; new Thread(new Runnable() { public void run() { final int n = mGifDecoder.getFrameCount(); final int ntimes = mGifDecoder.getLoopCount(); int repetitionCounter = 0; do { for (int i = 0; i < n; i++) { mTmpBitmap = mGifDecoder.getFrame(i); int t = mGifDecoder.getDelay(i); mHandler.post(mUpdateResults); try { Thread.sleep(t); } catch (InterruptedException e) { e.printStackTrace(); } } if (ntimes != 0) { repetitionCounter++; } } while (mIsPlayingGif && (repetitionCounter <= ntimes)); } }).start(); } }In order to use this view, activity layout must include a GifDecoderView object like this:
Get an InputStream object from Assets and display "animated Bitmap" in code:
InputStream stream = null; try { stream = getAssets().open("sample_gif_3.gif"); } catch (IOException e) { e.printStackTrace(); } gifDecoderView.playGif(stream);
Full Sample project code
- Main activity programmatically code:
package info.devexchanges.animatedgifs; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.webkit.WebView; import android.widget.TextView; import java.io.IOException; import java.io.InputStream; public class MainActivity extends AppCompatActivity { private TextView textViewInfo; private GifView gifView; private GifDecoderView gifDecoderView; private WebView webView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gifView = (GifView) findViewById(R.id.gifview); webView = (WebView) findViewById(R.id.web_view); gifDecoderView = (GifDecoderView) findViewById(R.id.gif_decoder_view); webView.loadUrl("file:///android_asset/sample_gif_2.gif"); webView.getSettings().setLoadWithOverviewMode(true); webView.getSettings().setUseWideViewPort(true); playGifByDecoderView(); } private void playGifByDecoderView() { InputStream stream = null; try { stream = getAssets().open("sample_gif_3.gif"); } catch (IOException e) { e.printStackTrace(); } gifDecoderView.playGif(stream); } }- Activity layout (xml) file:
- Strings resource:
After running app, we have this result:
Conclusions
- The Movie-class is not able to deal with every type of animated GIFs. For some formats, the first frame will be drawn well but every other won't.
- The WebView is easy to use but it load content slowest and it's hard to align GIF/content and set some other properties according to ourselves mind.
- The GifDecoder way will consuming memory. Bitmaps on pre-Honeycomb-devices are stored in an native heap, not in the heap the Dalvik VM uses. Still, this heap counts to the maximum of memory your app can use. So when you have a lot of Bitmaps, the garbage collector might not notice that you are running out of memory because it only controls the heap of the Dalvik VM.
Personally, I prefered Movie class. It's simple and does not affect the memory. But, which way you take depends on your needs. I hope through this post, you can find a suitable way for your own case. As usual, see my full project on @Github by click below button.