| Index: third_party/gif_player/src/jp/tomorrowkey/android/gifplayer/BaseGifDrawable.java
|
| diff --git a/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer/BaseGifDrawable.java b/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer/BaseGifDrawable.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..02112507997c4d23ca687dfd465e05a0e25aa1aa
|
| --- /dev/null
|
| +++ b/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer/BaseGifDrawable.java
|
| @@ -0,0 +1,959 @@
|
| +/*
|
| + * Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
|
| + *
|
| + * Licensed under the Apache License, Version 2.0 (the "License");
|
| + * you may not use this file except in compliance with the License.
|
| + * You may obtain a copy of the License at
|
| + *
|
| + * http://www.apache.org/licenses/LICENSE-2.0
|
| + *
|
| + * Unless required by applicable law or agreed to in writing, software
|
| + * distributed under the License is distributed on an "AS IS" BASIS,
|
| + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| + * See the License for the specific language governing permissions and
|
| + * limitations under the License.
|
| + */
|
| +
|
| +package jp.tomorrowkey.android.gifplayer;
|
| +
|
| +import android.graphics.Bitmap;
|
| +import android.graphics.Canvas;
|
| +import android.graphics.ColorFilter;
|
| +import android.graphics.Paint;
|
| +import android.graphics.PixelFormat;
|
| +import android.graphics.Rect;
|
| +import android.graphics.drawable.Animatable;
|
| +import android.graphics.drawable.Drawable;
|
| +import android.os.Handler;
|
| +import android.os.HandlerThread;
|
| +import android.os.Looper;
|
| +import android.os.Message;
|
| +import android.os.SystemClock;
|
| +import android.util.Log;
|
| +
|
| +/**
|
| + * A base GIF Drawable with support for animations.
|
| + *
|
| + * Inspired by http://code.google.com/p/android-gifview/
|
| + */
|
| +public class BaseGifDrawable extends Drawable implements Runnable, Animatable,
|
| + android.os.Handler.Callback {
|
| +
|
| + private static final String TAG = "GifDrawable";
|
| +
|
| + // Max decoder pixel stack size
|
| + private static final int MAX_STACK_SIZE = 4096;
|
| + private static final int MAX_BITS = 4097;
|
| +
|
| + // Frame disposal methods
|
| + private static final int DISPOSAL_METHOD_UNKNOWN = 0;
|
| + private static final int DISPOSAL_METHOD_LEAVE = 1;
|
| + private static final int DISPOSAL_METHOD_BACKGROUND = 2;
|
| + private static final int DISPOSAL_METHOD_RESTORE = 3;
|
| +
|
| + // Message types
|
| + private static final int READ_FRAME_REQ = 10;
|
| + private static final int READ_FRAME_RESP = 11;
|
| + private static final int RESET_DECODER = 12;
|
| +
|
| + // Specifies the minimum amount of time before a subsequent frame will be rendered.
|
| + private static final int MIN_FRAME_SCHEDULE_DELAY_MS = 5;
|
| +
|
| + private static final byte[] NETSCAPE2_0 = "NETSCAPE2.0".getBytes();
|
| +
|
| + private static Paint sPaint;
|
| + private static Paint sScalePaint;
|
| +
|
| + protected final BaseGifImage mGifImage;
|
| + private final byte[] mData;
|
| +
|
| + private int mPosition;
|
| + protected int mIntrinsicWidth;
|
| + protected int mIntrinsicHeight;
|
| +
|
| + private int mWidth;
|
| + private int mHeight;
|
| +
|
| + protected Bitmap mBitmap;
|
| + protected int[] mColors;
|
| + private boolean mScale;
|
| + private float mScaleFactor;
|
| +
|
| + // The following are marked volatile because they are read/written in the background decoder
|
| + // thread and read from the UI thread. No further synchronization is needed because their
|
| + // values will only ever change from at most once, and it is safe to lazily detect the change
|
| + // in the UI thread.
|
| + private volatile boolean mError;
|
| + private volatile boolean mDone;
|
| + private volatile boolean mAnimateOnLoad = true;
|
| +
|
| + private int mBackgroundColor;
|
| + private boolean mLocalColorTableUsed;
|
| + private int mLocalColorTableSize;
|
| + private int[] mLocalColorTable;
|
| + private int[] mActiveColorTable;
|
| + private boolean mInterlace;
|
| +
|
| + // Each frame specifies a sub-region of the image that should be updated. The values are
|
| + // clamped to the GIF dimensions if they exceed the intrinsic dimensions.
|
| + private int mFrameX, mFrameY, mFrameWidth, mFrameHeight;
|
| +
|
| + // This specifies the width of the actual data within a GIF frame. It will be equal to
|
| + // mFrameWidth unless the frame sub-region was clamped to prevent exceeding the intrinsic
|
| + // dimensions.
|
| + private int mFrameStep;
|
| +
|
| + private byte[] mBlock = new byte[256];
|
| + private int mDisposalMethod = DISPOSAL_METHOD_BACKGROUND;
|
| + private boolean mTransparency;
|
| + private int mTransparentColorIndex;
|
| +
|
| + // LZW decoder working arrays
|
| + private short[] mPrefix = new short[MAX_STACK_SIZE];
|
| + private byte[] mSuffix = new byte[MAX_STACK_SIZE];
|
| + private byte[] mPixelStack = new byte[MAX_STACK_SIZE + 1];
|
| + private byte[] mPixels;
|
| +
|
| + private boolean mBackupSaved;
|
| + private int[] mBackup;
|
| +
|
| + private int mFrameCount;
|
| +
|
| + private long mLastFrameTime;
|
| +
|
| + private boolean mRunning;
|
| + protected int mFrameDelay;
|
| + private int mNextFrameDelay;
|
| + protected boolean mScheduled;
|
| + private boolean mAnimationEnabled = true;
|
| + private final Handler mHandler = new Handler(Looper.getMainLooper(), this);
|
| + private static DecoderThread sDecoderThread;
|
| + private static Handler sDecoderHandler;
|
| +
|
| + private boolean mRecycled;
|
| + protected boolean mFirstFrameReady;
|
| + private boolean mEndOfFile;
|
| + private int mLoopCount = 0; // 0 to repeat endlessly.
|
| + private int mLoopIndex = 0;
|
| +
|
| + private final Bitmap.Config mBitmapConfig;
|
| + private boolean mFirstFrame = true;
|
| +
|
| + public BaseGifDrawable(BaseGifImage gifImage, Bitmap.Config bitmapConfig) {
|
| + this.mBitmapConfig = bitmapConfig;
|
| +
|
| + // Create the background decoder thread, if necessary.
|
| + if (sDecoderThread == null) {
|
| + sDecoderThread = new DecoderThread();
|
| + sDecoderThread.start();
|
| + sDecoderHandler = new Handler(sDecoderThread.getLooper(), sDecoderThread);
|
| + }
|
| +
|
| + if (sPaint == null) {
|
| + sPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
| + sScalePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
| + sScalePaint.setFilterBitmap(true);
|
| + }
|
| +
|
| + mGifImage = gifImage;
|
| + mData = gifImage.getData();
|
| + mPosition = mGifImage.mHeaderSize;
|
| + mFrameWidth = mFrameStep = mIntrinsicWidth = gifImage.getWidth();
|
| + mFrameHeight = mIntrinsicHeight = gifImage.getHeight();
|
| + mBackgroundColor = mGifImage.mBackgroundColor;
|
| + mError = mGifImage.mError;
|
| +
|
| + if (!mError) {
|
| + try {
|
| + mBitmap = Bitmap.createBitmap(mIntrinsicWidth, mIntrinsicHeight, mBitmapConfig);
|
| + if (mBitmap == null) {
|
| + throw new OutOfMemoryError("Cannot allocate bitmap");
|
| + }
|
| +
|
| + int pixelCount = mIntrinsicWidth * mIntrinsicHeight;
|
| + mColors = new int[pixelCount];
|
| + mPixels = new byte[pixelCount];
|
| +
|
| + mWidth = mIntrinsicHeight;
|
| + mHeight = mIntrinsicHeight;
|
| +
|
| + // Read the first frame
|
| + sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(READ_FRAME_REQ, this));
|
| + } catch (OutOfMemoryError e) {
|
| + mError = true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Sets the loop count for multi-frame animation.
|
| + */
|
| + public void setLoopCount(int loopCount) {
|
| + mLoopCount = loopCount;
|
| + }
|
| +
|
| + /**
|
| + * Returns the loop count for multi-frame animation.
|
| + */
|
| + public int getLoopCount() {
|
| + return mLoopCount;
|
| + }
|
| +
|
| + /**
|
| + * Sets whether to start animation on load or not.
|
| + */
|
| + public void setAnimateOnLoad(boolean animateOnLoad) {
|
| + mAnimateOnLoad = animateOnLoad;
|
| + }
|
| +
|
| + /**
|
| + * Returns {@code true} if the GIF is valid and {@code false} otherwise.
|
| + */
|
| + public boolean isValid() {
|
| + return !mError && mFirstFrameReady;
|
| + }
|
| +
|
| + public void onRecycle() {
|
| + if (mBitmap != null) {
|
| + mBitmap.recycle();
|
| + }
|
| + mBitmap = null;
|
| + mRecycled = true;
|
| + }
|
| +
|
| + /**
|
| + * Enables or disables the GIF from animating. GIF animations are enabled by default.
|
| + */
|
| + public void setAnimationEnabled(boolean animationEnabled) {
|
| + if (mAnimationEnabled == animationEnabled) {
|
| + return;
|
| + }
|
| +
|
| + mAnimationEnabled = animationEnabled;
|
| + if (mAnimationEnabled) {
|
| + start();
|
| + } else {
|
| + stop();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onBoundsChange(Rect bounds) {
|
| + super.onBoundsChange(bounds);
|
| + mWidth = bounds.width();
|
| + mHeight = bounds.height();
|
| + mScale = mWidth != mIntrinsicWidth && mHeight != mIntrinsicHeight;
|
| + if (mScale) {
|
| + mScaleFactor = Math.max((float) mWidth / mIntrinsicWidth,
|
| + (float) mHeight / mIntrinsicHeight);
|
| + }
|
| +
|
| + if (!mError && !mRecycled) {
|
| + // Request that the decoder reset itself
|
| + sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(RESET_DECODER, this));
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public boolean setVisible(boolean visible, boolean restart) {
|
| + boolean changed = super.setVisible(visible, restart);
|
| + if (visible) {
|
| + if (changed || restart) {
|
| + start();
|
| + }
|
| + } else {
|
| + stop();
|
| + }
|
| + return changed;
|
| + }
|
| +
|
| + @Override
|
| + public void draw(Canvas canvas) {
|
| + if (mError || mWidth == 0 || mHeight == 0 || mRecycled || !mFirstFrameReady) {
|
| + return;
|
| + }
|
| +
|
| + if (mScale) {
|
| + canvas.save();
|
| + canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
|
| + canvas.drawBitmap(mBitmap, 0, 0, sScalePaint);
|
| + canvas.restore();
|
| + } else {
|
| + canvas.drawBitmap(mBitmap, 0, 0, sPaint);
|
| + }
|
| +
|
| + if (mRunning) {
|
| + if (!mScheduled) {
|
| + // Schedule the next frame at mFrameDelay milliseconds from the previous frame or
|
| + // the minimum sceduling delay from now, whichever is later.
|
| + mLastFrameTime = Math.max(
|
| + mLastFrameTime + mFrameDelay,
|
| + SystemClock.uptimeMillis() + MIN_FRAME_SCHEDULE_DELAY_MS);
|
| + scheduleSelf(this, mLastFrameTime);
|
| + }
|
| + } else if (!mDone) {
|
| + start();
|
| + } else {
|
| + unscheduleSelf(this);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public int getIntrinsicWidth() {
|
| + return mIntrinsicWidth;
|
| + }
|
| +
|
| + @Override
|
| + public int getIntrinsicHeight() {
|
| + return mIntrinsicHeight;
|
| + }
|
| +
|
| + @Override
|
| + public int getOpacity() {
|
| + return PixelFormat.UNKNOWN;
|
| + }
|
| +
|
| + @Override
|
| + public void setAlpha(int alpha) {
|
| + }
|
| +
|
| + @Override
|
| + public void setColorFilter(ColorFilter cf) {
|
| + }
|
| +
|
| + @Override
|
| + public boolean isRunning() {
|
| + return mRunning;
|
| + }
|
| +
|
| + @Override
|
| + public void start() {
|
| + if (!isRunning()) {
|
| + mRunning = true;
|
| + if (!mAnimateOnLoad) {
|
| + mDone = true;
|
| + }
|
| + mLastFrameTime = SystemClock.uptimeMillis();
|
| + run();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void stop() {
|
| + if (isRunning()) {
|
| + unscheduleSelf(this);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void scheduleSelf(Runnable what, long when) {
|
| + if (mAnimationEnabled) {
|
| + super.scheduleSelf(what, when);
|
| + mScheduled = true;
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void unscheduleSelf(Runnable what) {
|
| + super.unscheduleSelf(what);
|
| + mRunning = false;
|
| + }
|
| +
|
| + /**
|
| + * Moves to the next frame.
|
| + */
|
| + @Override
|
| + public void run() {
|
| + if (mRecycled) {
|
| + return;
|
| + }
|
| +
|
| + // Send request to decoder to read the next frame
|
| + if (!mDone) {
|
| + sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(READ_FRAME_REQ, this));
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Restarts decoding the image from the beginning. Called from the background thread.
|
| + */
|
| + private void reset() {
|
| + // Return to the position of the first image frame in the stream.
|
| + mPosition = mGifImage.mHeaderSize;
|
| + mBackupSaved = false;
|
| + mFrameCount = 0;
|
| + mDisposalMethod = DISPOSAL_METHOD_UNKNOWN;
|
| + }
|
| +
|
| + /**
|
| + * Restarts animation if a limited number of loops of animation have been previously done.
|
| + */
|
| + public void restartAnimation() {
|
| + if (mDone && mLoopCount > 0) {
|
| + reset();
|
| + mDone = false;
|
| + mLoopIndex = 0;
|
| + run();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Reads color table as 256 RGB integer values. Called from the background thread.
|
| + *
|
| + * @param ncolors int number of colors to read
|
| + */
|
| + private void readColorTable(int[] colorTable, int ncolors) {
|
| + for (int i = 0; i < ncolors; i++) {
|
| + int r = mData[mPosition++] & 0xff;
|
| + int g = mData[mPosition++] & 0xff;
|
| + int b = mData[mPosition++] & 0xff;
|
| + colorTable[i] = 0xff000000 | (r << 16) | (g << 8) | b;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Reads GIF content blocks. Called from the background thread.
|
| + *
|
| + * @return true if the next frame has been parsed successfully, false if EOF
|
| + * has been reached
|
| + */
|
| + private void readNextFrame() {
|
| + // Don't clear the image if it is a terminator.
|
| + if ((mData[mPosition] & 0xff) == 0x3b) {
|
| + mEndOfFile = true;
|
| + return;
|
| + }
|
| + disposeOfLastFrame();
|
| +
|
| + mDisposalMethod = DISPOSAL_METHOD_UNKNOWN;
|
| + mTransparency = false;
|
| +
|
| + mEndOfFile = false;
|
| + mNextFrameDelay = 100;
|
| + mLocalColorTable = null;
|
| +
|
| + while (true) {
|
| + int code = mData[mPosition++] & 0xff;
|
| + switch (code) {
|
| + case 0: // Empty block, ignore
|
| + break;
|
| + case 0x21: // Extension. Extensions precede the corresponding image.
|
| + code = mData[mPosition++] & 0xff;
|
| + switch (code) {
|
| + case 0xf9: // graphics control extension
|
| + readGraphicControlExt();
|
| + break;
|
| + case 0xff: // application extension
|
| + readBlock();
|
| + boolean netscape = true;
|
| + for (int i = 0; i < NETSCAPE2_0.length; i++) {
|
| + if (mBlock[i] != NETSCAPE2_0[i]) {
|
| + netscape = false;
|
| + break;
|
| + }
|
| + }
|
| + if (netscape) {
|
| + readNetscapeExtension();
|
| + } 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 0x2C: // Image separator
|
| + readBitmap();
|
| + return;
|
| +
|
| + case 0x3b: // Terminator
|
| + mEndOfFile = true;
|
| + return;
|
| +
|
| + default: // We don't know what this is. Just skip it.
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Disposes of the previous frame. Called from the background thread.
|
| + */
|
| + private void disposeOfLastFrame() {
|
| + if (mFirstFrame) {
|
| + mFirstFrame = false;
|
| + return;
|
| + }
|
| + switch (mDisposalMethod) {
|
| + case DISPOSAL_METHOD_UNKNOWN:
|
| + case DISPOSAL_METHOD_LEAVE: {
|
| + mBackupSaved = false;
|
| + break;
|
| + }
|
| + case DISPOSAL_METHOD_RESTORE: {
|
| + if (mBackupSaved) {
|
| + System.arraycopy(mBackup, 0, mColors, 0, mBackup.length);
|
| + }
|
| + break;
|
| + }
|
| + case DISPOSAL_METHOD_BACKGROUND: {
|
| + mBackupSaved = false;
|
| +
|
| + // Fill last image rect area with background color
|
| + int color = 0;
|
| + if (!mTransparency) {
|
| + color = mBackgroundColor;
|
| + }
|
| + for (int i = 0; i < mFrameHeight; i++) {
|
| + int n1 = (mFrameY + i) * mIntrinsicWidth + mFrameX;
|
| + int n2 = n1 + mFrameWidth;
|
| + for (int k = n1; k < n2; k++) {
|
| + mColors[k] = color;
|
| + }
|
| + }
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Reads Graphics Control Extension values. Called from the background thread.
|
| + */
|
| + private void readGraphicControlExt() {
|
| + mPosition++; // Block size, fixed
|
| +
|
| + int packed = mData[mPosition++] & 0xff; // Packed fields
|
| +
|
| + mDisposalMethod = (packed & 0x1c) >> 2; // Disposal method
|
| + mTransparency = (packed & 1) != 0;
|
| + mNextFrameDelay = readShort() * 10; // Delay in milliseconds
|
| +
|
| + // It seems that there are broken tools out there that set a 0ms or 10ms
|
| + // timeout when they really want a "default" one.
|
| + // Following WebKit's lead (http://trac.webkit.org/changeset/73295)
|
| + // we use 10 frames per second as the default frame rate.
|
| + if (mNextFrameDelay <= 10) {
|
| + mNextFrameDelay = 100;
|
| + }
|
| +
|
| + mTransparentColorIndex = mData[mPosition++] & 0xff;
|
| +
|
| + mPosition++; // Block terminator - ignore
|
| + }
|
| +
|
| + /**
|
| + * Reads Netscape extension to obtain iteration count. Called from the background thread.
|
| + */
|
| + private void readNetscapeExtension() {
|
| + int count;
|
| + do {
|
| + count = readBlock();
|
| + } while ((count > 0) && !mError);
|
| + }
|
| +
|
| + /**
|
| + * Reads next frame image. Called from the background thread.
|
| + */
|
| + private void readBitmap() {
|
| + mFrameX = readShort(); // (sub)image position & size
|
| + mFrameY = readShort();
|
| +
|
| + int width = readShort();
|
| + int height = readShort();
|
| +
|
| + // Clamp the frame dimensions to the intrinsic dimensions.
|
| + mFrameWidth = Math.min(width, mIntrinsicWidth - mFrameX);
|
| + mFrameHeight = Math.min(height, mIntrinsicHeight - mFrameY);
|
| +
|
| + // The frame step is set to the specfied frame width before clamping.
|
| + mFrameStep = width;
|
| +
|
| + // Increase the size of the decoding buffer if necessary.
|
| + int framePixelCount = width * height;
|
| + if (framePixelCount > mPixels.length) {
|
| + mPixels = new byte[framePixelCount];
|
| + }
|
| +
|
| + int packed = mData[mPosition++] & 0xff;
|
| + // 3 - sort flag
|
| + // 4-5 - reserved lctSize = 2 << (packed & 7);
|
| + // 6-8 - local color table size
|
| + mInterlace = (packed & 0x40) != 0;
|
| + mLocalColorTableUsed = (packed & 0x80) != 0; // 1 - local color table flag interlace
|
| + mLocalColorTableSize = (int) Math.pow(2, (packed & 0x07) + 1);
|
| +
|
| + if (mLocalColorTableUsed) {
|
| + if (mLocalColorTable == null) {
|
| + mLocalColorTable = new int[256];
|
| + }
|
| + readColorTable(mLocalColorTable, mLocalColorTableSize);
|
| + mActiveColorTable = mLocalColorTable;
|
| + } else {
|
| + mActiveColorTable = mGifImage.mGlobalColorTable;
|
| + if (mGifImage.mBackgroundIndex == mTransparentColorIndex) {
|
| + mBackgroundColor = 0;
|
| + }
|
| + }
|
| + int savedColor = 0;
|
| + if (mTransparency) {
|
| + savedColor = mActiveColorTable[mTransparentColorIndex];
|
| + mActiveColorTable[mTransparentColorIndex] = 0;
|
| + }
|
| +
|
| + if (mActiveColorTable == null) {
|
| + mError = true;
|
| + }
|
| +
|
| + if (mError) {
|
| + return;
|
| + }
|
| +
|
| + decodeBitmapData();
|
| +
|
| + skip();
|
| +
|
| + if (mError) {
|
| + return;
|
| + }
|
| +
|
| + if (mDisposalMethod == DISPOSAL_METHOD_RESTORE) {
|
| + backupFrame();
|
| + }
|
| +
|
| + populateImageData();
|
| +
|
| + if (mTransparency) {
|
| + mActiveColorTable[mTransparentColorIndex] = savedColor;
|
| + }
|
| +
|
| + mFrameCount++;
|
| + }
|
| +
|
| + /**
|
| + * Stores the relevant portion of the current frame so that it can be restored
|
| + * before the next frame is rendered. Called from the background thread.
|
| + */
|
| + private void backupFrame() {
|
| + if (mBackupSaved) {
|
| + return;
|
| + }
|
| +
|
| + if (mBackup == null) {
|
| + mBackup = null;
|
| + try {
|
| + mBackup = new int[mColors.length];
|
| + } catch (OutOfMemoryError e) {
|
| + Log.e(TAG, "GifDrawable.backupFrame threw an OOME", e);
|
| + }
|
| + }
|
| +
|
| + if (mBackup != null) {
|
| + System.arraycopy(mColors, 0, mBackup, 0, mColors.length);
|
| + mBackupSaved = true;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Decodes LZW image data into pixel array. Called from the background thread.
|
| + */
|
| + private void decodeBitmapData() {
|
| + int npix = mFrameWidth * mFrameHeight;
|
| +
|
| + // Initialize GIF data stream decoder.
|
| + int dataSize = mData[mPosition++] & 0xff;
|
| + int clear = 1 << dataSize;
|
| + int endOfInformation = clear + 1;
|
| + int available = clear + 2;
|
| + int oldCode = -1;
|
| + int codeSize = dataSize + 1;
|
| + int codeMask = (1 << codeSize) - 1;
|
| + for (int code = 0; code < clear; code++) {
|
| + mPrefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
|
| + mSuffix[code] = (byte) code;
|
| + }
|
| +
|
| + // Decode GIF pixel stream.
|
| + int datum = 0;
|
| + int bits = 0;
|
| + int first = 0;
|
| + int top = 0;
|
| + int pi = 0;
|
| + while (pi < npix) {
|
| + int blockSize = mData[mPosition++] & 0xff;
|
| + if (blockSize == 0) {
|
| + break;
|
| + }
|
| +
|
| + int blockEnd = mPosition + blockSize;
|
| + while (mPosition < blockEnd) {
|
| + datum += (mData[mPosition++] & 0xff) << bits;
|
| + bits += 8;
|
| +
|
| + while (bits >= codeSize) {
|
| + // Get the next code.
|
| + int code = datum & codeMask;
|
| + datum >>= codeSize;
|
| + bits -= codeSize;
|
| +
|
| + // Interpret the code
|
| + if (code == clear) {
|
| + // Reset decoder.
|
| + codeSize = dataSize + 1;
|
| + codeMask = (1 << codeSize) - 1;
|
| + available = clear + 2;
|
| + oldCode = -1;
|
| + continue;
|
| + }
|
| +
|
| + // Check for explicit end-of-stream
|
| + if (code == endOfInformation) {
|
| + mPosition = blockEnd;
|
| + return;
|
| + }
|
| +
|
| + if (oldCode == -1) {
|
| + mPixels[pi++] = mSuffix[code];
|
| + oldCode = code;
|
| + first = code;
|
| + continue;
|
| + }
|
| +
|
| + int inCode = code;
|
| + if (code >= available) {
|
| + mPixelStack[top++] = (byte) first;
|
| + code = oldCode;
|
| + if (top == MAX_BITS) {
|
| + mError = true;
|
| + return;
|
| + }
|
| + }
|
| +
|
| + while (code >= clear) {
|
| + if (code >= MAX_BITS || code == mPrefix[code]) {
|
| + mError = true;
|
| + return;
|
| + }
|
| +
|
| + mPixelStack[top++] = mSuffix[code];
|
| + code = mPrefix[code];
|
| +
|
| + if (top == MAX_BITS) {
|
| + mError = true;
|
| + return;
|
| + }
|
| + }
|
| +
|
| + first = mSuffix[code];
|
| + mPixelStack[top++] = (byte) first;
|
| +
|
| + // Add new code to the dictionary
|
| + if (available < MAX_STACK_SIZE) {
|
| + mPrefix[available] = (short) oldCode;
|
| + mSuffix[available] = (byte) first;
|
| + available++;
|
| +
|
| + if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) {
|
| + codeSize++;
|
| + codeMask += available;
|
| + }
|
| + }
|
| +
|
| + oldCode = inCode;
|
| +
|
| + // Drain the pixel stack.
|
| + do {
|
| + mPixels[pi++] = mPixelStack[--top];
|
| + } while (top > 0);
|
| + }
|
| + }
|
| + }
|
| +
|
| + while (pi < npix) {
|
| + mPixels[pi++] = 0; // clear missing pixels
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Populates the color array with pixels for the next frame.
|
| + */
|
| + private void populateImageData() {
|
| +
|
| + // 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 < mFrameHeight; i++) {
|
| + int line = i;
|
| + if (mInterlace) {
|
| + if (iline >= mFrameHeight) {
|
| + 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 += mFrameY;
|
| + if (line < mIntrinsicHeight) {
|
| + int k = line * mIntrinsicWidth;
|
| + int dx = k + mFrameX; // start of line in dest
|
| + int dlim = dx + mFrameWidth; // end of dest line
|
| +
|
| + // It is unnecesary to test if dlim is beyond the edge of the destination line,
|
| + // since mFrameWidth is clamped to a maximum of mIntrinsicWidth - mFrameX.
|
| +
|
| + int sx = i * mFrameStep; // start of line in source
|
| + while (dx < dlim) {
|
| + // map color and insert in destination
|
| + int index = mPixels[sx++] & 0xff;
|
| + int c = mActiveColorTable[index];
|
| + if (c != 0) {
|
| + mColors[dx] = c;
|
| + }
|
| + dx++;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Reads next variable length block from input. Called from the background thread.
|
| + *
|
| + * @return number of bytes stored in "buffer"
|
| + */
|
| + private int readBlock() {
|
| + int blockSize = mData[mPosition++] & 0xff;
|
| + if (blockSize > 0) {
|
| + System.arraycopy(mData, mPosition, mBlock, 0, blockSize);
|
| + mPosition += blockSize;
|
| + }
|
| + return blockSize;
|
| + }
|
| +
|
| + /**
|
| + * Reads next 16-bit value, LSB first. Called from the background thread.
|
| + */
|
| + private int readShort() {
|
| + // read 16-bit value, LSB first
|
| + int byte1 = mData[mPosition++] & 0xff;
|
| + int byte2 = mData[mPosition++] & 0xff;
|
| + return byte1 | (byte2 << 8);
|
| + }
|
| +
|
| + /**
|
| + * Skips variable length blocks up to and including next zero length block.
|
| + * Called from the background thread.
|
| + */
|
| + private void skip() {
|
| + int blockSize;
|
| + do {
|
| + blockSize = mData[mPosition++] & 0xff;
|
| + mPosition += blockSize;
|
| + } while (blockSize > 0);
|
| + }
|
| +
|
| + @Override
|
| + public boolean handleMessage(Message msg) {
|
| + if (msg.what == BaseGifDrawable.READ_FRAME_RESP) {
|
| + mFrameDelay = msg.arg1;
|
| + if (mBitmap != null) {
|
| + mBitmap.setPixels(mColors, 0, mIntrinsicWidth,
|
| + 0, 0, mIntrinsicWidth, mIntrinsicHeight);
|
| + postProcessFrame(mBitmap);
|
| + mFirstFrameReady = true;
|
| + mScheduled = false;
|
| + invalidateSelf();
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Gives a subclass a chance to apply changes to the mutable bitmap
|
| + * before showing the frame.
|
| + */
|
| + protected void postProcessFrame(Bitmap bitmap) {
|
| + }
|
| +
|
| + /**
|
| + * Background thread that handles reading and decoding frames of GIF images.
|
| + */
|
| + private static class DecoderThread extends HandlerThread
|
| + implements android.os.Handler.Callback {
|
| + private static final String DECODER_THREAD_NAME = "GifDecoder";
|
| +
|
| + public DecoderThread() {
|
| + super(DECODER_THREAD_NAME);
|
| + }
|
| +
|
| + @Override
|
| + public boolean handleMessage(Message msg) {
|
| + BaseGifDrawable gif = (BaseGifDrawable) msg.obj;
|
| + if (gif == null || gif.mBitmap == null || gif.mRecycled) {
|
| + return true;
|
| + }
|
| +
|
| + switch (msg.what) {
|
| +
|
| + case READ_FRAME_REQ:
|
| + // Processed on background thread
|
| + do {
|
| + try {
|
| + gif.readNextFrame();
|
| + } catch (ArrayIndexOutOfBoundsException e) {
|
| + gif.mEndOfFile = true;
|
| + }
|
| +
|
| + // Check for EOF
|
| + if (gif.mEndOfFile) {
|
| + if (gif.mFrameCount == 0) {
|
| + // could not read first frame
|
| + gif.mError = true;
|
| + } else if (gif.mFrameCount > 1) {
|
| + if (gif.mLoopCount == 0 || ++gif.mLoopIndex < gif.mLoopCount) {
|
| + // Repeat the animation
|
| + gif.reset();
|
| + } else {
|
| + gif.mDone = true;
|
| + }
|
| + } else {
|
| + // Only one frame. Mark as done.
|
| + gif.mDone = true;
|
| + }
|
| + }
|
| + } while (gif.mEndOfFile && !gif.mError && !gif.mDone);
|
| + gif.mHandler.sendMessage(gif.mHandler.obtainMessage(READ_FRAME_RESP,
|
| + gif.mNextFrameDelay, 0));
|
| + return true;
|
| +
|
| + case RESET_DECODER:
|
| + gif.reset();
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| + }
|
| + }
|
| +}
|
|
|