Back to project page ByakuGallery.
The source code is released under:
Apache License
If you think the Android project ByakuGallery listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
package com.diegocarloslima.byakugallery.lib; //w w w . j a va 2s . c o m import java.io.FileDescriptor; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; import android.support.v4.util.LruCache; import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; import android.widget.ImageView; public class TileBitmapDrawable extends Drawable { private static final int TILE_SIZE_DENSITY_HIGH = 256; private static final int TILE_SIZE_DEFAULT = 128; // A shared cache is used between instances to minimize OutOfMemoryError private static BitmapLruCache sBitmapCache; private static final Object sBitmapCacheLock = new Object(); // Instance ids are used to identify a cache hit for a specific instance of TileBitmapDrawable on the shared BitmapLruCache private static final AtomicInteger sInstanceIds = new AtomicInteger(1); private final int mInstanceId = sInstanceIds.getAndIncrement(); // The reference of the parent ImageView is needed in order to get the Matrix values and determine the visible area private final WeakReference<ImageView> mParentView; private final BitmapRegionDecoder mRegionDecoder; private final BlockingQueue<Tile> mDecodeQueue = new LinkedBlockingQueue<Tile>(); private final DecoderWorker mDecoderWorker; private final int mIntrinsicWidth; private final int mIntrinsicHeight; private final int mTileSize; private final Bitmap mScreenNail; private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); private Matrix mMatrix; private final float[] mMatrixValues = new float[9]; private float[] mLastMatrixValues = new float[9]; private final Rect mTileRect = new Rect(); private final Rect mVisibleAreaRect = new Rect(); private final Rect mScreenNailRect = new Rect(); public static void attachTileBitmapDrawable(ImageView imageView, String path, Drawable placeHolder, OnInitializeListener listener) { new InitializationTask(imageView, placeHolder, listener).execute(path); } public static void attachTileBitmapDrawable(ImageView imageView, FileDescriptor fd, Drawable placeHolder, OnInitializeListener listener) { new InitializationTask(imageView, placeHolder, listener).execute(fd); } public static void attachTileBitmapDrawable(ImageView imageView, InputStream is, Drawable placeHolder, OnInitializeListener listener) { new InitializationTask(imageView, placeHolder, listener).execute(is); } private TileBitmapDrawable(ImageView parentView, BitmapRegionDecoder decoder, Bitmap screenNail) { mParentView = new WeakReference<ImageView>(parentView); synchronized(decoder) { mRegionDecoder = decoder; mIntrinsicWidth = mRegionDecoder.getWidth(); mIntrinsicHeight = mRegionDecoder.getHeight(); } final DisplayMetrics metrics = new DisplayMetrics(); getDisplayMetrics(parentView.getContext(), metrics); mTileSize = metrics.densityDpi >= DisplayMetrics.DENSITY_HIGH ? TILE_SIZE_DENSITY_HIGH : TILE_SIZE_DEFAULT; mScreenNail = screenNail; synchronized(sBitmapCacheLock) { if(sBitmapCache == null) { // The Tile can be reduced up to half of its size until the next level of tiles is displayed final int maxHorizontalTiles = (int) Math.ceil(2 * metrics.widthPixels / (float) mTileSize); final int maxVerticalTiles = (int) Math.ceil(2 * metrics.heightPixels / (float) mTileSize); // The shared cache will have the minimum required size to display all visible tiles // Here, we multiply by 4 because in ARGB_8888 config, each pixel is stored on 4 bytes final int cacheSize = 4 * maxHorizontalTiles * maxVerticalTiles * mTileSize * mTileSize; sBitmapCache = new BitmapLruCache(cacheSize); } } mDecoderWorker = new DecoderWorker(this, mRegionDecoder, mDecodeQueue); mDecoderWorker.start(); } @Override public void setAlpha(int alpha) { final int oldAlpha = mPaint.getAlpha(); if(alpha != oldAlpha) { mPaint.setAlpha(alpha); invalidateSelf(); } } @Override public int getAlpha() { return mPaint.getAlpha(); } @Override public int getOpacity() { if (mScreenNail == null || mScreenNail.hasAlpha() || mPaint.getAlpha() < 255) { return PixelFormat.TRANSLUCENT; } return PixelFormat.OPAQUE; } @Override public void setColorFilter(ColorFilter cf) { mPaint.setColorFilter(cf); invalidateSelf(); } @Override public int getIntrinsicWidth() { return mIntrinsicWidth; } @Override public int getIntrinsicHeight() { return mIntrinsicHeight; } @Override public void draw(Canvas canvas) { final ImageView parentView = mParentView.get(); if(parentView == null) { return; } final int parentViewWidth = parentView.getWidth(); final int parentViewHeight = parentView.getHeight(); mMatrix = parentView.getImageMatrix(); mMatrix.getValues(mMatrixValues); final float translationX = mMatrixValues[Matrix.MTRANS_X]; final float translationY = mMatrixValues[Matrix.MTRANS_Y]; final float scale = mMatrixValues[Matrix.MSCALE_X]; // If the matrix values have changed, the decode queue must be cleared in order to avoid decoding unused tiles if(translationX != mLastMatrixValues[Matrix.MTRANS_X] || translationY != mLastMatrixValues[Matrix.MTRANS_Y] || scale != mLastMatrixValues[Matrix.MSCALE_X]) { mDecodeQueue.clear(); } mLastMatrixValues = Arrays.copyOf(mMatrixValues, mMatrixValues.length); // The scale required to display the whole Bitmap inside the ImageView. It will be the minimum allowed scale value final float minScale = Math.min(parentViewWidth / (float) mIntrinsicWidth, parentViewHeight / (float) mIntrinsicHeight); // The number of allowed levels for this Bitmap. Each subsequent level is half size of the previous one final int levelCount = Math.max(1, MathUtils.ceilLog2(mIntrinsicWidth / (mIntrinsicWidth * minScale))); // sampleSize = 2 ^ currentLevel final int currentLevel = MathUtils.clamp(MathUtils.floorLog2(1 / scale), 0, levelCount - 1); final int sampleSize = 1 << currentLevel; final int currentTileSize = mTileSize * sampleSize; final int horizontalTiles = (int) Math.ceil(mIntrinsicWidth / (float) currentTileSize); final int verticalTiles = (int) Math.ceil(mIntrinsicHeight / (float) currentTileSize); final int visibleAreaLeft = Math.max(0, (int) (-translationX / scale)); final int visibleAreaTop = Math.max(0, (int) (-translationY / scale)); final int visibleAreaRight = Math.min(mIntrinsicWidth, Math.round((-translationX + parentViewWidth) / scale)); final int visibleAreaBottom = Math.min(mIntrinsicHeight, Math.round((-translationY + parentViewHeight) / scale)); mVisibleAreaRect.set(visibleAreaLeft, visibleAreaTop, visibleAreaRight, visibleAreaBottom); boolean cacheMiss = false; for(int i = 0; i < horizontalTiles; i++) { for(int j = 0; j < verticalTiles; j++) { final int tileLeft = i * currentTileSize; final int tileTop = j * currentTileSize; final int tileRight = (i + 1) * currentTileSize <= mIntrinsicWidth ? (i + 1) * currentTileSize : mIntrinsicWidth; final int tileBottom = (j + 1) * currentTileSize <= mIntrinsicHeight ? (j + 1) * currentTileSize : mIntrinsicHeight; mTileRect.set(tileLeft, tileTop, tileRight, tileBottom); if(Rect.intersects(mVisibleAreaRect, mTileRect)) { final Tile tile = new Tile(mInstanceId, mTileRect, i, j, currentLevel); Bitmap cached = null; synchronized(sBitmapCacheLock) { cached = sBitmapCache.get(tile.getKey()); } if(cached != null) { canvas.drawBitmap(cached, null, mTileRect, mPaint); } else { cacheMiss = true; synchronized (mDecodeQueue) { if(!mDecodeQueue.contains(tile)) { mDecodeQueue.add(tile); } } // The screenNail is used while the proper tile is being decoded final int screenNailLeft = Math.round(tileLeft * mScreenNail.getWidth() / (float) mIntrinsicWidth); final int screenNailTop = Math.round(tileTop * mScreenNail.getHeight() / (float) mIntrinsicHeight); final int screenNailRight = Math.round(tileRight * mScreenNail.getWidth() / (float) mIntrinsicWidth); final int screenNailBottom = Math.round(tileBottom * mScreenNail.getHeight() / (float) mIntrinsicHeight); mScreenNailRect.set(screenNailLeft, screenNailTop, screenNailRight, screenNailBottom); canvas.drawBitmap(mScreenNail, mScreenNailRect, mTileRect, mPaint); } } } } // If we had a cache miss, we will need to redraw until all needed tiles have been decoded by our worker thread if(cacheMiss) { invalidateSelf(); } } @Override protected void finalize() throws Throwable { mDecoderWorker.quit(); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private static void getDisplayMetrics(Context context, DisplayMetrics outMetrics) { final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); final Display display = wm.getDefaultDisplay(); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { display.getRealMetrics(outMetrics); } else { display.getMetrics(outMetrics); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { try { outMetrics.widthPixels = (Integer) Display.class.getMethod("getRawWidth").invoke(display); outMetrics.heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(display); return; } catch (Exception e) {} } } } public interface OnInitializeListener { public void onStartInitialization(); public void onEndInitialization(); } private static final class Tile { private final int mInstanceId; private final Rect mTileRect; private final int mHorizontalPos; private final int mVerticalPos; private final int mLevel; private Tile(int instanceId, Rect tileRect, int horizontalPos, int verticalPos, int level) { mInstanceId = instanceId; mTileRect = new Rect(); mTileRect.set(tileRect); mHorizontalPos = horizontalPos; mVerticalPos = verticalPos; mLevel = level; } public String getKey() { return "#" + mInstanceId + "#" + mHorizontalPos + "#" + mVerticalPos + "#" + mLevel; } @Override public int hashCode() { return getKey().hashCode(); } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o instanceof TileBitmapDrawable) { return getKey().equals(((Tile) o).getKey()); } return false; } } private static final class BitmapLruCache extends LruCache<String, Bitmap> { private BitmapLruCache(int maxSize) { super(maxSize); } @Override protected int sizeOf(String key, Bitmap value) { return getBitmapSize(value); } @TargetApi(Build.VERSION_CODES.KITKAT) private static int getBitmapSize(Bitmap bitmap) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return bitmap.getAllocationByteCount(); } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { return bitmap.getByteCount(); } return bitmap.getRowBytes() * bitmap.getHeight(); } } private static final class InitializationTask extends AsyncTask<Object, Void, TileBitmapDrawable> { private final ImageView mImageView; private final OnInitializeListener mListener; private InitializationTask(ImageView imageView, Drawable placeHolder, OnInitializeListener listener) { mImageView = imageView; mListener = listener; if(mListener != null) { mListener.onStartInitialization(); } if(placeHolder != null) { mImageView.setImageDrawable(placeHolder); } } @Override protected TileBitmapDrawable doInBackground(Object... params) { BitmapRegionDecoder decoder = null; try { if(params[0] instanceof String) { decoder = BitmapRegionDecoder.newInstance((String) params[0], false); } else if(params[0] instanceof FileDescriptor) { decoder = BitmapRegionDecoder.newInstance((FileDescriptor) params[0], false); } else { decoder = BitmapRegionDecoder.newInstance((InputStream) params[0], false); } } catch (Exception e) { throw new RuntimeException(e); } final DisplayMetrics metrics = new DisplayMetrics(); final WindowManager wm = (WindowManager) mImageView.getContext().getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getMetrics(metrics); final float minScale = Math.min(metrics.widthPixels / (float) decoder.getWidth(), metrics.heightPixels / (float) decoder.getHeight()); final int levelCount = Math.max(1, MathUtils.ceilLog2(decoder.getWidth() / (decoder.getWidth() * minScale))); final Rect screenNailRect = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Config.ARGB_8888; options.inPreferQualityOverSpeed = true; options.inSampleSize = (1 << (levelCount - 1)); Bitmap screenNail = null; try { final Bitmap bitmap = decoder.decodeRegion(screenNailRect, options); screenNail = Bitmap.createScaledBitmap(bitmap, Math.round(decoder.getWidth() * minScale), Math.round(decoder.getHeight() * minScale), true); if(!bitmap.equals(screenNail)) { bitmap.recycle(); } } catch (OutOfMemoryError e) { // We're under memory pressure. Let's try again with a smaller size options.inSampleSize <<= 1; screenNail = decoder.decodeRegion(screenNailRect, options); } TileBitmapDrawable drawable = new TileBitmapDrawable(mImageView, decoder, screenNail); return drawable; } @Override protected void onPostExecute(TileBitmapDrawable result) { if(mListener != null) { mListener.onEndInitialization(); } mImageView.setImageDrawable(result); } } private static final class DecoderWorker extends Thread { private final WeakReference<TileBitmapDrawable> mDrawableReference; private final BitmapRegionDecoder mDecoder; private final BlockingQueue<Tile> mDecodeQueue; private boolean mQuit; private DecoderWorker(TileBitmapDrawable drawable, BitmapRegionDecoder decoder, BlockingQueue<Tile> decodeQueue) { mDrawableReference = new WeakReference<TileBitmapDrawable>(drawable); mDecoder = decoder; mDecodeQueue = decodeQueue; } @Override public void run() { while(true) { if(mDrawableReference.get() == null) { return; } Tile tile; try { tile = mDecodeQueue.take(); } catch (InterruptedException e) { if(mQuit) { return; } continue; } synchronized(sBitmapCacheLock) { if(sBitmapCache.get(tile.getKey()) != null) { continue; } } final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Config.ARGB_8888; options.inPreferQualityOverSpeed = true; options.inSampleSize = (1 << tile.mLevel); Bitmap bitmap = null; synchronized(mDecoder) { try { bitmap = mDecoder.decodeRegion(tile.mTileRect, options); } catch(OutOfMemoryError e) { // Skip for now. The screenNail will be used instead } } if(bitmap == null) { continue; } synchronized(sBitmapCacheLock) { sBitmapCache.put(tile.getKey(), bitmap); } } } public void quit() { mQuit = true; interrupt(); } } }