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.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.*; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import com.nineoldandroids.animation.ObjectAnimator; import java.util.Collections; import java.util.HashSet; import java.util.Set; import android.widget.ListView; /** * <pre> * Simplified version of deprecated {@link com.datdo.mobilib.util.MblImageLoader} * * 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. Only load images for currently displayed cells, which is very useful for {@link android.widget.ListView}. * 5. Fading animation when bitmap is loaded successfully. * Override abstract methods to customize this loader. Additional configurations are set via {@link com.datdo.mobilib.util.MblSimpleImageLoader.MblOptions} * * Sample code: * * {@code * public class UserAdapter extends BaseAdapter { * * private MblSimpleImageLoader<User> mUserAvatarLoader = new MblSimpleImageLoader<User>() { * * protected User getItemBoundWithView(View view) { * return (User) view.getTag(); * } * * protected ImageView getImageViewBoundWithView(View view) { * return (ImageView) view.findViewById(R.id.avatar_image_view); * } * * protected String getItemId(User user) { * return user.getId(); * } * * protected void retrieveImage(User user, final MblRetrieveImageCallback cb) { * MblApi.get(item, null, null, Long.MAX_VALUE, true, new MblApiCallback() { * * public void onSuccess(int statusCode, byte[] data) { * cb.onRetrievedByteArray(data); * }; * * public void onFailure(int error, String errorMessage) { * cb.onRetrievedError(); * } * }, null); * } * * protected void onError(ImageView imageView, T item) { * imageView.setImageResource(R.drawable.default_avatar); * } * }; * * public View getView(int pos, View convertView, ViewGroup parent) { * * // ... * * view.setTag(user); * mUserImageLoader.loadImage(view); * * return view; * } * } * } * </pre> * @param <T> class of object bound with an child views of {@link ViewGroup} */ public abstract class MblSimpleImageLoader<T> { /** * <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 getImageViewBoundWithView(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> * Handle error when bitmap loading fails. * </pre> */ protected abstract void onError(ImageView imageView, T item); /** * <pre> * Callback class for {@link #retrieveImage(Object, MblRetrieveImageCallback)} * Choose appropriate method to invoke when bitmap data is successfully loaded, or {@link #onRetrievedError()} when fail * </pre> */ public static interface MblRetrieveImageCallback { public void onRetrievedByteArray(byte[] bmData); public void onRetrievedBitmap(Bitmap bm); public void onRetrievedFile(String path); public void onRetrievedError(); } /** * Additional configurations for {@link MblSimpleImageLoader} */ public static class MblOptions { /** * Interface to customize progress view which indicates image loading */ public static interface MblProgressViewGenerator { public View generate(); } private boolean mSerializeImageLoading = true; private long mDelayedDurationInParallelMode = 500; private boolean mEnableProgressView = true; private boolean mEnableFadingAnimation = true; private MblProgressViewGenerator mProgressViewGenerator; /** * Configure whether progress view is displayed to indicate that image is being loaded. Default TRUE. */ public MblOptions setEnableProgressView(boolean enableProgressView) { mEnableProgressView = enableProgressView; return this; } /** * Configure whether fading animation is played when image is fully loaded and displayed to ImageView. Default TRUE. */ public MblOptions setEnableFadingAnimation(boolean enableFadingAnimation) { mEnableFadingAnimation = enableFadingAnimation; return this; } /** * Configure the progress view to indicate image loading. Default NULL. */ public MblOptions setProgressViewGenerator(MblProgressViewGenerator progressViewGenerator) { mProgressViewGenerator = progressViewGenerator; return this; } /** * Configure whether image is loaded one by one. Default TRUE. */ public MblOptions setSerializeImageLoading(boolean serializeImageLoading) { mSerializeImageLoading = serializeImageLoading; return this; } /** * <pre> * If you call {@link #setSerializeImageLoading(boolean)} with FALSE parameter, image loader will load images in parallel mode, which means multiple images are loaded at the same time. * Anyway, parallel mode will cause a problem: if user want to see last views of a very long {@link ListView}, he needs to scroll very fast to bottom, then all views of {@link ListView} will be loaded, which is very bad for performance. * Therefore, in parallel mode, image loader does not load image for view right away, but wait for a delayed duration before starting it. When use scrolls very fast, views are not loaded because their data are replaced by new data before delayed duration expires. * This method is to customize the delayed duration, in millisecond. Default 500 * </pre> */ public MblOptions setDelayedDurationInParallelMode(long delayedDurationInParallelMode) { mDelayedDurationInParallelMode = delayedDurationInParallelMode; return this; } } private static final String TAG = MblUtils.getTag(MblImageLoader.class); private static final int FRAME_ID = 1430125134; private static LruCache<String, Bitmap> sBitmapCache; private static Set<String> sKeySet = Collections.synchronizedSet(new HashSet<String>()); private static boolean sDoubleCacheSize = false; private MblSerializer mSerializer; private MblOptions mOptions; private int mProgressViewFrameWidth; private int mProgressViewFrameHeight; public MblSimpleImageLoader() { // set default options mOptions = new MblOptions(); // initialize bitmap cache if (sBitmapCache == null) { Context context = MblUtils.getCurrentContext(); int cacheSize = 2 * 1024 * 1024; // 2MB; 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; } sBitmapCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { super.entryRemoved(evicted, key, oldValue, newValue); sKeySet.remove(key); } }; } // initialize serializer mSerializer = new MblSerializer(); } /** * <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 (sBitmapCache != null) { throw new RuntimeException("doubleCacheSize() must be called before first instance of this class being created"); } sDoubleCacheSize = true; } /** * <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) { // check if this method is executed on main thread if (!MblUtils.isMainThread()) { MblUtils.executeOnMainThread(new Runnable() { @Override public void run() { loadImage(view); } }); return; } // check if item is available final T item = getItemBoundWithView(view); if (item == null) { onError(getImageViewBoundWithView(view), getItemBoundWithView(view)); return; } // check if ImageView is available final ImageView imageView = getImageViewBoundWithView(view); if (imageView == null) { onError(getImageViewBoundWithView(view), getItemBoundWithView(view)); return; } // check if bitmap is in cache int w = getImageViewWidth(imageView); int h = getImageViewHeight(imageView); if (isValidSizes(w, h)) { String cacheKey = generateCacheKey(item, w, h); final Bitmap bm = sBitmapCache.get(cacheKey); if (isValidBitmap(bm)) { imageView.setImageBitmap(bm); hideProgressBar(imageView); return; } } imageView.setImageBitmap(null); showProgressBar(imageView); final MblSerializer.Task task = new MblSerializer.Task() { @Override public void run(final Runnable finishCallback) { // check if view is still bound with original item if (!isStillBound(view, item)) { finishCallback.run(); return; } // check if we need to wait until ImageView is fully displayed int w = getImageViewWidth(imageView); int h = getImageViewHeight(imageView); if (!isValidSizes(w, h)) { final Runnable[] timeoutAction = new Runnable[]{null}; final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { MblUtils.removeOnGlobalLayoutListener(imageView, this); MblUtils.getMainThreadHandler().removeCallbacks(timeoutAction[0]); if (isStillBound(view, item)) { loadImage(view); } } }; timeoutAction[0] = new Runnable() { @Override public void run() { MblUtils.removeOnGlobalLayoutListener(imageView, globalLayoutListener); if (isStillBound(view, item)) { loadImage(view); } } }; imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); MblUtils.getMainThreadHandler().postDelayed(timeoutAction[0], 500l); finishCallback.run(); return; } // check if bitmap is in cache final String cacheKey = generateCacheKey(item, w, h); Bitmap bm = sBitmapCache.get(cacheKey); if (isValidBitmap(bm)) { imageView.setImageBitmap(bm); hideProgressBar(imageView); finishCallback.run(); return; } // load bitmap from server/file retrieveImage(item, new MblRetrieveImageCallback() { private void onRetrieved(final Object data) { if (!isStillBound(view, item)) { finishCallback.run(); return; } MblUtils.executeOnAsyncThread(new Runnable() { @Override public void run() { if (!isStillBound(view, item)) { finishCallback.run(); return; } try { int w = getImageViewWidth(imageView); int h = getImageViewHeight(imageView); final Bitmap bm; if (data instanceof byte[]) { bm = MblUtils.loadBitmapMatchSpecifiedSize(w, h, (byte[]) data); } else if (data instanceof Bitmap) { bm = (Bitmap) data; } else if (data instanceof String) { bm = MblUtils.loadBitmapMatchSpecifiedSize(w, h, (String) data); } else { bm = null; } if (isValidBitmap(bm)) { sBitmapCache.put(cacheKey, bm); sKeySet.add(cacheKey); MblUtils.executeOnMainThread(new Runnable() { @Override public void run() { if (isStillBound(view, item)) { imageView.setImageBitmap(bm); hideProgressBar(imageView); animateImageView(imageView); } finishCallback.run(); } }); } else { onRetrievedError(); } } catch (OutOfMemoryError e) { Log.e(TAG, "OutOfMemoryError", e); // release 1/2 of cache size for memory sBitmapCache.trimToSize(sBitmapCache.size() / 2); System.gc(); // error onRetrievedError(); } catch (Throwable t) { Log.e(TAG, "", t); onRetrievedError(); } } }); } @Override public void onRetrievedByteArray(final byte[] bmData) { onRetrieved(bmData); } @Override public void onRetrievedBitmap(Bitmap bm) { onRetrieved(bm); } @Override public void onRetrievedFile(String path) { onRetrieved(path); } @Override public void onRetrievedError() { MblUtils.executeOnMainThread(new Runnable() { @Override public void run() { if (isStillBound(view, item)) { hideProgressBar(imageView); onError(imageView, item); } finishCallback.run(); } }); } }); } }; if (mOptions.mSerializeImageLoading) { mSerializer.run(task); } else { MblUtils.getMainThreadHandler().postDelayed(new Runnable() { @Override public void run() { task.run(new Runnable() { @Override public void run() {} }); } }, mOptions.mDelayedDurationInParallelMode); } } @SuppressWarnings("ResourceType") private void showProgressBar(ImageView imageView) { if (!mOptions.mEnableProgressView) { return; } // get parent view of ImageView ViewGroup parent = (ViewGroup) imageView.getParent(); // check if we need to show progress bar if (parent == null || parent.getId() == FRAME_ID) { return; } // create Frame final FrameLayout frame = new FrameLayout(MblUtils.getCurrentContext()); frame.setId(FRAME_ID); frame.setLayoutParams(imageView.getLayoutParams()); // change ImageView imageView.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); imageView.setVisibility(View.INVISIBLE); // add/remove views int index = parent.indexOfChild(imageView); parent.removeView(imageView); frame.addView(imageView); parent.addView(frame, index); // create ProgressBar and add it to frame final View[] progressView = new View[]{null}; final Runnable addProgressView = new Runnable() { @Override public void run() { progressView[0] = getProgressView(); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( Math.min(MblUtils.pxFromDp(50), mProgressViewFrameWidth / 2), Math.min(MblUtils.pxFromDp(50), mProgressViewFrameHeight / 2)); lp.gravity = Gravity.CENTER; progressView[0].setLayoutParams(lp); frame.addView(progressView[0]); } }; if (mProgressViewFrameWidth > 0 && mProgressViewFrameHeight > 0) { addProgressView.run(); } OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { MblUtils.removeOnGlobalLayoutListener(frame, this); frame.setTag(null); if (frame.getWidth() == 0 || frame.getHeight() == 0) { return; } if (progressView[0] == null) { mProgressViewFrameWidth = frame.getWidth(); mProgressViewFrameHeight = frame.getHeight(); addProgressView.run(); } else if (mProgressViewFrameWidth != frame.getWidth() || mProgressViewFrameHeight != frame.getHeight()) { mProgressViewFrameWidth = frame.getWidth(); mProgressViewFrameHeight = frame.getHeight(); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)progressView[0].getLayoutParams(); lp.width = Math.min(MblUtils.pxFromDp(50), mProgressViewFrameWidth / 2); lp.height = Math.min(MblUtils.pxFromDp(50), mProgressViewFrameHeight / 2); progressView[0].setLayoutParams(lp); } } }; frame.getViewTreeObserver().addOnGlobalLayoutListener(listener); frame.setTag(listener); } @SuppressWarnings("ResourceType") private void hideProgressBar(ImageView imageView) { if (!mOptions.mEnableProgressView) { return; } // get parent view of ImageView ViewGroup temp = (ViewGroup) imageView.getParent(); // check if we need to hide progress bar if (temp == null || temp.getId() != FRAME_ID) { return; } // check if ImageView has valid sizes int w = getImageViewWidth(imageView); int h = getImageViewHeight(imageView); if (!isValidSizes(w, h)) { return; } // frame & parent FrameLayout frame = (FrameLayout) temp; if (frame.getTag() != null) { MblUtils.removeOnGlobalLayoutListener(frame, (OnGlobalLayoutListener)frame.getTag()); frame.setTag(null); } ViewGroup parent = (ViewGroup) frame.getParent(); // change ImageView imageView.setVisibility(View.VISIBLE); imageView.setLayoutParams(frame.getLayoutParams()); // add/remove views int index = parent.indexOfChild(frame); frame.removeView(imageView); parent.removeView(frame); parent.addView(imageView, index); } private String generateCacheKey(T item, int w, int h) { String key = TextUtils.join("#", new Object[]{ generateCacheKey(item), w, h }); return key; } private String generateCacheKey(T item) { return generateCacheKey(item.getClass(), getItemId(item)); } private String generateCacheKey(Class clazz, String id) { String key = TextUtils.join("#", new Object[]{ clazz, id, }); return key; } private int getImageViewWidth(ImageView imageView) { ViewGroup.LayoutParams lp = imageView.getLayoutParams(); if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { return -1; // do not care } else if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT){ return imageView.getWidth(); // 0 or parent 's width } else { return lp.width; // specified width } } private int getImageViewHeight(ImageView imageView) { ViewGroup.LayoutParams lp = imageView.getLayoutParams(); if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { return -1; // do not care } else if (lp.height == ViewGroup.LayoutParams.MATCH_PARENT){ return imageView.getHeight(); // 0 or parent 's height } else { return lp.height; // specified height } } private boolean isValidBitmap(Bitmap bm) { return bm != null && !bm.isRecycled() && bm.getWidth() != 0 && bm.getHeight() != 0; } private boolean isValidSizes(int w, int h) { return w != 0 || h != 0; } private boolean isStillBound(View view, T item) { return item == getItemBoundWithView(view); } private void animateImageView(ImageView imageView) { if (!mOptions.mEnableFadingAnimation) { return; } ObjectAnimator.ofFloat(imageView, "alpha", 0, 1) .setDuration(250) .start(); } /** * <pre> * Remove bitmap of item from memory cache. Bitmap will be reloaded when it is required. * </pre> * @param item */ public void invalidate(T item) { invalidate(item.getClass(), getItemId(item)); } /** * Similar to {@link #invalidate(Object)} * @param clazz class of item * @param id id of item (similar to id retrieved by {@link #getItemId(Object)}) */ public void invalidate(final Class clazz, final String id) { MblUtils.executeOnMainThread(new Runnable() { @Override public void run() { String cacheKey = generateCacheKey(clazz, id); Set<String> keys = new HashSet<String>(sKeySet); for (String key : keys) { if (key.startsWith(cacheKey)) { sBitmapCache.remove(key); } } } }); } /** * Invalidate all item of specified class */ public void invalidate(final Class clazz) { invalidate(clazz, ""); } /** * Get options for this image loader. */ public MblOptions getOptions() { return mOptions; } /** * Set options for this image loader. */ public MblSimpleImageLoader setOptions(MblOptions options) { if (options != null) { mOptions = options; } else { mOptions = new MblOptions(); } return this; } private View getProgressView() { if (mOptions.mProgressViewGenerator != null) { return mOptions.mProgressViewGenerator.generate(); } else { ProgressBar progress = new ProgressBar(MblUtils.getCurrentContext()); progress.setIndeterminate(true); return progress; } } }