package com.koushikdutta.ion; import android.graphics.Bitmap; import android.os.Build; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.util.FileCache; import com.koushikdutta.ion.bitmap.BitmapInfo; import com.koushikdutta.ion.bitmap.LocallyCachedStatus; import com.koushikdutta.ion.bitmap.PostProcess; import com.koushikdutta.ion.bitmap.Transform; import com.koushikdutta.ion.builder.AnimateGifMode; import com.koushikdutta.ion.builder.BitmapFutureBuilder; import com.koushikdutta.ion.builder.Builders; import java.util.ArrayList; import java.util.List; /** * Created by koush on 5/23/13. */ abstract class IonBitmapRequestBuilder implements BitmapFutureBuilder, Builders.Any.BF { private static final SimpleFuture<Bitmap> FUTURE_BITMAP_NULL_URI = new SimpleFuture<Bitmap>() { { setComplete(new NullPointerException("uri")); } }; IonRequestBuilder builder; Ion ion; ArrayList<Transform> transforms; ScaleMode scaleMode; int resizeWidth; int resizeHeight; AnimateGifMode animateGifMode = AnimateGifMode.ANIMATE; boolean deepZoom; ArrayList<PostProcess> postProcess; void reset() { ion = null; transforms = null; scaleMode = null; resizeWidth = 0; resizeHeight = 0; animateGifMode = AnimateGifMode.ANIMATE; builder = null; deepZoom = false; postProcess = null; } public IonBitmapRequestBuilder(IonRequestBuilder builder) { this.builder = builder; ion = builder.ion; } public IonBitmapRequestBuilder(Ion ion) { this.ion = ion; } static void doAnimation(ImageView imageView, Animation animation, int animationResource) { if (imageView == null) return; if (animation == null && animationResource != 0) animation = AnimationUtils.loadAnimation(imageView.getContext(), animationResource); if (animation == null) { imageView.setAnimation(null); return; } imageView.startAnimation(animation); } protected IonRequestBuilder ensureBuilder() { return builder; } @Override public IonBitmapRequestBuilder transform(Transform transform) { if (transform == null) return this; if (transforms == null) transforms = new ArrayList<Transform>(); transforms.add(transform); return this; } @Override public IonBitmapRequestBuilder postProcess(PostProcess postProcess) { if (this.postProcess == null) this.postProcess = new ArrayList<PostProcess>(); this.postProcess.add(postProcess); return transform(new TransformBitmap.PostProcessNullTransform(postProcess.key())); } private String computeDecodeKey() { return computeDecodeKey(builder, resizeWidth, resizeHeight, animateGifMode != AnimateGifMode.NO_ANIMATE, deepZoom); } public static String computeDecodeKey(IonRequestBuilder builder, int resizeWidth, int resizeHeight, boolean animateGif, boolean deepZoom) { // the decode key is a hash of the uri of the image, and any decode // specific flags. this includes: // inSampleSize (determined from resizeWidth/resizeHeight) // gif animation mode // deep zoom String decodeKey = builder.uri; decodeKey += "resize=" + resizeWidth + "," + resizeHeight; if (!animateGif) decodeKey += ":noAnimate"; if (deepZoom) decodeKey += ":deepZoom"; return FileCache.toKeyString(decodeKey); } public void addDefaultTransform() { if (resizeHeight > 0 || resizeWidth > 0) { if (transforms == null) transforms = new ArrayList<Transform>(); transforms.add(0, new DefaultTransform(resizeWidth, resizeHeight, scaleMode)); } else if (scaleMode != null) { throw new IllegalStateException("Must call resize when using " + scaleMode); } } public String computeBitmapKey(String decodeKey) { return computeBitmapKey(decodeKey, transforms); } public static String computeBitmapKey(String decodeKey, List<Transform> transforms) { assert decodeKey != null; // determine the key for this bitmap after all transformations String bitmapKey = decodeKey; if (transforms != null && transforms.size() > 0) { for (Transform transform : transforms) { bitmapKey += transform.key(); } bitmapKey = FileCache.toKeyString(bitmapKey); } return bitmapKey; } @Override public LocallyCachedStatus isLocallyCached() { if (builder.noCache || deepZoom) return LocallyCachedStatus.NOT_CACHED; final String decodeKey = computeDecodeKey(); addDefaultTransform(); String bitmapKey = computeBitmapKey(decodeKey); BitmapInfo info = builder.ion.bitmapCache.get(bitmapKey); // memory cache if (info != null && info.exception == null) return LocallyCachedStatus.CACHED; FileCache fileCache = ion.responseCache.getFileCache(); if (hasTransforms() && fileCache.exists(bitmapKey)) return LocallyCachedStatus.CACHED; if (fileCache.exists(decodeKey)) return LocallyCachedStatus.MAYBE_CACHED; return LocallyCachedStatus.NOT_CACHED; } @Override public BitmapInfo asCachedBitmap() { final String decodeKey = computeDecodeKey(); addDefaultTransform(); String bitmapKey = computeBitmapKey(decodeKey); return builder.ion.bitmapCache.get(bitmapKey); } BitmapFetcher executeCache() { return executeCache(resizeWidth, resizeHeight); } BitmapFetcher executeCache(int sampleWidth, int sampleHeight) { final String decodeKey = computeDecodeKey(); String bitmapKey = computeBitmapKey(decodeKey); // TODO: eliminate this allocation? BitmapFetcher ret = new BitmapFetcher(); ret.bitmapKey = bitmapKey; ret.decodeKey = decodeKey; ret.hasTransforms = hasTransforms(); ret.sampleWidth = sampleWidth; ret.sampleHeight = sampleHeight; ret.builder = builder; ret.transforms = transforms; ret.animateGif = animateGifMode != AnimateGifMode.NO_ANIMATE; ret.deepZoom = deepZoom; ret.postProcess = postProcess; // see if this request can be fulfilled from the cache if (!builder.noCache) { BitmapInfo bitmap = builder.ion.bitmapCache.get(bitmapKey); if (bitmap != null) { ret.info = bitmap; return ret; } } return ret; } @Override public Future<Bitmap> asBitmap() { if (builder.uri == null) { return FUTURE_BITMAP_NULL_URI; } // see if we get something back synchronously addDefaultTransform(); final BitmapFetcher bitmapFetcher = executeCache(); if (bitmapFetcher.info != null) { SimpleFuture<Bitmap> ret = new SimpleFuture<Bitmap>(); ret.setComplete(bitmapFetcher.info.exception, bitmapFetcher.info.bitmap); return ret; } final BitmapInfoToBitmap ret = new BitmapInfoToBitmap(builder.contextReference); AsyncServer.post(Ion.mainHandler, new Runnable() { @Override public void run() { bitmapFetcher.execute(); // we're loading, so let's register for the result. ion.bitmapsPending.add(bitmapFetcher.bitmapKey, ret); } }); return ret; } private void checkNoTransforms(String name) { if (hasTransforms()) { throw new IllegalStateException("Can't apply " + name + " after transform has been called." + name + " is applied to the original resized bitmap."); } } @Override public IonBitmapRequestBuilder centerCrop() { checkNoTransforms("centerCrop"); scaleMode = ScaleMode.CenterCrop; return this; } @Override public IonBitmapRequestBuilder fitXY() { checkNoTransforms("fitXY"); scaleMode = ScaleMode.FitXY; return this; } @Override public IonBitmapRequestBuilder fitCenter() { checkNoTransforms("fitCenter"); scaleMode = ScaleMode.FitCenter; return this; } @Override public IonBitmapRequestBuilder centerInside() { checkNoTransforms("centerInside"); scaleMode = ScaleMode.CenterInside; return this; } @Override public IonBitmapRequestBuilder resize(int width, int height) { // TODO: prevent multiple calls to resize and friends? if (hasTransforms()) { throw new IllegalStateException("Can't apply resize after transform has been called." + "resize is applied to the original bitmap."); } if (deepZoom) throw new IllegalStateException("Can not resize with deepZoom."); resizeWidth = width; resizeHeight = height; return this; } @Override public IonBitmapRequestBuilder resizeWidth(int width) { return resize(width, 0); } @Override public IonBitmapRequestBuilder resizeHeight(int height) { return resize(0, height); } public IonBitmapRequestBuilder smartSize(boolean smartSize) { //don't want to disable device resize if user has already resized the Bitmap. if (resizeWidth > 0 || resizeHeight > 0) throw new IllegalStateException("Can't set smart size after resize has been called."); if (deepZoom) throw new IllegalStateException("Can not smartSize with deepZoom."); if (!smartSize) { resizeWidth = -1; resizeHeight = -1; } else { resizeWidth = 0; resizeHeight = 0; } return this; } @Override public IonBitmapRequestBuilder animateGif(AnimateGifMode mode) { this.animateGifMode = mode; return this; } @Override public IonBitmapRequestBuilder deepZoom() { if (Build.VERSION.SDK_INT < 10) return this; this.deepZoom = true; if (resizeWidth > 0 || resizeHeight > 0) throw new IllegalStateException("Can't deepZoom with resize."); if (hasTransforms()) throw new IllegalStateException("Can't deepZoom with transforms."); resizeWidth = 0; resizeHeight = 0; return this; } boolean hasTransforms() { return transforms != null && transforms.size() > 0; } }