package com.koushikdutta.ion;
import android.content.res.Resources;
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.Drawable;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.animation.Animation;
import android.widget.ImageView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.http.ResponseCacheMiddleware;
import com.koushikdutta.ion.bitmap.BitmapInfo;
import java.lang.ref.WeakReference;
/**
* Created by koush on 6/8/13.
*/
class IonDrawable extends Drawable {
private Paint paint;
private BitmapInfo info;
private int placeholderResource;
private Drawable placeholder;
private int errorResource;
private Drawable error;
private Resources resources;
private int loadedFrom;
private IonDrawableCallback callback;
private boolean disableFadeIn;
private int resizeWidth;
private int resizeHeight;
private Ion ion;
public IonDrawable cancel() {
requestCount++;
return this;
}
public IonDrawable ion(Ion ion) {
this.ion = ion;
return this;
}
public SimpleFuture<ImageView> getFuture() {
return callback.imageViewFuture;
}
public IonDrawable setDisableFadeIn(boolean disableFadeIn) {
this.disableFadeIn = disableFadeIn;
return this;
}
public IonDrawable setInAnimation(Animation inAnimation, int inAnimationResource) {
callback.inAnimation = inAnimation;
callback.inAnimationResource = inAnimationResource;
return this;
}
// 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 WeakReference<ImageView> imageViewRef;
private String bitmapKey;
private SimpleFuture<ImageView> imageViewFuture = new SimpleFuture<ImageView>();
private Animation inAnimation;
private int inAnimationResource;
private int requestId;
public IonDrawableCallback(IonDrawable drawable, ImageView imageView) {
ionDrawableRef = new WeakReference<IonDrawable>(drawable);
imageViewRef = new WeakReference<ImageView>(imageView);
}
@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
ImageView imageView = imageViewRef.get();
if (imageView == null)
return;
IonDrawable drawable = ionDrawableRef.get();
if (drawable == null)
return;
if (imageView.getDrawable() != drawable)
return;
// see if the ImageView is still waiting for the same request
if (drawable.requestCount != requestId)
return;
imageView.setImageDrawable(null);
drawable.setBitmap(result, result.loadedFrom);
imageView.setImageDrawable(drawable);
IonBitmapRequestBuilder.doAnimation(imageView, inAnimation, inAnimationResource);
if (!IonRequestBuilder.checkContext(imageView.getContext())) {
imageViewFuture.cancel();
return;
}
imageViewFuture.setComplete(e, imageView);
}
}
int requestCount;
public void register(Ion ion, String bitmapKey) {
callback.requestId = ++requestCount;
String previousKey = callback.bitmapKey;
if (TextUtils.equals(previousKey, bitmapKey))
return;
callback.bitmapKey = bitmapKey;
ion.bitmapsPending.add(bitmapKey, callback);
if (previousKey == 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(previousKey, callback)) {
// find out who owns this thing, to see if it is a candidate for removal
Object owner = ion.bitmapsPending.tag(previousKey);
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);
}
if (owner instanceof DeferredLoadBitmap) {
DeferredLoadBitmap defer = (DeferredLoadBitmap)owner;
ion.bitmapsPending.remove(defer.key);
}
}
ion.processDeferred();
}
private static final int DEFAULT_PAINT_FLAGS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
public IonDrawable(Resources resources, ImageView imageView) {
this.resources = resources;
paint = new Paint(DEFAULT_PAINT_FLAGS);
callback = new IonDrawableCallback(this, imageView);
}
int currentFrame;
private boolean invalidateScheduled;
private int textureDim;
private int maxLevel;
public IonDrawable setBitmap(BitmapInfo info, int loadedFrom) {
this.loadedFrom = loadedFrom;
requestCount++;
if (this.info == info)
return this;
invalidateSelf();
this.info = info;
currentFrame = 0;
invalidateScheduled = false;
if (info == null) {
callback.bitmapKey = 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;
}
callback.bitmapKey = info.key;
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 == this.error) || (resource != 0 && resource == errorResource))
return this;
this.errorResource = resource;
this.error = drawable;
invalidateSelf();
return this;
}
public IonDrawable setPlaceholder(int resource, Drawable drawable) {
if ((drawable != null && drawable == this.placeholder) || (resource != 0 && resource == placeholderResource))
return this;
this.placeholderResource = resource;
this.placeholder = drawable;
invalidateSelf();
return this;
}
@Override
public void setFilterBitmap(boolean filter) {
paint.setFilterBitmap(filter);
invalidateSelf();
}
@Override
public void setDither(boolean dither) {
paint.setDither(dither);
invalidateSelf();
}
private Drawable tryGetErrorResource() {
if (error != null)
return error;
if (errorResource == 0)
return null;
return error = resources.getDrawable(errorResource);
}
@Override
public int getIntrinsicWidth() {
if (info != null) {
if (info.decoder != null)
return info.originalSize.x;
if (info.bitmaps != null)
return info.bitmaps[0].getScaledWidth(resources.getDisplayMetrics().densityDpi);
}
if (resizeWidth > 0)
return resizeWidth;
if (info != null) {
Drawable error = tryGetErrorResource();
if (error != null)
return error.getIntrinsicWidth();
}
if (placeholder != null) {
return placeholder.getIntrinsicWidth();
} else if (placeholderResource != 0) {
Drawable d = resources.getDrawable(placeholderResource);
assert d != null;
return d.getIntrinsicWidth();
}
return -1;
}
@Override
public int getIntrinsicHeight() {
if (info != null) {
if (info.decoder != null)
return info.originalSize.y;
if (info.bitmaps != null)
return info.bitmaps[0].getScaledHeight(resources.getDisplayMetrics().densityDpi);
}
if (resizeHeight > 0)
return resizeHeight;
if (info != null) {
if (error != null) {
return error.getIntrinsicHeight();
} else if (errorResource != 0) {
Drawable d = resources.getDrawable(errorResource);
assert d != null;
return d.getIntrinsicHeight();
}
}
if (placeholder != null) {
return placeholder.getIntrinsicHeight();
} else if (placeholderResource != 0) {
Drawable d = resources.getDrawable(placeholderResource);
assert d != null;
return d.getIntrinsicHeight();
}
return -1;
}
public static final long FADE_DURATION = 200;
private Runnable invalidate = new Runnable() {
@Override
public void run() {
invalidateScheduled = false;
currentFrame++;
invalidateSelf();
}
};
private static final double LOG_2 = Math.log(2);
private static final int TILE_DIM = 256;
FutureCallback<BitmapInfo> tileCallback = new FutureCallback<BitmapInfo>() {
@Override
public void onCompleted(Exception e, BitmapInfo result) {
invalidateSelf();
}
};
@Override
public void draw(Canvas canvas) {
if (info == null) {
if (placeholder == null && placeholderResource != 0)
placeholder = resources.getDrawable(placeholderResource);
if (placeholder != null) {
placeholder.setBounds(getBounds());
placeholder.draw(canvas);
}
return;
}
if (info.drawTime == 0)
info.drawTime = SystemClock.uptimeMillis();
long destAlpha = 0xFF;
if(!disableFadeIn) {
destAlpha = ((SystemClock.uptimeMillis() - info.drawTime) << 8) / FADE_DURATION;
destAlpha = Math.min(destAlpha, 0xFF);
}
if (destAlpha != 255) {
if (placeholder == null && placeholderResource != 0)
placeholder = resources.getDrawable(placeholderResource);
if (placeholder != null) {
placeholder.setBounds(getBounds());
placeholder.draw(canvas);
}
}
if (info.decoder != null) {
// 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.bitmaps != null && info.bitmaps[0] != null) {
canvas.drawBitmap(info.bitmaps[0], 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 = ResponseCacheMiddleware.toKeyString(info.key + "," + level + "," + x + "," + y);
BitmapInfo tile = ion.bitmapCache.get(tileKey);
if (tile != null && tile.bitmaps != null) {
// render it
// System.out.println("bitmap is: " + tile.bitmaps[0].getWidth() + "x" + tile.bitmaps[0].getHeight());
canvas.drawBitmap(tile.bitmaps[0], 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 = ResponseCacheMiddleware.toKeyString(info.key + "," + parentLevel + "," + parentX + "," + parentY);
tile = ion.bitmapCache.get(tileKey);
if (tile != null && tile.bitmaps != 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.bitmaps == 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.bitmaps[0], sourceRect, texRect, paint);
if (DEBUG_ZOOM) {
paint.setColor(Color.RED);
paint.setAlpha(0x80);
canvas.drawRect(texRect, paint);
paint.setAlpha(0xFF);
}
}
}
}
else if (info.bitmaps != null) {
paint.setAlpha((int)destAlpha);
canvas.drawBitmap(info.bitmaps[currentFrame % info.bitmaps.length], null, getBounds(), paint);
paint.setAlpha(0xFF);
if (info.delays != null) {
int delay = info.delays[currentFrame % info.delays.length];
if (!invalidateScheduled) {
invalidateScheduled = true;
unscheduleSelf(invalidate);
scheduleSelf(invalidate, SystemClock.uptimeMillis() + Math.max(delay, 100));
}
}
}
else {
Drawable error = tryGetErrorResource();
if (error != null) {
error.setAlpha((int)destAlpha);
error.setBounds(getBounds());
error.draw(canvas);
error.setAlpha(0xFF);
}
}
if (destAlpha != 255)
invalidateSelf();
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;
switch (loadedFrom) {
case Loader.LoaderEmitter.LOADED_FROM_CACHE:
sourceColor = Color.CYAN;
break;
case Loader.LoaderEmitter.LOADED_FROM_CONDITIONAL_CACHE:
sourceColor = Color.YELLOW;
break;
case Loader.LoaderEmitter.LOADED_FROM_MEMORY:
sourceColor = Color.GREEN;
break;
default:
sourceColor = Color.RED;
break;
}
paint.setColor(sourceColor);
canvas.drawRect(0, -9, 6.5f, 9, paint);
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return (info == null || info.bitmaps == null || info.bitmaps[0].hasAlpha() || paint.getAlpha() < 255) ?
PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
}
static IonDrawable getOrCreateIonDrawable(ImageView imageView) {
Drawable current = imageView.getDrawable();
IonDrawable ret;
if (current == null || !(current instanceof IonDrawable))
ret = new IonDrawable(imageView.getResources(), imageView);
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;
}
}