| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.photos.drawables; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| 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.util.Log; |
| |
| import com.android.photos.data.GalleryBitmapPool; |
| |
| import java.io.InputStream; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| public abstract class AutoThumbnailDrawable<T> extends Drawable { |
| |
| private static final String TAG = "AutoThumbnailDrawable"; |
| |
| private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor(); |
| private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance(); |
| private static byte[] sTempStorage = new byte[64 * 1024]; |
| |
| // UI thread only |
| private Paint mPaint = new Paint(); |
| private Matrix mDrawMatrix = new Matrix(); |
| |
| // Decoder thread only |
| private BitmapFactory.Options mOptions = new BitmapFactory.Options(); |
| |
| // Shared, guarded by mLock |
| private Object mLock = new Object(); |
| private Bitmap mBitmap; |
| protected T mData; |
| private boolean mIsQueued; |
| private int mImageWidth, mImageHeight; |
| private Rect mBounds = new Rect(); |
| private int mSampleSize = 1; |
| |
| public AutoThumbnailDrawable() { |
| mPaint.setAntiAlias(true); |
| mPaint.setFilterBitmap(true); |
| mDrawMatrix.reset(); |
| mOptions.inTempStorage = sTempStorage; |
| } |
| |
| protected abstract byte[] getPreferredImageBytes(T data); |
| protected abstract InputStream getFallbackImageStream(T data); |
| protected abstract boolean dataChangedLocked(T data); |
| |
| public void setImage(T data, int width, int height) { |
| if (!dataChangedLocked(data)) return; |
| synchronized (mLock) { |
| mImageWidth = width; |
| mImageHeight = height; |
| mData = data; |
| setBitmapLocked(null); |
| refreshSampleSizeLocked(); |
| } |
| invalidateSelf(); |
| } |
| |
| private void setBitmapLocked(Bitmap b) { |
| if (b == mBitmap) { |
| return; |
| } |
| if (mBitmap != null) { |
| sBitmapPool.put(mBitmap); |
| } |
| mBitmap = b; |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| super.onBoundsChange(bounds); |
| synchronized (mLock) { |
| mBounds.set(bounds); |
| if (mBounds.isEmpty()) { |
| mBitmap = null; |
| } else { |
| refreshSampleSizeLocked(); |
| updateDrawMatrixLocked(); |
| } |
| } |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mBitmap != null) { |
| canvas.save(); |
| canvas.clipRect(mBounds); |
| canvas.concat(mDrawMatrix); |
| canvas.drawBitmap(mBitmap, 0, 0, mPaint); |
| canvas.restore(); |
| } else { |
| // TODO: Draw placeholder...? |
| } |
| } |
| |
| private void updateDrawMatrixLocked() { |
| if (mBitmap == null || mBounds.isEmpty()) { |
| mDrawMatrix.reset(); |
| return; |
| } |
| |
| float scale; |
| float dx = 0, dy = 0; |
| |
| int dwidth = mBitmap.getWidth(); |
| int dheight = mBitmap.getHeight(); |
| int vwidth = mBounds.width(); |
| int vheight = mBounds.height(); |
| |
| // Calculates a matrix similar to ScaleType.CENTER_CROP |
| if (dwidth * vheight > vwidth * dheight) { |
| scale = (float) vheight / (float) dheight; |
| dx = (vwidth - dwidth * scale) * 0.5f; |
| } else { |
| scale = (float) vwidth / (float) dwidth; |
| dy = (vheight - dheight * scale) * 0.5f; |
| } |
| if (scale < .8f) { |
| Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize); |
| } else if (scale > 1.5f) { |
| Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize); |
| } |
| |
| mDrawMatrix.setScale(scale, scale); |
| mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); |
| } |
| |
| private int calculateSampleSizeLocked(int dwidth, int dheight) { |
| float scale; |
| |
| int vwidth = mBounds.width(); |
| int vheight = mBounds.height(); |
| |
| // Inverse of updateDrawMatrixLocked |
| if (dwidth * vheight > vwidth * dheight) { |
| scale = (float) dheight / (float) vheight; |
| } else { |
| scale = (float) dwidth / (float) vwidth; |
| } |
| int result = Math.round(scale); |
| return result > 0 ? result : 1; |
| } |
| |
| private void refreshSampleSizeLocked() { |
| if (mBounds.isEmpty() || mImageWidth == 0 || mImageHeight == 0) { |
| return; |
| } |
| |
| int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); |
| if (sampleSize != mSampleSize || mBitmap == null) { |
| mSampleSize = sampleSize; |
| loadBitmapLocked(); |
| } |
| } |
| |
| private void loadBitmapLocked() { |
| if (!mIsQueued && !mBounds.isEmpty()) { |
| unscheduleSelf(mUpdateBitmap); |
| sThreadPool.execute(mLoadBitmap); |
| mIsQueued = true; |
| } |
| } |
| |
| public float getAspectRatio() { |
| return (float) mImageWidth / (float) mImageHeight; |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return -1; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return -1; |
| } |
| |
| @Override |
| public int getOpacity() { |
| Bitmap bm = mBitmap; |
| return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? |
| PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| int oldAlpha = mPaint.getAlpha(); |
| if (alpha != oldAlpha) { |
| mPaint.setAlpha(alpha); |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter cf) { |
| mPaint.setColorFilter(cf); |
| invalidateSelf(); |
| } |
| |
| private final Runnable mLoadBitmap = new Runnable() { |
| @Override |
| public void run() { |
| T data; |
| synchronized (mLock) { |
| data = mData; |
| } |
| int preferredSampleSize = 1; |
| byte[] preferred = getPreferredImageBytes(data); |
| boolean hasPreferred = (preferred != null && preferred.length > 0); |
| if (hasPreferred) { |
| mOptions.inJustDecodeBounds = true; |
| BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); |
| mOptions.inJustDecodeBounds = false; |
| } |
| int sampleSize, width, height; |
| synchronized (mLock) { |
| if (dataChangedLocked(data)) { |
| return; |
| } |
| width = mImageWidth; |
| height = mImageHeight; |
| if (hasPreferred) { |
| preferredSampleSize = calculateSampleSizeLocked( |
| mOptions.outWidth, mOptions.outHeight); |
| } |
| sampleSize = calculateSampleSizeLocked(width, height); |
| mIsQueued = false; |
| } |
| Bitmap b = null; |
| InputStream is = null; |
| try { |
| if (hasPreferred) { |
| mOptions.inSampleSize = preferredSampleSize; |
| mOptions.inBitmap = sBitmapPool.get( |
| mOptions.outWidth / preferredSampleSize, |
| mOptions.outHeight / preferredSampleSize); |
| b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); |
| if (mOptions.inBitmap != null && b != mOptions.inBitmap) { |
| sBitmapPool.put(mOptions.inBitmap); |
| mOptions.inBitmap = null; |
| } |
| } |
| if (b == null) { |
| is = getFallbackImageStream(data); |
| mOptions.inSampleSize = sampleSize; |
| mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize); |
| b = BitmapFactory.decodeStream(is, null, mOptions); |
| if (mOptions.inBitmap != null && b != mOptions.inBitmap) { |
| sBitmapPool.put(mOptions.inBitmap); |
| mOptions.inBitmap = null; |
| } |
| } |
| } catch (Exception e) { |
| Log.d(TAG, "Failed to fetch bitmap", e); |
| return; |
| } finally { |
| try { |
| if (is != null) { |
| is.close(); |
| } |
| } catch (Exception e) {} |
| if (b != null) { |
| synchronized (mLock) { |
| if (!dataChangedLocked(data)) { |
| setBitmapLocked(b); |
| scheduleSelf(mUpdateBitmap, 0); |
| } |
| } |
| } |
| } |
| } |
| }; |
| |
| private final Runnable mUpdateBitmap = new Runnable() { |
| @Override |
| public void run() { |
| synchronized (AutoThumbnailDrawable.this) { |
| updateDrawMatrixLocked(); |
| invalidateSelf(); |
| } |
| } |
| }; |
| |
| } |