package com.koushikdutta.ion;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.widget.ImageView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.async.util.FileCache;
import com.koushikdutta.ion.bitmap.BitmapInfo;
import com.koushikdutta.ion.gif.GifDecoder;
import com.koushikdutta.ion.gif.GifFrame;
import java.lang.ref.WeakReference;
/**
* Created by koush on 6/8/13.
*/
class IonDrawable extends LayerDrawable {
private static final double LOG_2 = Math.log(2);
private static final int TILE_DIM = 256;
private static final long FADE_DURATION = 200;
private static final int DEFAULT_PAINT_FLAGS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
private Paint paint;
private BitmapInfo info;
private int placeholderResource;
private Drawable placeholder;
private int errorResource;
private Drawable error;
private Resources resources;
private ResponseServedFrom servedFrom;
private boolean fadeIn;
private int resizeWidth;
private int resizeHeight;
private boolean repeatAnimation;
private Ion ion;
private BitmapFetcher bitmapFetcher;
private IonDrawableCallback callback;
private FutureCallback<IonDrawable> loadCallback;
private IonGifDecoder gifDecoder;
private Drawable bitmapDrawable;
private int textureDim;
private int maxLevel;
private BitmapDrawableFactory bitmapDrawableFactory;
private final Drawable NULL_PLACEHOLDER;
private final Drawable NULL_BITMAPINFO;
private final Drawable NULL_ERROR;
public FutureCallback<IonDrawable> getLoadCallback() {
return loadCallback;
}
public IonDrawable setLoadCallback(FutureCallback<IonDrawable> loadCallback) {
this.loadCallback = loadCallback;
return this;
}
public IonDrawable ion(Ion ion) {
if (ion == null)
throw new AssertionError("null ion");
this.ion = ion;
return this;
}
public Drawable getCurrentDrawable() {
if (info == null) {
if (placeholderResource != 0)
return resources.getDrawable(placeholderResource);
}
if (info != null) {
if (info.bitmap != null)
return new BitmapDrawable(resources, info.bitmap);
else if (info.gifDecoder != null) {
GifFrame last = info.gifDecoder.getLastFrame();
if (last != null)
return new BitmapDrawable(resources, last.image);
if (placeholderResource != 0)
return resources.getDrawable(placeholderResource);
return null;
}
}
if (errorResource != 0)
return resources.getDrawable(errorResource);
return null;
}
public BitmapInfo getBitmapInfo() {
return info;
}
// create an internal static class that can act as a callback.
// dont let it hold strong references to anything.
static class IonDrawableCallback implements FutureCallback<BitmapInfo> {
private WeakReference<IonDrawable> ionDrawableRef;
private String bitmapKey;
private Ion ion;
public IonDrawableCallback(IonDrawable drawable) {
ionDrawableRef = new WeakReference<IonDrawable>(drawable);
// imageViewRef = new ContextReference.ImageViewContextReference(imageView);
}
public void register(Ion ion, String bitmapKey) {
String previousKey = this.bitmapKey;
Ion previousIon = this.ion;
if (TextUtils.equals(previousKey, bitmapKey) && this.ion == ion)
return;
this.ion = ion;
this.bitmapKey = bitmapKey;
if (ion != null)
ion.bitmapsPending.add(bitmapKey, this);
unregister(previousIon, previousKey);
}
private void unregister(Ion ion, String key) {
if (key == null)
return;
// unregister this drawable from the bitmaps that are
// pending.
// if this drawable was the only thing waiting for this bitmap,
// then the removeItem call will return the TransformBitmap/LoadBitmap instance
// that was providing the result.
if (ion.bitmapsPending.removeItem(key, this)) {
// find out who owns this thing, to see if it is a candidate for removal
Object owner = ion.bitmapsPending.tag(key);
if (owner instanceof TransformBitmap) {
TransformBitmap info = (TransformBitmap)owner;
ion.bitmapsPending.remove(info.key);
// this transform is also backed by a LoadBitmap* or a DeferredLoadBitmap, grab that
// if it is the only waiter
if (ion.bitmapsPending.removeItem(info.downloadKey, info))
owner = ion.bitmapsPending.tag(info.downloadKey);
}
// only cancel deferred loads... LoadBitmap means a download is already in progress.
// due to view recycling, cancelling that may be bad, as it may be rerequested again
// during the recycle process.
if (owner instanceof DeferredLoadBitmap) {
DeferredLoadBitmap defer = (DeferredLoadBitmap)owner;
ion.bitmapsPending.remove(defer.key);
}
}
ion.processDeferred();
}
@Override
public void onCompleted(Exception e, BitmapInfo result) {
assert Thread.currentThread() == Looper.getMainLooper().getThread();
assert result != null;
// see if the imageview is still alive and cares about this result
IonDrawable drawable = ionDrawableRef.get();
if (drawable == null)
return;
drawable
.setBitmap(result, result.servedFrom)
.updateLayers();
FutureCallback<IonDrawable> callback = drawable.loadCallback;
if (callback != null)
callback.onCompleted(e, drawable);
}
}
class IonGifDecoder {
GifDecoder gifDecoder;
Exception exception;
GifFrame currentFrame;
long nextFrameRender;
public IonGifDecoder(BitmapInfo info){
gifDecoder = info.gifDecoder.mutate();
currentFrame = gifDecoder.getLastFrame();
}
Runnable loader = new Runnable() {
@Override
public void run() {
try {
gifDecoder.nextFrame();
}
catch (Exception e) {
exception = e;
}
Ion.mainHandler.post(postLoad);
}
};
Runnable postLoad = new Runnable() {
@Override
public void run() {
isLoading = false;
invalidateSelf();
}
};
long getDelay() {
// error case?
if (currentFrame == null)
return 1000 / 10;
long delay = currentFrame.delay;
if (delay == 0)
delay = 1000 / 10;
return delay;
}
public GifFrame getCurrentFrame() {
long now = System.currentTimeMillis();
if (nextFrameRender == 0) {
nextFrameRender = now + getDelay();
scheduleNextFrame();
}
if (now >= nextFrameRender) {
// see if a frame is available
if (gifDecoder.getLastFrame() != currentFrame) {
// we have a frame waiting, grab it i guess.
currentFrame = gifDecoder.getLastFrame();
// check if we need to drop frames, or maintain timing
if (now > nextFrameRender + getDelay())
nextFrameRender = now + getDelay();
else
nextFrameRender += getDelay();
}
scheduleNextFrame();
}
return currentFrame;
}
boolean isLoading;
public synchronized void scheduleNextFrame() {
if (isLoading)
return;
if (exception != null)
return;
if (gifDecoder.getStatus() == GifDecoder.STATUS_FINISH && repeatAnimation)
gifDecoder.restart();
isLoading = true;
Ion.getBitmapLoadExecutorService().execute(loader);
}
}
public IonDrawable setFadeIn(boolean fadeIn) {
this.fadeIn = fadeIn;
return this;
}
public IonDrawable setBitmapFetcher(BitmapFetcher bitmapFetcher) {
this.bitmapFetcher = bitmapFetcher;
if (ion == null)
throw new AssertionError("null ion");
return this;
}
public IonDrawable setBitmapDrawableFactory(BitmapDrawableFactory factory) {
this.bitmapDrawableFactory = factory;
return this;
}
public void cancel() {
callback.register(null, null);
bitmapFetcher = null;
}
public IonDrawable(Resources resources) {
super(new Drawable[] { new BitmapDrawable((Bitmap)null), new BitmapDrawable((Bitmap)null), new BitmapDrawable((Bitmap)null) });
setId(0, 0);
setId(1, 1);
setId(2, 2);
NULL_PLACEHOLDER = getDrawable(0);
NULL_BITMAPINFO = getDrawable(1);
NULL_ERROR = getDrawable(2);
this.resources = resources;
paint = new Paint(DEFAULT_PAINT_FLAGS);
callback = new IonDrawableCallback(this);
}
public IonDrawable updateLayers() {
// always set up the placeholder, it will disappear automagically
tryGetPlaceholderResource();
if (placeholder == null)
setDrawableByLayerId(0, NULL_PLACEHOLDER);
else
setDrawableByLayerId(0, placeholder);
if (info == null) {
setDrawableByLayerId(1, NULL_BITMAPINFO);
setDrawableByLayerId(2, NULL_ERROR);
return this;
}
// error case
if (info.bitmap == null && info.decoder == null && info.gifDecoder == null) {
setDrawableByLayerId(1, NULL_BITMAPINFO);
tryGetErrorResource();
if (error == null)
setDrawableByLayerId(2, NULL_ERROR);
else
setDrawableByLayerId(2, error);
return this;
}
if (info.decoder == null && info.gifDecoder == null) {
// normal bitmap
tryGetBitmapResource();
setDrawableByLayerId(1, bitmapDrawable);
}
else {
// gif or deepzoom
setDrawableByLayerId(1, NULL_BITMAPINFO);
}
setDrawableByLayerId(2, NULL_ERROR);
return this;
}
public IonDrawable setBitmap(BitmapInfo info, ResponseServedFrom servedFrom) {
if (this.info == info)
return this;
cancel();
this.servedFrom = servedFrom;
this.info = info;
gifDecoder = null;
bitmapDrawable = null;
invalidateSelf();
if (info == null)
return this;
if (info.decoder != null) {
// find number of tiles across to fit
double wlevel = (double)info.originalSize.x / TILE_DIM;
double hlevel = (double)info.originalSize.y / TILE_DIM;
// find the level: find how many power of 2 tiles are necessary
// to fit the entire image. ie, fit it into a square.
double level = Math.max(wlevel, hlevel);
level = Math.log(level) / LOG_2;
maxLevel = (int)Math.ceil(level);
// now, we know the entire image will fit in a square image of
// this dimension:
textureDim = TILE_DIM << maxLevel;
}
else if (info.gifDecoder != null) {
gifDecoder = new IonGifDecoder(info);
}
return this;
}
public IonDrawable setRepeatAnimation(boolean repeatAnimation) {
this.repeatAnimation = repeatAnimation;
return this;
}
public IonDrawable setSize(int resizeWidth, int resizeHeight) {
if (this.resizeWidth == resizeWidth && this.resizeHeight == resizeHeight)
return this;
this.resizeWidth = resizeWidth;
this.resizeHeight = resizeHeight;
invalidateSelf();
return this;
}
public IonDrawable setError(int resource, Drawable drawable) {
if ((drawable != null && drawable == error) || (resource != 0 && resource == errorResource))
return this;
errorResource = resource;
error = drawable;
return this;
}
public IonDrawable setPlaceholder(int resource, Drawable drawable) {
if ((drawable != null && drawable == placeholder) || (resource != 0 && resource == placeholderResource))
return this;
placeholderResource = resource;
placeholder = drawable;
return this;
}
private Drawable tryGetErrorResource() {
if (error != null)
return error;
if (errorResource == 0)
return null;
error = resources.getDrawable(errorResource);
return error;
}
private Drawable tryGetBitmapResource() {
if (bitmapDrawable != null)
return bitmapDrawable;
if (info == null)
return null;
if (info.gifDecoder != null)
return null;
if (info.decoder != null)
return null;
if (info.bitmap == null)
return null;
bitmapDrawable = bitmapDrawableFactory.fromBitmap(resources, info.bitmap);
return bitmapDrawable;
}
private Drawable tryGetPlaceholderResource() {
if (placeholder != null)
return placeholder;
if (placeholderResource == 0)
return null;
placeholder = resources.getDrawable(placeholderResource);
return placeholder;
}
private FutureCallback<BitmapInfo> tileCallback = new FutureCallback<BitmapInfo>() {
@Override
public void onCompleted(Exception e, BitmapInfo result) {
invalidateSelf();
}
};
@Override
public int getIntrinsicWidth() {
// first check if image was loaded
if (info != null) {
if (info.decoder != null)
return info.originalSize.x;
if (info.bitmap != null)
return info.bitmap.getScaledWidth(resources.getDisplayMetrics().densityDpi);
}
if (gifDecoder != null)
return gifDecoder.gifDecoder.getWidth();
// check eventual image size...
if (resizeWidth > 0)
return resizeWidth;
// no image, but there was an error
if (info != null) {
Drawable error = tryGetErrorResource();
if (error != null)
return error.getIntrinsicWidth();
}
// check placeholder
Drawable placeholder = tryGetPlaceholderResource();
if (placeholder != null)
return placeholder.getIntrinsicWidth();
// we're SOL
return -1;
}
@Override
public int getIntrinsicHeight() {
if (info != null) {
if (info.decoder != null)
return info.originalSize.y;
if (info.bitmap != null)
return info.bitmap.getScaledHeight(resources.getDisplayMetrics().densityDpi);
}
if (gifDecoder != null)
return gifDecoder.gifDecoder.getHeight();
if (resizeHeight > 0)
return resizeHeight;
if (info != null) {
Drawable error = tryGetErrorResource();
if (error != null)
return error.getIntrinsicHeight();
}
Drawable placeholder = tryGetPlaceholderResource();
if (placeholder != null)
return placeholder.getIntrinsicHeight();
return -1;
}
@Override
public void draw(Canvas canvas) {
if (info == null) {
// draw stuff
super.draw(canvas);
// see if we can fetch a bitmap
if (bitmapFetcher != null) {
if (bitmapFetcher.sampleWidth == 0 && bitmapFetcher.sampleHeight == 0) {
if (canvas.getWidth() != 1)
bitmapFetcher.sampleWidth = canvas.getWidth();
if (canvas.getHeight() != 1)
bitmapFetcher.sampleHeight = canvas.getHeight();
// now that we have final dimensions, reattempt to find the image in the cache
bitmapFetcher.recomputeDecodeKey();
BitmapInfo found = ion.bitmapCache.get(bitmapFetcher.bitmapKey);
if (found != null) {
// won't be needing THIS anymore
bitmapFetcher = null;
// found what we're looking for, but can't draw at this very moment,
// since we need to trigger a new measure.
callback.onCompleted(null, found);
return;
}
}
// no image found fetch it.
callback.register(ion, bitmapFetcher.bitmapKey);
// check to see if there's too many imageview loads
// already in progress
if (BitmapFetcher.shouldDeferImageView(ion)) {
bitmapFetcher.defer();
} else {
bitmapFetcher.execute();
}
// won't be needing THIS anymore
bitmapFetcher = null;
}
// well, can't do anything else here.
return;
}
if (info.decoder != null) {
drawDeepZoom(canvas);
return;
}
if (info.drawTime == 0)
info.drawTime = SystemClock.uptimeMillis();
long destAlpha = 0xFF;
if (fadeIn) {
destAlpha = ((SystemClock.uptimeMillis() - info.drawTime) << 8) / FADE_DURATION;
destAlpha = Math.min(destAlpha, 0xFF);
}
// remove plaeholder if not visible
if (destAlpha == 255) {
if (placeholder != null) {
placeholder = null;
setDrawableByLayerId(0, NULL_PLACEHOLDER);
}
} else {
// invalidate to fade in
if (placeholder != null)
invalidateSelf();
}
if (info.gifDecoder != null) {
super.draw(canvas);
GifFrame frame = gifDecoder.getCurrentFrame();
if (frame != null) {
paint.setAlpha((int) destAlpha);
canvas.drawBitmap(frame.image, null, getBounds(), paint);
paint.setAlpha(0xFF);
invalidateSelf();
}
return;
}
if (info.bitmap != null) {
if (bitmapDrawable != null)
bitmapDrawable.setAlpha((int)destAlpha);
} else {
if (error != null)
error.setAlpha((int)destAlpha);
}
super.draw(canvas);
if (true)
return;
// stolen from picasso
canvas.save();
canvas.rotate(45);
paint.setColor(Color.WHITE);
canvas.drawRect(0, -10, 7.5f, 10, paint);
int sourceColor;
if (servedFrom == ResponseServedFrom.LOADED_FROM_CACHE)
sourceColor = Color.CYAN;
else if (servedFrom == ResponseServedFrom.LOADED_FROM_CONDITIONAL_CACHE)
sourceColor = Color.YELLOW;
else if (servedFrom == ResponseServedFrom.LOADED_FROM_MEMORY)
sourceColor = Color.GREEN;
else
sourceColor = Color.RED;
paint.setColor(sourceColor);
canvas.drawRect(0, -9, 6.5f, 9, paint);
canvas.restore();
}
private void drawDeepZoom(Canvas canvas) {
// zoom 0: entire image fits in a TILE_DIMxTILE_DIM square
// draw base bitmap for empty tiles
// figure out zoom level
// figure out which tiles need rendering
// draw stuff that needs drawing
// missing tile? fetch it
// use parent level tiles for tiles that do not exist
// TODO: crossfading?
Rect clip = canvas.getClipBounds();
Rect bounds = getBounds();
float zoom = (float)canvas.getWidth() / (float)clip.width();
float zoomWidth = zoom * bounds.width();
float zoomHeight = zoom * bounds.height();
double wlevel = Math.log(zoomWidth / TILE_DIM) / LOG_2;
double hlevel = Math.log(zoomHeight/ TILE_DIM) / LOG_2;
double maxLevel = Math.max(wlevel, hlevel);
int visibleLeft = Math.max(0, clip.left);
int visibleRight = Math.min(bounds.width(), clip.right);
int visibleTop = Math.max(0, clip.top);
int visibleBottom = Math.min(bounds.height(), clip.bottom);
int level = (int)Math.floor(maxLevel);
level = Math.min(this.maxLevel, level);
level = Math.max(level, 0);
int levelTiles = 1 << level;
int textureTileDim = textureDim / levelTiles;
// System.out.println("textureTileDim: " + textureTileDim);
// System.out.println(info.key + " visible: " + new Rect(visibleLeft, visibleTop, visibleRight, visibleBottom));
final boolean DEBUG_ZOOM = false;
if (info.bitmap != null) {
canvas.drawBitmap(info.bitmap, null, getBounds(), paint);
if (DEBUG_ZOOM) {
paint.setColor(Color.RED);
paint.setAlpha(0x80);
canvas.drawRect(getBounds(), paint);
paint.setAlpha(0xFF);
}
}
else {
paint.setColor(Color.BLACK);
canvas.drawRect(getBounds(), paint);
}
int sampleSize = 1;
while (textureTileDim / sampleSize > TILE_DIM)
sampleSize <<= 1;
for (int y = 0; y < levelTiles; y++) {
int top = textureTileDim * y;
int bottom = textureTileDim * (y + 1);
bottom = Math.min(bottom, bounds.bottom);
// TODO: start at visible pos
if (bottom < visibleTop)
continue;
if (top > visibleBottom)
break;
for (int x = 0; x < levelTiles; x++) {
int left = textureTileDim * x;
int right = textureTileDim * (x + 1);
right = Math.min(right, bounds.right);
// TODO: start at visible pos
if (right < visibleLeft)
continue;
if (left > visibleRight)
break;
Rect texRect = new Rect(left, top, right, bottom);
// find, render/fetch
// System.out.println("rendering: " + texRect + " for: " + bounds);
String tileKey = FileCache.toKeyString(info.key, ",", level, ",", x, ",", y);
BitmapInfo tile = ion.bitmapCache.get(tileKey);
if (tile != null && tile.bitmap != null) {
// render it
// System.out.println("bitmap is: " + tile.bitmaps[0].getWidth() + "x" + tile.bitmaps[0].getHeight());
canvas.drawBitmap(tile.bitmap, null, texRect, paint);
continue;
}
// TODO: cancellation of unnecessary regions when fast pan/zooming
if (ion.bitmapsPending.tag(tileKey) == null) {
// fetch it
// System.out.println(info.key + ": fetching region: " + texRect + " sample size: " + sampleSize);
LoadBitmapRegion region = new LoadBitmapRegion(ion, tileKey, info.decoder, texRect, sampleSize);
}
ion.bitmapsPending.add(tileKey, tileCallback);
int parentLeft = 0;
int parentTop = 0;
int parentUp = 1;
int parentLevel = level - parentUp;
if (x % 2 == 1)
parentLeft++;
if (y % 2 == 1)
parentTop++;
int parentX = x >> 1;
int parentY = y >> 1;
while (parentLevel >= 0) {
tileKey = FileCache.toKeyString(info.key, ",", parentLevel, ",", parentX, ",", parentY);
tile = ion.bitmapCache.get(tileKey);
if (tile != null && tile.bitmap != null)
break;
if (parentX % 2 == 1) {
parentLeft += 1 << parentUp;
}
if (parentY % 2 == 1) {
parentTop += 1 << parentUp;
}
parentLevel--;
parentUp++;
parentX >>= 1;
parentY >>= 1;
}
// well, i give up
if (tile == null || tile.bitmap == null)
continue;
int subLevelTiles = 1 << parentLevel;
int subtileDim = textureDim / subLevelTiles;
int subSampleSize = 1;
while (subtileDim / subSampleSize > TILE_DIM)
subSampleSize <<= 1;
int subTextureDim = subtileDim / subSampleSize;
// System.out.println(String.format("falling back for %s,%s,%s to %s,%s,%s: %s,%s (%s to %s)", x, y, level, parentX, parentY, parentLevel, parentLeft, parentTop, subTextureDim, subTextureDim >> parentUp));
subTextureDim >>= parentUp;
int sourceLeft = subTextureDim * parentLeft;
int sourceTop = subTextureDim * parentTop;
Rect sourceRect = new Rect(sourceLeft, sourceTop, sourceLeft + subTextureDim, sourceTop + subTextureDim);
canvas.drawBitmap(tile.bitmap, sourceRect, texRect, paint);
if (DEBUG_ZOOM) {
paint.setColor(Color.RED);
paint.setAlpha(0x80);
canvas.drawRect(texRect, paint);
paint.setAlpha(0xFF);
}
}
}
}
@Override
public void setAlpha(int alpha) {
super.setAlpha(alpha);
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
super.setColorFilter(cf);
paint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return (info == null || info.bitmap == null || info.bitmap.hasAlpha() || paint.getAlpha() < 255) ?
PixelFormat.TRANSLUCENT : super.getOpacity();
}
static IonDrawable getOrCreateIonDrawable(ImageView imageView) {
Drawable current = imageView.getDrawable();
IonDrawable ret;
if (current == null || !(current instanceof IonDrawable))
ret = new IonDrawable(imageView.getResources());
else
ret = (IonDrawable)current;
// invalidate self doesn't seem to trigger the dimension check to be called by imageview.
// are drawable dimensions supposed to be immutable?
imageView.setImageDrawable(null);
return ret;
}
}