package com.xiaomi.xms.sales.loader; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Handler; import android.os.Message; import android.support.v4.util.LruCache; import android.widget.ImageView; import com.xiaomi.xms.sales.model.Image; import com.xiaomi.xms.sales.request.Request; import com.xiaomi.xms.sales.request.RequestStream; import com.xiaomi.xms.sales.util.Coder; import com.xiaomi.xms.sales.util.Device; import com.xiaomi.xms.sales.util.LogUtil; import com.xiaomi.xms.sales.widget.SelfBindView; import com.xiaomi.xms.sales.widget.gallery.ZoomImageView; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Encapsulate an image and all its binding views. NOTE: Different views maybe * display the same image. */ class ImageLoaderItem { public Image image; public ArrayList<ImageView> views; public ImageLoaderItem(Image image, ArrayList<ImageView> views) { this.image = image; this.views = views; } /** * Add a view into binding array. If the view is already in the array, do * nothing. */ public void add(ImageView view) { for (ImageView v : views) { if (v == view) return; } views.add(view); } /** * Only for debug. */ @Override public String toString() { String to = image.toString(); to += "=>["; for (ImageView v : views) { to += "" + v.hashCode() + ","; } to += "]"; return to; } } /** * A hash map of loading items. */ class ImageLoaderItemMap { private ConcurrentHashMap<Image, ImageLoaderItem> mData; public ImageLoaderItemMap() { mData = new ConcurrentHashMap<Image, ImageLoaderItem>(); } /** * Add an image and its binding view into database. */ public void add(Image image, ImageView view) { if (!mData.containsKey(image)) { mData.put(image, new ImageLoaderItem(image, new ArrayList<ImageView>())); } mData.get(image).add(view); } public void remove(Image image) { if (image == null) return; mData.remove(image); } public ImageLoaderItem get(Image image) { return mData.get(image); } public Iterator<Image> iterator() { return mData.keySet().iterator(); } public boolean contains(Image image) { return mData.contains(image); } /** * Only for debug. */ @Override public String toString() { String to = new String(); Iterator<Image> it = iterator(); while (it.hasNext()) { Image image = it.next(); ImageLoaderItem item = get(image); to += item.toString(); to += ", "; } return to; } } public class ImageLoader implements Handler.Callback { private static final String TAG = "ImageLoader"; private static ImageLoader sLoader; private Context mContext; // loading images thread pool private ExecutorService mExecutor; private static final int THREAD_POOL_COUNT = 6; private volatile boolean mPauseLoading; private Handler mUIHandler; private static final int MESSAGE_REQUEST_LOADED = 1; // LRU cache size private static final int BITMAP_CACHE_SIZE = 10 * 1024 * 1024; // LRU cache 中缓存的Bitmap信息 private static class BitmapHolder { private static final int NEEDED = 0; private static final int LOADED = 1; private static final int LOADING = 2; int mState; // 当前的加载状态 Reference<Bitmap> mBitmapRef; // 缓存的Bitmap信息 byte[] mBytes; } // 缓存图片的LRU cache private final LruCache<Image, BitmapHolder> mBitmapCache; private Byte mBitmapCacheLock = new Byte((byte) 0); // 请求加载数据的Request private final ImageLoaderItemMap mPendingRequest; /** * Record the latest image that a view is bound to. NOTE: A view may be * bound to different images in time line. Only the latest image is bound to * the view. This is usually happened at view-reuse in list view. */ private final ConcurrentHashMap<ImageView, Image> mLatestRequest; /** * Must be called when the application is launched. */ public static void init(Context context) { if (sLoader == null) { sLoader = new ImageLoader(context); } } public synchronized static ImageLoader getInstance() { return sLoader; } /** * Load the image from URL and bind to the specified view. * * @param defaultImageRes A local resource ID as the default image. The * default image will be shown before the actual image is loaded. */ public void loadImage(ImageView view, Image image, int defaultImageRes) { loadImage(view, image, null, defaultImageRes); } public void loadImage(ImageView view, Image image, Bitmap defaultBitmap) { loadImage(view, image, defaultBitmap, 0); } /** * Load image from local cache. If the image is in local cache, return the * image. Otherwise, return null. The method is time consuming, so do NOT * call it from synchronized methods. */ public Bitmap syncLoadLocalImage(Image image, boolean fetchRemote) { return decodeBitmap(loadImage(image, fetchRemote)); } private ImageLoader(Context context) { mBitmapCache = new LruCache<Image, ImageLoader.BitmapHolder>(BITMAP_CACHE_SIZE); mPendingRequest = new ImageLoaderItemMap(); mLatestRequest = new ConcurrentHashMap<ImageView, Image>(); mUIHandler = new Handler(this); mExecutor = Executors.newFixedThreadPool(THREAD_POOL_COUNT); mContext = context; } private void loadImage(ImageView view, Image image, Bitmap defaultBitmap, int defaultImageRes) { if (image != null && image.isValid()) { mLatestRequest.put(view, image); int loadState = loadCachedPhoto(view, image); LogUtil.d(TAG, "loadImage " + image.toString() + ", state:" + loadState); if (loadState == BitmapHolder.LOADED) { mLatestRequest.remove(view); return; } // if not loaded, bind to the default image first bindDefaultImage(view, defaultBitmap, defaultImageRes); mPendingRequest.add(image, view); // if needed, load now if (loadState == BitmapHolder.NEEDED) { requestLoading(image); } } else { bindDefaultImage(view, defaultBitmap, defaultImageRes); } } /** * Look up image in local cache. If find it, bind it to the view right now. * Otherwise, tell the caller that further loading is needed. */ private int loadCachedPhoto(ImageView view, Image image) { synchronized (mBitmapCacheLock) { BitmapHolder holder = null; holder = mBitmapCache.get(image); if (holder != null && holder.mState == BitmapHolder.LOADED) { if (holder.mBitmapRef.get() == null) { inflateBitmap(holder); } bindImage(view, holder.mBitmapRef.get(), image); return BitmapHolder.LOADED; } else if (holder != null) { return holder.mState; } } return BitmapHolder.NEEDED; } private void inflateBitmap(BitmapHolder holder) { try { if (holder.mBytes != null) { Bitmap bitmap = decodeBitmap(holder.mBytes); holder.mBitmapRef = new SoftReference<Bitmap>(bitmap); bitmap = null; } else { LogUtil.e(TAG, "The holder's bytes should not be null"); } } catch (OutOfMemoryError e) { e.printStackTrace(); } finally { if (holder.mBitmapRef == null) { holder.mBitmapRef = new SoftReference<Bitmap>(null); } } } private Bitmap decodeBitmap(byte[] bytes) { if (bytes != null && bytes.length > 0) { // First decode with inJustDecodeBounds=true to check dimensions BitmapFactory.Options ops = new BitmapFactory.Options(); ops.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(bytes, 0, bytes.length, ops); // Calculate inSampleSize ops.inSampleSize = calculateInSampleSize(ops, Device.DISPLAY_WIDTH, Device.DISPLAY_HEIGHT); // Decode bitmap with inSampleSize set ops.inJustDecodeBounds = false; ops.inPurgeable = true; return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, ops); } return null; } private int calculateInSampleSize(BitmapFactory.Options opt, int reqWidth, int reqHeight) { // Raw height and width of image final int height = opt.outHeight; final int width = opt.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int heightRatio = height / reqHeight; final int widthRatio = width / reqWidth; // Choose the smallest ratio as inSampleSize value, this will // guarantee a final image with both dimensions larger than or equal // to the requested height and width. inSampleSize = Math.min(heightRatio, widthRatio); } return inSampleSize; } private boolean requestLoading(Image image) { if (!mPauseLoading) { mExecutor.execute(new LoadImageRunnable(image)); return true; } return false; } public void pauseLoading() { mPauseLoading = true; } public void resumeLoading() { mPauseLoading = false; Iterator<Image> iterator = mPendingRequest.iterator(); while (iterator.hasNext()) { requestLoading(iterator.next()); } } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_REQUEST_LOADED: { final Image image = (Image) msg.obj; if (image == null) return false; synchronized (mBitmapCacheLock) { BitmapHolder holder = mBitmapCache.get(image); // Check whether the holder is in the cache since the // cached data maybe garbaged. if (holder != null && holder.mState == BitmapHolder.LOADED) { if (holder.mBitmapRef.get() == null) { inflateBitmap(holder); } // find the views which need the image ImageLoaderItem item = mPendingRequest.get(image); if (item == null) return true; for (ImageView v : item.views) { // image should be the latest request of the view if (image.equals(mLatestRequest.get(v))) { bindImage(v, holder.mBitmapRef.get(), image); mLatestRequest.remove(v); LogUtil.d(TAG, "handleMessage: view " + v.hashCode() + " bind to " + image.toString()); } } mPendingRequest.remove(image); } else { // If the holder object is garbaged, reload the image. LogUtil.d(TAG, "handleMessage:image " + image + " was garbaged"); requestLoading(image); } } break; } default: break; } return false; } private class LoadImageRunnable implements Runnable { private Image image; public LoadImageRunnable(Image image) { this.image = image; } @Override public void run() { synchronized (mBitmapCacheLock) { // 如果该图片已经在加载中或者加载完毕,那么直接返回 BitmapHolder holder = mBitmapCache.get(image); if (holder != null && holder.mState != BitmapHolder.NEEDED) { return; } // 如果该图片尚未开始加载,那么把它加入正在加载队列 if (holder == null) { holder = new BitmapHolder(); } holder.mState = BitmapHolder.LOADING; mBitmapCache.put(image, holder); LogUtil.d(TAG, "RunnableLoadImage:" + image.toString() + " cached to be loaded"); } // 加载图片或到Cache中取,或到网络上抓取 byte[] bitmapData = loadImage(image, true); synchronized (mBitmapCacheLock) { BitmapHolder holder = mBitmapCache.get(image); if (holder != null) { if (bitmapData != null) { holder.mState = BitmapHolder.LOADED; holder.mBytes = bitmapData; inflateBitmap(holder); LogUtil.d(TAG, "LoadImageRunnable:" + image.toString() + " was loaded"); } else { holder.mState = BitmapHolder.NEEDED; LogUtil.e(TAG, "LoadImageRunnable:" + image.toString() + " load error"); } } else { holder = new BitmapHolder(); if (bitmapData != null) { holder.mState = BitmapHolder.LOADED; holder.mBytes = bitmapData; inflateBitmap(holder); LogUtil.d(TAG, "LoadImageRunnable:" + image.toString() + " was loaded"); } else { holder.mState = BitmapHolder.NEEDED; LogUtil.e(TAG, "LoadImageRunnable:" + image.toString() + " load error"); } } } // 加载完毕,通知UI线程绑定与imageToLoad图片相关的View if (bitmapData != null) { Message msg = mUIHandler.obtainMessage(MESSAGE_REQUEST_LOADED); msg.obj = image; mUIHandler.sendMessage(msg); } } }; private void bindImage(ImageView view, Bitmap bitmap, Image image) { if (view != null && bitmap != null && image != null) { if (view instanceof SelfBindView) { ((SelfBindView) view).SelfBindViewCallBack.bindView(view, bitmap, image); } else { if (view instanceof ZoomImageView) { ((ZoomImageView) view).setImageBitmapResetBase(bitmap, true); } else { view.setImageBitmap(image.proccessImage(bitmap)); } } } } private void bindDefaultImage(ImageView view, Bitmap defaultBitmap, int defaultResId) { if (view instanceof ZoomImageView) { Bitmap bitmap = defaultBitmap; if (bitmap == null && defaultResId != 0) { bitmap = BitmapFactory.decodeResource(mContext.getResources(), defaultResId); } ((ZoomImageView) view).setImageBitmapResetBase(bitmap, true); } else { if (defaultBitmap != null) { view.setImageBitmap(defaultBitmap); } else if (defaultResId != 0) { view.setImageResource(defaultResId); } else { view.setImageBitmap(null); } } } private byte[] loadImage(Image image, boolean fetchRemote) { final String cacheFileName = Coder.encodeSHA(image.getFileUrl()); final File file = new File(mContext.getCacheDir() + File.separator + cacheFileName); FileOutputStream os = null; ByteArrayOutputStream bos = null; FileInputStream is = null; try { bos = new ByteArrayOutputStream(); if (!file.exists() && fetchRemote) { RequestStream rs = new RequestStream(image.getFileUrl()); if (rs.requestStream(bos) != Request.STATUS_OK) { return null; } byte[] buffer = bos.toByteArray(); if (buffer != null && buffer.length > 0) { os = new FileOutputStream(file); os.write(buffer); os.flush(); return buffer; } } else if (file.exists()) { is = new FileInputStream(file); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { bos.write(buffer, 0, len); } if (bos.size() > 0) { return bos.toByteArray(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if (bos != null) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } }