package com.datdo.mobilib.util;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageView;
import android.widget.ListView;
import com.nineoldandroids.animation.ObjectAnimator;
import junit.framework.Assert;
import java.util.Vector;
/**
* <pre>
* DEPRECATED. Should use {@link com.datdo.mobilib.util.MblSimpleImageLoader} instead.
*
* Smart loader to display images for child views in a {@link ViewGroup}.
* Features of this loader:
* 1. Load images sequentially.
* 2. Automatically scale images to match sizes of {@link ImageView}.
* 3. Cache images using {@link LruCache}.
* 4. Prioritize loading by last-recently-displayed, which means {@link ImageView} being displayed has higher priority than {@link ImageView} which is no longer displayed.
* This feature is very useful when user scrolls a {@link ListView}.
* Override abstract methods use customize this loader.
*
* Here is sample usage of this loader:
*
* <code>
* public class MyAdapter extends BaseAdapter {
*
* private MblImageLoader{@literal <}Item> mItemImageLoader = new MblImageLoader{@literal <}Item>() {
* // override all abstract methods
* // ...
* };
*
* {@literal @}Override
* public View getView(int pos, View convertView, ViewGroup parent) {
* // create or update view
* // ...
*
* mItemImageLoader.loadImage(view);
*
* return view;
* }
* }
* </code>
* </pre>
* @param <T> class of object bound with an child views of {@link ViewGroup}
*/
@Deprecated
public abstract class MblImageLoader<T> {
/**
* <pre>
* Check condition to load image for an item.
* </pre>
* @return true if should load image for item
*/
protected abstract boolean shouldLoadImageForItem(T item);
/**
* <pre>
* Get resource id of default image for items those are not necessary to load image
* </pre>
* @see #shouldLoadImageForItem(Object)
*/
protected abstract int getDefaultImageResource(T item);
/**
* <pre>
* Get resource id of default image for items those fails to load image
* </pre>
*/
protected abstract int getErrorImageResource(T item);
/**
* <pre>
* Get resource id of default image for items those are being loaded
* </pre>
*/
protected abstract int getLoadingIndicatorImageResource(T item);
/**
* <pre>
* Get data object bound with each child view.
* </pre>
*/
protected abstract T getItemBoundWithView(View view);
/**
* <pre>
* Extract {@link ImageView} used to display image from each child view
* </pre>
*/
protected abstract ImageView getImageViewFromView(View view);
/**
* <pre>
* Specify an ID for each data object. The ID is used for caching so please make it unique throughout the app.
* </pre>
*/
protected abstract String getItemId(T item);
/**
* <pre>
* Do your own image loading here (from HTTP/HTTPS, from file, etc...).
* This method is always invoked in main thread. Therefore, it is strongly recommended to do the loading asynchronously.
* </pre>
* @param item
* @param cb call method of this callback when you finished the loading
*/
protected abstract void retrieveImage(T item, MblRetrieveImageCallback cb);
/**
* <pre>
* Callback class for {@link MblImageLoader#retrieveImage(Object, MblRetrieveImageCallback)}
* When loading image finished, call 1 of 2 methods depending on returned data.
* If loading image failed, call any method with NULL argument.
* </pre>
*/
public static interface MblRetrieveImageCallback {
public void onRetrievedByteArray(byte[] bmData);
public void onRetrievedBitmap(Bitmap bm);
public void onRetrievedFile(String path);
}
private static final String TAG = MblUtils.getTag(MblImageLoader.class);
private static final int DEFAULT_CACHE_SIZE = 2 * 1024 * 1024; // 2MB
private static final String CACHE_KEY_SEPARATOR = "#";
private static final class MblCachedImageData {
public int resId = 0;
public Bitmap bitmap;
protected MblCachedImageData(int resId, Bitmap bitmap) {
Assert.assertTrue(resId > 0 || bitmap != null);
Assert.assertFalse(resId > 0 && bitmap != null);
this.resId = resId;
this.bitmap = bitmap;
}
}
private final Vector<Pair<T, View>> mQueue = new Vector<Pair<T,View>>();
private boolean mLoadingImage = false;
private static LruCache<String, MblCachedImageData> sStringPictureLruCache;
private static boolean sDoubleCacheSize = false;
private static void initCacheIfNeeded() {
if (sStringPictureLruCache == null) {
Context context = MblUtils.getCurrentContext();
int cacheSize = DEFAULT_CACHE_SIZE;
if (context != null) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
cacheSize = memoryClassBytes / 8;
}
if (sDoubleCacheSize) {
cacheSize = cacheSize * 2;
}
sStringPictureLruCache = new LruCache<String, MblCachedImageData>(cacheSize) {
@Override
protected void entryRemoved(boolean evicted, String key, MblCachedImageData oldValue, MblCachedImageData newValue) {
Log.v(TAG, "Image cache size: " + size());
}
@Override
protected int sizeOf(String key, MblCachedImageData value) {
if (value.bitmap != null) {
Bitmap bm = value.bitmap;
return bm.getRowBytes() * bm.getHeight();
} else if (value.resId > 0) {
return 4;
}
return 0;
}
};
}
}
private static MblCachedImageData remove(String key) {
synchronized (sStringPictureLruCache) {
return sStringPictureLruCache.remove(key);
}
}
private static void put(String key, MblCachedImageData val) {
synchronized (sStringPictureLruCache) {
sStringPictureLruCache.put(key, val);
Log.v(TAG, "Image cache size: " + sStringPictureLruCache.size());
}
}
private static MblCachedImageData get(String key) {
return sStringPictureLruCache.get(key);
}
public MblImageLoader() {
initCacheIfNeeded();
}
/**
* <pre>
* Double memory-cache 's size to increase number of bitmap being kept in memory.
* Call this method before creating any instance.
* </pre>
*/
public static void doubleCacheSize() {
if (sStringPictureLruCache != null) {
throw new RuntimeException("doubleCacheSize() must be called before first instance of this class being created");
}
sDoubleCacheSize = true;
}
/**
* <pre>
* Stop loading. This methods should be called when the view did disappear.
* </pre>
*/
public void stop() {
synchronized (mQueue) {
mQueue.clear();
}
}
/**
* <pre>
* Request loading for a child view.
* The loading request is put into a queue and executed sequentially.
* </pre>
* @param view the child view for which you want to load image
*/
public void loadImage(final View view) {
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
T item = getItemBoundWithView(view);
final ImageView imageView = getImageViewFromView(view);
if (item == null || imageView == null) return;
if (!shouldLoadImageForItem(item)) {
setImageViewResource(imageView, getDefaultImageResource(item));
return;
}
int w = getImageViewWidth(imageView);
int h = getImageViewHeight(imageView);
if (w == 0 && h == 0) {
final Runnable[] timeoutAction = new Runnable[] { null };
final OnGlobalLayoutListener globalLayoutListener = new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
MblUtils.removeOnGlobalLayoutListener(imageView, this);
MblUtils.getMainThreadHandler().removeCallbacks(timeoutAction[0]);
loadImage(view);
}
};
timeoutAction[0] = new Runnable() {
@Override
public void run() {
MblUtils.removeOnGlobalLayoutListener(imageView, globalLayoutListener);
loadImage(view);
}
};
imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
MblUtils.getMainThreadHandler().postDelayed(timeoutAction[0], 500l);
return;
}
String fullCacheKey = getFullCacheKey(item, w, h);
MblCachedImageData pic = get(fullCacheKey);
if(pic != null) {
if (pic.bitmap != null) {
Bitmap bm = pic.bitmap;
if (!bm.isRecycled()) {
imageView.setImageBitmap(bm);
} else {
remove(fullCacheKey);
handleBitmapUnavailable(view, imageView, item);
}
} else if (pic.resId > 0) {
setImageViewResource(imageView, pic.resId);
}
} else {
handleBitmapUnavailable(view, imageView, item);
}
}
});
}
private void handleBitmapUnavailable(View view, ImageView imageView, T item) {
setImageViewResource(imageView, getLoadingIndicatorImageResource(item));
synchronized (mQueue) {
mQueue.add(new Pair<T, View>(item, view));
}
loadNextImage();
}
private Pair<T, View> getNextPair() {
synchronized (mQueue) {
if (mQueue.isEmpty()) {
return null;
} else {
return mQueue.remove(0);
}
}
}
private boolean isItemBoundWithView(T item, View view) {
// if item and view 's item are same object, just return TRUE
T viewItem = getItemBoundWithView(view);
if (item != null && item == viewItem) {
return true;
}
// otherwise, compare id
String id1 = item != null ? getItemId(item) : null;
String id2 = viewItem != null ? getItemId(viewItem) : null;
return id1 != null && id2 != null && TextUtils.equals(id1, id2);
}
private void loadNextImage() {
if (mLoadingImage) return;
Pair<T, View> pair = getNextPair();
if (pair == null) return;
final T item = pair.first;
final View view = pair.second;
final ImageView imageView = getImageViewFromView(view);
if (!isItemBoundWithView(item, view)) {
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
loadNextImage();
}
});
return;
}
if (!shouldLoadImageForItem(item)) {
setImageViewResource(imageView, getDefaultImageResource(item));
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
loadNextImage();
}
});
return;
}
final String fullCacheKey = getFullCacheKey(
item,
getImageViewWidth(imageView),
getImageViewHeight(imageView));
MblCachedImageData pic = get(fullCacheKey);
if(pic != null) {
boolean isSet = false;
if (pic.bitmap != null) {
Bitmap bm = pic.bitmap;
if (!bm.isRecycled()) {
imageView.setImageBitmap(bm);
isSet = true;
} else {
remove(fullCacheKey);
}
} else if (pic.resId > 0) {
setImageViewResource(imageView, pic.resId);
isSet = true;
}
if (isSet) {
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
loadNextImage();
}
});
return;
}
}
mLoadingImage = true;
final boolean isNetworkConnected = MblUtils.isNetworkConnected();
retrieveImage(item, new MblRetrieveImageCallback() {
@Override
public void onRetrievedByteArray(final byte[] bmData) {
if (MblUtils.isEmpty(bmData)) {
handleBadReturnedBitmap(item, view, fullCacheKey, !isNetworkConnected);
} else {
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
try {
int w = getImageViewWidth(imageView);
int h = getImageViewHeight(imageView);
Bitmap bm = MblUtils.loadBitmapMatchSpecifiedSize(w, h, bmData);
if (bm == null) {
handleBadReturnedBitmap(item, view, fullCacheKey, !isNetworkConnected);
} else {
Log.d(TAG, "Scale bitmap: w=" + w + ", h=" + h +
", bm.w=" + bm.getWidth() + ", bm.h=" + bm.getHeight());
handleGoodReturnedBitmap(item, view, fullCacheKey, bm);
}
} catch (OutOfMemoryError e) {
Log.e(TAG, "OutOfMemoryError", e);
handleOutOfMemory(item, view, fullCacheKey);
}
}
});
}
}
@Override
public void onRetrievedFile(final String path) {
if (MblUtils.isEmpty(path)) {
handleBadReturnedBitmap(item, view, fullCacheKey, !isNetworkConnected);
} else {
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
try {
int w = getImageViewWidth(imageView);
int h = getImageViewHeight(imageView);
Bitmap bm = MblUtils.loadBitmapMatchSpecifiedSize(w, h, path);
if (bm == null) {
handleBadReturnedBitmap(item, view, fullCacheKey, !isNetworkConnected);
} else {
Log.d(TAG, "Scale bitmap: w=" + w + ", h=" + h +
", bm.w=" + bm.getWidth() + ", bm.h=" + bm.getHeight());
handleGoodReturnedBitmap(item, view, fullCacheKey, bm);
}
} catch (OutOfMemoryError e) {
Log.e(TAG, "OutOfMemoryError", e);
handleOutOfMemory(item, view, fullCacheKey);
}
}
});
}
}
@Override
public void onRetrievedBitmap(Bitmap bm) {
if (bm == null) {
handleBadReturnedBitmap(item, view, fullCacheKey, !isNetworkConnected);
} else {
handleGoodReturnedBitmap(item, view, fullCacheKey, bm);
}
}
});
}
private void handleGoodReturnedBitmap(final T item, final View view, final String fullCacheKey, final Bitmap bm) {
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
put(fullCacheKey, new MblCachedImageData(0, bm));
postLoadImageForItem(item, view);
}
});
}
private void handleBadReturnedBitmap(final T item, final View view, final String fullCacheKey, final boolean shouldRetry) {
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
int errorImageRes = getErrorImageResource(item);
if (errorImageRes > 0) {
put(fullCacheKey, new MblCachedImageData(errorImageRes, null));
}
postLoadImageForItem(item, view);
// failed due to network disconnect -> should try to load later
if (shouldRetry) {
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
remove(fullCacheKey);
}
});
}
}
});
}
private void handleOutOfMemory(final T item, final View view, final String fullCacheKey) {
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
// release 1/2 of cache size for memory
synchronized (sStringPictureLruCache) {
sStringPictureLruCache.trimToSize(sStringPictureLruCache.size()/2);
}
System.gc();
handleBadReturnedBitmap(item, view, fullCacheKey, true);
}
});
}
private void postLoadImageForItem(final T item, final View view) {
if (isItemBoundWithView(item, view)) {
loadImage(view);
animateImageView(getImageViewFromView(view));
}
// run loadNextImage() using "post" to prevent deep recursion
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
mLoadingImage = false;
loadNextImage();
}
});
}
private void setImageViewResource(ImageView imageView, int resId) {
if (resId <= 0) {
imageView.setImageBitmap(null);
} else {
imageView.setImageResource(resId);
}
}
private String getFullCacheKey(T item, int w, int h) {
String key = TextUtils.join(CACHE_KEY_SEPARATOR, new Object[] {
item.getClass().getSimpleName(),
MblUtils.md5(getItemId(item)),
w,
h
});
return key;
}
private int getImageViewWidth(ImageView imageView) {
LayoutParams lp = imageView.getLayoutParams();
if (lp.width == LayoutParams.WRAP_CONTENT) {
return -1; // do not care
} else if (lp.width == LayoutParams.MATCH_PARENT){
return imageView.getWidth(); // 0 or parent 's width
} else {
return lp.width; // specified width
}
}
private int getImageViewHeight(ImageView imageView) {
LayoutParams lp = imageView.getLayoutParams();
if (lp.height == LayoutParams.WRAP_CONTENT) {
return -1; // do not care
} else if (lp.height == LayoutParams.MATCH_PARENT){
return imageView.getHeight(); // 0 or parent 's height
} else {
return lp.height; // specified height
}
}
protected void animateImageView(ImageView imageView) {
// animation alpha 0 -> 1
ObjectAnimator.ofFloat(imageView, "alpha", 0, 1)
.setDuration(250)
.start();
}
}