Java tutorial
/** * MediaStorePhotoAdapter * Copyright(c) 2014 saki t_saki@serenegiant * Licensed under the Apache License, Version 2.0 (the "License"); * * MediaStorePhotoAdapter is a descendent of CursorAdapter that can load images asynchronusly * from MediaStore.Images.Thumbnails and set them to ImageView(that id is R.id.thumbnail) * there are two type mode, one is displaying all images (DISPLAY_IMAGE) * and the other is group by bucketId and shows only top image of each group (DISPLAY_BUCKET). * You can also narrow the range of displaying images with bucketid in DISPLAY_IMAGE mode. * * Most code of LoaderDrawable in this class is originally came * from BitmapJobDrawable.java in Android Gallery app * * 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.serenegiant.testmediastore; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; 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.Build; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; import android.support.v4.util.LruCache; import android.support.v4.widget.CursorAdapter; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; public class MediaStorePhotoAdapter extends CursorAdapter { private static final boolean DEBUG = false; // TODO set false when releasing private static final String TAG = "MediaStorePhotoAdapter"; // for thread pool private static final int CORE_POOL_SIZE = 4; // initial/minimum threads private static final int MAX_POOL_SIZE = 32; // maximum threads private static final int KEEP_ALIVE_TIME = 10; // time periods while keep the idle thread private static final ThreadPoolExecutor EXECUTER = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); // for thumbnail cache(in memory) // rate of memory usage for cache, 'CACHE_RATE = 8' means use 1/8 of available memory for image cache private static final int CACHE_RATE = 8; private static LruCache<Long, Bitmap> sThumbnailCache; private static int mThumbnailWidth = 200, mThumbnailHeight = 200; public static final int DISPLAY_IMAGE = 0; public static final int DISPLAY_BUCKET = 1; private static final String[] PROJ_ID = { MediaStore.Images.Media._ID, // index=0 for Cursor, column number=1 in SQL statement MediaStore.Images.Media.BUCKET_ID, // index=1 for Cursor, column number=2 in SQL statement MediaStore.Images.Media.TITLE, // index=2 for Cursor, column number=3 in SQL statement MediaStore.Images.Media.BUCKET_DISPLAY_NAME, // index=3 for Cursor, column number=4 in SQL statement }; private static final String[] PROJ_IMAGE = { MediaStore.Images.Media._ID, // index=0 for Cursor, column number=1 in SQL statement MediaStore.Images.Media.BUCKET_ID, // index=1 for Cursor, column number=2 in SQL statement MediaStore.Images.Media.TITLE, // index=2 for Cursor, column number=3 in SQL statement MediaStore.Images.Media.BUCKET_DISPLAY_NAME, // index=3 for Cursor, column number=4 in SQL statement MediaStore.Images.Media.DATA, // index=4 for Cursor, column number=5 in SQL statement MediaStore.Images.Media.DESCRIPTION, // index=5 for Cursor, column number=6 in SQL statement MediaStore.Images.Media.ORIENTATION, // index=6 for Cursor, column number=7 in SQL statement }; // "1) GROUP BY (2" means "SELECT ... FROM ... WHERE (1) GROUP BY (2)" private static final String SELECTION_GROUP_BY_BUCKET = "1) GROUP BY (2"; // group by BUCKET_ID private static final String SELECTION_IMAGE = MediaStore.Images.Media.BUCKET_ID + "=?"; // these values should be fit to PROJ_ID/PROJ_IMAGE private static final int PROJ_INDEX_ID = 0; private static final int PROJ_INDEX_BUCKET_ID = 1; private static final int PROJ_INDEX_TITLE = 2; private static final int PROJ_INDEX_BUCKET_NAME = 3; private static final int PROJ_IMAGE_INDEX_DATA = 4; // only for PROJ_IMAGE private static final int PROJ_IMAGE_INDEX_DESCRIPTION = 5; // only for PROJ_IMAGE private static final int PROJ_IMAGE_INDEX_ORIENTATION = 6; // only for PROJ_IMAGE private final LayoutInflater mInflater; private final ContentResolver mCr; private final int mMemClass; private final int mLayoutId; private final MyAsyncQueryHandler mQueryHandler; private Cursor mMediaInfoCursor; private String mSelection; private String[] mSelectionArgs; private int mDisplayType = DISPLAY_IMAGE; private boolean mShowTitle; private int mBucketId = 0; public static class MediaInfo { public int bucketId; public String data; public String title; public String bucketName; public String description; public int orientation; public String contentType; public int width; // API >= 16 public int height; // API >= 16 } public MediaStorePhotoAdapter(Context context, int id_layout) { super(context, null, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); mInflater = LayoutInflater.from(context); mCr = context.getContentResolver(); mQueryHandler = new MyAsyncQueryHandler(mCr, this); // getMemoryClass return the available memory amounts for app as mega bytes(API >= 5) mMemClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); mLayoutId = id_layout; onContentChanged(); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { // this method is called within UI thread and should return as soon as possible final View view = mInflater.inflate(mLayoutId, parent, false); getViewHolder(view); return view; } @Override public void bindView(View view, Context context, Cursor cursor) { // this method is called within UI thread and should return as soon as possible final ViewHolder holder = getViewHolder(view); final ImageView iv = holder.mImageView; final TextView tv = holder.mTitleView; Drawable drawable = iv.getDrawable(); if ((drawable == null) || !(drawable instanceof LoaderDrawable)) { drawable = new LoaderDrawable(mCr); iv.setImageDrawable(drawable); } ((LoaderDrawable) drawable).startLoad(cursor.getLong(PROJ_INDEX_ID)); if (tv != null) { tv.setVisibility(mShowTitle ? View.VISIBLE : View.GONE); if (mShowTitle) tv.setText(cursor .getString(mDisplayType == DISPLAY_IMAGE ? PROJ_INDEX_TITLE : PROJ_INDEX_BUCKET_NAME)); } } private ViewHolder getViewHolder(View view) { ViewHolder holder; // you can use View#getTag()/setTag() instead of using View#getTag(int)/setTag(int) // but we assume that using getTag(int)/setTag(int) and keeping getTag()/setTag() left for user is better. holder = (ViewHolder) view.getTag(R.id.mediastorephotoadapter); if (holder == null) { holder = new ViewHolder(); if (view instanceof ImageView) { holder.mImageView = (ImageView) view; view.setTag(R.id.mediastorephotoadapter, holder); } else { View v = view.findViewById(R.id.thumbnail); if (v instanceof ImageView) holder.mImageView = (ImageView) v; v = view.findViewById(R.id.title); if (v instanceof TextView) holder.mTitleView = (TextView) v; view.setTag(R.id.mediastorephotoadapter, holder); } } return holder; } @Override protected void finalize() throws Throwable { changeCursor(null); if (mMediaInfoCursor != null) { mMediaInfoCursor.close(); mMediaInfoCursor = null; } super.finalize(); } @Override protected void onContentChanged() { createBitmapCache(false); mQueryHandler.requery(); } /** * return thumbnail image at specific position. * this method is synchronously executed and may take time * @return null if the position value is out of range etc. */ @Override public Bitmap getItem(int position) { Bitmap result = null; try { result = getThumbnail(mCr, getItemId(position), mThumbnailWidth, mThumbnailHeight); } catch (FileNotFoundException e) { Log.w(TAG, e); } catch (IOException e) { Log.w(TAG, e); } return result; } /** * return image with specific size(only scale-down or original size are available now) * if width=0 and height=0, return image with original size. * this method is synchronously executed and may take time * @return null if the position value is out of range etc. * @param position * @param width * @param height * @return * @throws FileNotFoundException * @throws IOException */ public Bitmap getImage(int position, int width, int height) throws FileNotFoundException, IOException { return getImage(mCr, getItemId(position), width, height); } /** * get mediainfo at specified position * @param position * @return */ public synchronized MediaInfo getMediaInfo(int position) { MediaInfo info = null; /* // if you don't need to frequently call this method, temporary query may be better to reduce memory usage. // but it will take more time. final Cursor cursor = mCr.query( ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, getItemId(position)), PROJ_IMAGE, mSelection, mSelectionArgs, MediaStore.Images.Media.DEFAULT_SORT_ORDER); if (cursor != null) { try { if (cursor.moveToFirst()) { info = readMediaInfo(cursor, new MediaInfo()); } } finally { cursor.close(); } } */ if (mMediaInfoCursor == null) { mMediaInfoCursor = mCr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJ_IMAGE, mSelection, mSelectionArgs, MediaStore.Images.Media.DEFAULT_SORT_ORDER); } if (mMediaInfoCursor.moveToPosition(position)) { info = readMediaInfo(mMediaInfoCursor, new MediaInfo()); } return info; } private static final MediaInfo readMediaInfo(Cursor cursor, MediaInfo info) { info.bucketId = cursor.getInt(PROJ_INDEX_BUCKET_ID); info.title = cursor.getString(PROJ_INDEX_TITLE); info.bucketName = cursor.getString(PROJ_INDEX_BUCKET_NAME); info.data = cursor.getString(PROJ_IMAGE_INDEX_DATA); info.description = cursor.getString(PROJ_IMAGE_INDEX_DESCRIPTION); info.orientation = cursor.getInt(PROJ_IMAGE_INDEX_ORIENTATION); return info; } /** * set thumbnail size, if you set size to zero, the size is 96x96(MediaStore.Images.Thumbnails.MICRO_KIND) * @param size */ public void setThumbnailSize(int size) { if ((mThumbnailWidth != size) || (mThumbnailHeight != size)) { mThumbnailWidth = mThumbnailHeight = size; createBitmapCache(true); onContentChanged(); } } /** * set thumbnail size, if you set both width and height to zero, the size is 96x96(MediaStore.Images.Thumbnails.MICRO_KIND) * @param width * @param height */ public void setThumbnailSize(int width, int height) { if ((mThumbnailWidth != width) || (mThumbnailHeight != height)) { mThumbnailWidth = width; mThumbnailHeight = height; createBitmapCache(true); onContentChanged(); } } public void setShowTitle(boolean showTitle) { if (mShowTitle != showTitle) { mShowTitle = showTitle; onContentChanged(); } } public boolean getShowTitle() { return mShowTitle; } /** * @param displayType = DISPLAY_IMAGE/DISPLAY_BUCKET */ public synchronized void setDisplayType(int displayType) { if (mDisplayType != displayType % 2) { mDisplayType = displayType % 2; onContentChanged(); } mBucketId = 0; } public int getDisplayType() { return mDisplayType; } public int getBucketId() { return mBucketId; } public void setBucketId(int bucketId) { if (mBucketId != bucketId) { mBucketId = bucketId; onContentChanged(); } } /** * asynchronusly add bitmap to standard camera directory * @param bitmap * @param title * @param description */ public void add(final Bitmap bitmap, final String title, final String description) { if (bitmap != null) { EXECUTER.execute(new Runnable() { @Override public void run() { final String url = MediaStore.Images.Media.insertImage(mCr, bitmap, title, description); if (url == null) { Log.w(TAG, "fail to insert image"); } } }); } } /** * asynchronusly add bitmap to specific named directory with automatically generated file name. * @param bitmap * @param dirName application specific directory name(ExternalStorageDirectory is added automatically inside this method) * @param title * @param description */ public void add(final Bitmap bitmap, final String dirName, final String title, final String description) { if (bitmap != null && !TextUtils.isEmpty(dirName)) { EXECUTER.execute(new Runnable() { @SuppressLint("InlinedApi") @Override public void run() { final StringBuilder sb = new StringBuilder(); // directory name sb.append(Environment.getExternalStorageDirectory()).append("/").append(dirName); final String directory = sb.toString(); final File dir = new File(directory); if (!dir.exists()) { dir.mkdirs(); // we need to create directory with missing parent if not exist } // file path sb.setLength(0); sb.append(directory).append("/").append(System.currentTimeMillis()).append(".jpg"); final String filePath = sb.toString(); // API >= 8 OutputStream out = null; try { out = new FileOutputStream(new File(filePath)); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out); } catch (FileNotFoundException e) { Log.w(TAG, e); } finally { if (out != null) { try { out.close(); } catch (IOException e) { Log.w(TAG, e); } } } /* // we can request to add image to MediaStore using MediaScannerConnection#scanFile // but that method cannot add specified title/description, // so we add(insert) by ourselves MediaScannerConnection.scanFile(mContext, new String[] {filePath}, new String[]{"image/jpeg"}, null); */ final ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.TITLE, title); values.put(MediaStore.Images.Media.DESCRIPTION, description); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.DATA, filePath); // if you want to confirm the result of insertion, // you can use AsyncQueryHandler#startInsert and #onInsertComplete callback // instead of using ContentResolver#insert // mQueryHandler.startInsert(0, null, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); mCr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); } }); } } /** * asynchronusly delete image at specified position. * @param position */ public void delete(int position) { final long id = getItemId(position); if (id > 0) { mQueryHandler.startDelete(0, this, ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), null, null); } } /** * request to run command on other thread than UI thread */ public static void queuEvent(Runnable command) { EXECUTER.execute(command); } /** * if you finish using this adapter class in your app, * you can call this method to free internal thumbnail cache */ public static void destroy() { if (sThumbnailCache != null) { sThumbnailCache.evictAll(); sThumbnailCache = null; } } private static final class MyAsyncQueryHandler extends AsyncQueryHandler { private final MediaStorePhotoAdapter mAdapter; public MyAsyncQueryHandler(ContentResolver cr, MediaStorePhotoAdapter adapter) { super(cr); mAdapter = adapter; } public void requery() { synchronized (mAdapter) { mAdapter.mSelection = null; mAdapter.mSelectionArgs = null; if (mAdapter.mMediaInfoCursor != null) { mAdapter.mMediaInfoCursor.close(); mAdapter.mMediaInfoCursor = null; } switch (mAdapter.mDisplayType) { case DISPLAY_IMAGE: if (mAdapter.mBucketId != 0) { mAdapter.mSelection = SELECTION_IMAGE; mAdapter.mSelectionArgs = new String[] { Integer.toString(mAdapter.mBucketId) }; } break; case DISPLAY_BUCKET: mAdapter.mSelection = SELECTION_GROUP_BY_BUCKET; break; } startQuery(0, mAdapter, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJ_ID, mAdapter.mSelection, mAdapter.mSelectionArgs, MediaStore.Images.Media.DEFAULT_SORT_ORDER); } } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // super.onQueryComplete(token, cookie, cursor); // this is empty method final Cursor oldCursor = mAdapter.swapCursor(cursor); if ((oldCursor != null) && !oldCursor.isClosed()) oldCursor.close(); } } /** * create thumbnail cache */ @SuppressLint("NewApi") private final void createBitmapCache(boolean clear) { if (clear && (sThumbnailCache != null)) { sThumbnailCache.evictAll(); System.gc(); } if (sThumbnailCache == null) { // use 1/CACHE_RATE of available memory as memory cache final int cacheSize = 1024 * 1024 * mMemClass / CACHE_RATE; // [MB] => [bytes] sThumbnailCache = new LruCache<Long, Bitmap>(cacheSize) { @Override protected int sizeOf(Long key, Bitmap bitmap) { // control memory usage instead of bitmap counts return bitmap.getRowBytes() * bitmap.getHeight(); // [bytes] } }; } if (Build.VERSION.SDK_INT >= 9) { EXECUTER.allowCoreThreadTimeOut(true); // this makes core threads can terminate } // in many case, calling createBitmapCache method means start the new query // and need to prepare to run asynchronus tasks EXECUTER.prestartAllCoreThreads(); } private static final Bitmap getImage(ContentResolver cr, long id, int requestWidth, int requestHeight) throws FileNotFoundException, IOException { Bitmap result = null; final ParcelFileDescriptor pfd = cr.openFileDescriptor( ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), "r"); if (pfd != null) { try { final BitmapFactory.Options options = new BitmapFactory.Options(); // just decorde to get image size options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, options); // calculate sub-sampling options.inSampleSize = calcSampleSize(options, requestWidth, requestHeight); options.inJustDecodeBounds = false; result = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, options); } finally { pfd.close(); } } return result; } private static final Bitmap getThumbnail(ContentResolver cr, long id, int requestWidth, int requestHeight) throws FileNotFoundException, IOException { // try to get from internal thumbnail cache(in memory), this may be redundant Bitmap result = sThumbnailCache.get(id); if (result == null) { BitmapFactory.Options options = null; int kind = MediaStore.Images.Thumbnails.MICRO_KIND; if ((requestWidth > 96) || (requestHeight > 96) || (requestWidth * requestHeight > 128 * 128)) kind = MediaStore.Images.Thumbnails.MINI_KIND; result = MediaStore.Images.Thumbnails.getThumbnail(cr, id, kind, options); if (result != null) { if (DEBUG) Log.v(TAG, String.format("getThumbnail:id=%d(%d,%d)", id, result.getWidth(), result.getHeight())); // add to internal thumbnail cache(in memory) sThumbnailCache.put(id, result); } } return result; } /** * calculate maximum sub-sampling size that the image size is greater or equal to requested size * @param options * @param requestWidth * @param requestHeight * @return maximum sub-sampling size */ private static final int calcSampleSize(BitmapFactory.Options options, final int requestWidth, final int requestHeight) { final int imageWidth = options.outWidth; final int imageHeight = options.outHeight; int reqWidth = requestWidth, reqHeight = requestHeight; if (requestWidth == 0) { if (requestHeight > 0) reqWidth = (int) (imageWidth * requestHeight / (float) imageHeight); else reqWidth = imageWidth; } if (requestHeight == 0) { if (requestWidth > 0) reqHeight = (int) (imageHeight * requestWidth / (float) imageHeight); else reqHeight = imageHeight; } int inSampleSize = 1; if ((imageHeight > reqHeight) || (imageWidth > reqWidth)) { if (imageWidth > imageHeight) { inSampleSize = (int) Math.round(imageHeight / (float) reqHeight); // Math.floor } else { inSampleSize = (int) Math.round(imageWidth / (float) reqWidth); // Math.floor } } /* if (DEBUG) Log.v(TAG, String.format("calcSampleSize:image=(%d,%d),request=(%d,%d),inSampleSize=%d", imageWidth, imageHeight, reqWidth, reqHeight, inSampleSize)); */ return inSampleSize; } private static final class ViewHolder { TextView mTitleView; ImageView mImageView; } /** * LoaderDrawable is a descendent of Drawable to load image asynchronusly and draw * We want to use BitmapDrawable but we can't because it has no public/protected method * to set Bitmap after construction. * * Most code of LoaderDrawable came from BitmapJobDrawable.java in Android Gallery app * * 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. */ private static final class LoaderDrawable extends Drawable implements Runnable { private final ContentResolver mContentResolver; private final Paint mPaint = new Paint(); private final Paint mDebugPaint = new Paint(); private final Matrix mDrawMatrix = new Matrix(); private Bitmap mBitmap; private int mRotation = 0; private ThumbnailLoader mLoader; public LoaderDrawable(ContentResolver cr) { mContentResolver = cr; mDebugPaint.setColor(Color.RED); mDebugPaint.setTextSize(18); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); updateDrawMatrix(getBounds()); } @Override public void draw(Canvas canvas) { final Rect bounds = getBounds(); if (mBitmap != null) { canvas.save(); canvas.clipRect(bounds); canvas.concat(mDrawMatrix); canvas.rotate(mRotation, bounds.centerX(), bounds.centerY()); canvas.drawBitmap(mBitmap, 0, 0, mPaint); canvas.restore(); } else { mPaint.setColor(0xFFCCCCCC); canvas.drawRect(bounds, mPaint); } if (DEBUG) { canvas.drawText(Long.toString(mLoader != null ? mLoader.mId : -1), bounds.centerX(), bounds.centerY(), mDebugPaint); } } private void updateDrawMatrix(Rect bounds) { if (mBitmap == null || bounds.isEmpty()) { mDrawMatrix.reset(); return; } final float dwidth = mBitmap.getWidth(); final float dheight = mBitmap.getHeight(); final int vwidth = bounds.width(); final int vheight = bounds.height(); float scale; float dx = 0, dy = 0; // 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; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); /* // Calculates a matrix similar to ScaleType.CENTER_INSIDE if (dwidth <= vwidth && dheight <= vheight) { scale = 1.0f; } else { scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f); dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); */ invalidateSelf(); } @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(); } @Override public int getIntrinsicWidth() { return mThumbnailWidth; } @Override public int getIntrinsicHeight() { return mThumbnailHeight; } @Override public int getOpacity() { Bitmap bm = mBitmap; return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; } /** * callback to set bitmap on UI thread after asyncronus loading * request call this callback in ThumbnailLoader#run at the end of asyncronus loading */ @Override public void run() { setBitmap(mLoader.getBitmap()); } /** * start loading image asynchronusly * @param cursor */ public void startLoad(long id) { if (mLoader != null) mLoader.cancelLoad(); // try to get from internal thumbnail cache final Bitmap newBitmap = sThumbnailCache.get(id); if (newBitmap == null) { // only start loading if the thumbnail does not exist in internal thumbnail cache mBitmap = null; // re-using ThumbnailLoader will cause several problems on some devices... mLoader = new ThumbnailLoader(this); mLoader.startLoad(id); } else { setBitmap(newBitmap); } invalidateSelf(); } private void setBitmap(Bitmap bitmap) { if (bitmap != mBitmap) { mBitmap = bitmap; updateDrawMatrix(getBounds()); } } } /** * Runnable to load image asynchronusly */ private static final class ThumbnailLoader implements Runnable { private final LoaderDrawable mParent; private final FutureTask<Bitmap> mTask; private long mId; private Bitmap mBitmap; public ThumbnailLoader(LoaderDrawable parent) { mParent = parent; mTask = new FutureTask<Bitmap>(this, null); } /** * start loading * @param id */ public synchronized void startLoad(long id) { mId = id; mBitmap = null; EXECUTER.execute(mTask); } /** * cancel loading */ public void cancelLoad() { mTask.cancel(true); } @Override public void run() { long id; synchronized (this) { id = mId; } if (!mTask.isCancelled()) { try { mBitmap = getThumbnail(mParent.mContentResolver, id, mThumbnailWidth, mThumbnailHeight); } catch (Exception e) { if (DEBUG) Log.w(TAG, e); } } if (mTask.isCancelled() || (id != mId) || (mBitmap == null)) { return; // return without callback } // set callback mParent.scheduleSelf(mParent, 0); } public Bitmap getBitmap() { return mBitmap; } } }