package com.tomclaw.mandarin.util;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.LongSparseArray;
import android.util.Log;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.IntBuffer;
import java.util.concurrent.Semaphore;
public class GifDrawable extends Drawable implements Animatable {
private static final String TAG = "GifImageView";
private static final boolean INFO = true;
private static final boolean DEBUG = true;
private static final boolean VERBOSE = true;
private Bitmap imageBitmap;
private final Matrix matrix = new Matrix();
private final GifFileDecoder decoder;
public static interface DiagnosticsCallback {
public void onDiagnostics(String value);
}
public static DiagnosticsCallback diagnosticsCallback;
public static final int STATE_STOPPED = 0;
public static final int STATE_PLAYING = 1;
public static final int STATE_PAUSED = 2;
protected final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
public GifDrawable(GifFileDecoder decoder) {
paint.setAntiAlias(false);
paint.setFilterBitmap(false);
paint.setDither(false);
this.decoder = decoder;
imageBitmap = Bitmap.createBitmap(decoder.getWidth(), decoder.getHeight(),
Bitmap.Config.ARGB_8888);
}
public int getGifState() {
return GifDrawable.getState(this);
}
public void play() {
switch (getGifState()) {
case STATE_PLAYING:
if (INFO) Log.i(TAG, "already playing");
break;
case STATE_STOPPED:
GifDrawable.start(this);
break;
case STATE_PAUSED:
GifDrawable.resume(this);
}
}
public void pause() {
if (getGifState() == STATE_PLAYING) {
GifDrawable.pause(this);
} else {
Log.w(TAG, "can't pause");
}
}
public void stop() {
if (getGifState() == STATE_PLAYING || getGifState() == STATE_PAUSED) {
GifDrawable.stop(this);
} else {
Log.w(TAG, "can't stop");
}
}
// Static dispatcher methods
private static final int MSG_REDRAW = 1;
private static final int MSG_FINALIZE = 2;
private static LongSparseArray<ThreadInfo> threads = new LongSparseArray<ThreadInfo>();
private static Handler mainHandler;
private static class ThreadInfo {
public WeakReference<GifDrawable> view;
public Semaphore pause = new Semaphore(1);
public boolean paused = false;
}
private static class ThreadParam {
public long threadId;
public Bitmap bitmap;
}
static {
mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
mainThread(msg.what, (ThreadParam) msg.obj);
}
};
}
private synchronized static void start(GifDrawable view) {
if (INFO) Log.i(TAG, "start");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
backgroundThread();
}
});
ThreadInfo info = new ThreadInfo();
info.view = new WeakReference<>(view);
threads.put(thread.getId(), info);
thread.start();
}
private synchronized static void stop(GifDrawable view) {
if (INFO) Log.i(TAG, "stop");
ThreadInfo info = getThreadInfo(view);
if (info != null) {
stopThread(info);
}
}
public synchronized static void stopAll() {
for (int i = 0; i < threads.size(); i++) {
stopThread(threads.valueAt(i));
}
}
private static void stopThread(ThreadInfo info) {
info.view.clear();
if (info.paused) {
info.pause.release();
info.paused = false;
}
}
private synchronized static void pause(GifDrawable view) {
if (INFO) Log.i(TAG, "pause");
ThreadInfo info = getThreadInfo(view);
if (info != null && !info.paused) {
try {
info.pause.acquire();
} catch (InterruptedException ex) {
}
info.paused = true;
}
}
private synchronized static void resume(GifDrawable view) {
if (INFO) Log.i(TAG, "resume");
ThreadInfo info = getThreadInfo(view);
if (info != null && info.paused) {
info.pause.release();
info.paused = false;
}
}
private synchronized static int getState(GifDrawable view) {
ThreadInfo info = getThreadInfo(view);
if (info == null) {
return STATE_STOPPED;
}
if (info.paused) {
return STATE_PAUSED;
}
return STATE_PLAYING;
}
private synchronized static ThreadInfo getThreadInfo(GifDrawable view) {
for (int i = 0; i < threads.size(); i++) {
ThreadInfo info = threads.valueAt(i);
GifDrawable threadView = info.view.get();
if (view.equals(threadView))
return info;
}
return null;
}
private synchronized static ThreadInfo getThreadInfo(long threadId) {
return threads.get(threadId);
}
private synchronized static void removeThread(long threadId) {
threads.remove(threadId);
}
private static void backgroundThread() {
long threadId = Thread.currentThread().getId();
ThreadInfo info = getThreadInfo(threadId);
if (info == null) {
return;
}
if (DEBUG) Log.d(TAG, "started thread " + threadId);
long startTime = System.currentTimeMillis();
long infoTime = startTime + 10 * 1000;
int delay = 0;
long frameIndex = 0;
long decodeTimes = 0;
long delays = 0;
boolean diagDone = false;
GifDrawable view = info.view.get();
GifFileDecoder decoder;
if (view != null) {
decoder = view.decoder;
Bitmap bitmap = view.imageBitmap;
try {
while (decoder.hasFrame()) {
// decode frame
long frameStart = System.currentTimeMillis();
int[] pixels = decoder.readFrame();
if (pixels == null) {
if (DEBUG) Log.d(TAG, "null frame, stopping");
break;
}
long decodeTime = System.currentTimeMillis() - frameStart;
if (VERBOSE) Log.v(TAG, "decoded frame in " + decodeTime + " delay " + delay);
// wait until the end of delay set by previous frame
Thread.sleep(Math.max(0, delay - decodeTime));
// check for pause
info.pause.acquire();
info.pause.release();
// check if view still exists
if (info.view.get() == null) {
break;
}
// send frame to view
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(pixels));
sendToMain(threadId, MSG_REDRAW, null);
delay = decoder.getDelay();
// some logging
if (diagnosticsCallback != null && !diagDone) {
frameIndex++;
decodeTimes += decodeTime;
delays += delay;
if (System.currentTimeMillis() > startTime + 5 * 1000) {
long fpsa = frameIndex * 1000 / decodeTimes;
long fpsb = frameIndex * 1000 / delays;
String value = "size: " + bitmap.getWidth() + " x " + bitmap.getHeight() +
"\nfps: " + fpsa + " / " + fpsb;
diagnosticsCallback.onDiagnostics(value);
diagDone = true;
}
}
if (System.currentTimeMillis() > infoTime) {
if (INFO) Log.i(TAG, "Gif thread still running");
infoTime += 10 * 1000;
}
if (System.currentTimeMillis() > startTime + 4 * 60 * 60 * 1000) {
throw new RuntimeException("Gif thread leaked, fix your code");
}
}
} catch (IOException ex) {
Logger.log("gif drawable warn", ex);
} catch (InterruptedException ex) {
Logger.log("gif drawable err", ex);
} finally {
if (DEBUG) Log.d(TAG, "stopping decoder");
decoder.stop();
}
}
sendToMain(threadId, MSG_FINALIZE, null);
if (DEBUG) Log.d(TAG, "finished thread " + threadId);
}
private static void sendToMain(long threadId, int what, Bitmap bitmap) {
ThreadParam param = new ThreadParam();
param.threadId = threadId;
param.bitmap = bitmap;
mainHandler.obtainMessage(what, param).sendToTarget();
}
private static void mainThread(int what, ThreadParam obj) {
if (what == MSG_FINALIZE) {
if (DEBUG) Log.d(TAG, "removing thread " + obj.threadId);
removeThread(obj.threadId);
return;
}
ThreadInfo info = getThreadInfo(obj.threadId);
if (info == null) {
if (DEBUG) Log.d(TAG, "no thread info");
return;
}
GifDrawable view = info.view.get();
if (view == null) {
if (DEBUG) Log.d(TAG, "no view");
return;
}
if (what == MSG_REDRAW) {
view.invalidateSelf();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
matrix.setRectToRect(new RectF(0, 0, imageBitmap.getWidth(), imageBitmap.getHeight()),
new RectF(getBounds()), Matrix.ScaleToFit.CENTER);
}
@Override
public void draw(Canvas canvas) {
if (imageBitmap != null) {
canvas.drawBitmap(imageBitmap, matrix, paint);
}
}
@Override
public int getIntrinsicWidth() {
return imageBitmap.getWidth();
}
@Override
public int getIntrinsicHeight() {
return imageBitmap.getHeight();
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
@Override
public void start() {
play();
}
@Override
public boolean isRunning() {
return getGifState() == STATE_PLAYING;
}
}