android图片:多选相册的实现

上一篇文章简单介绍了图片的加载,但是实际业务中,加载图片的需求会比这复杂得多,例如这篇博客要讲的多选相册的实现,会涉及到下面几个问题:

 

1、  取得图片路径:这里涉及到contentprovider,不是本文重点,这里只提供代码,不做详细解释。

 

2、  耗时操作:相册图片的读取是从硬盘读取的,这是一个耗时操作,不能直接在ui主线程操作,应该另起线程,可以使用AsyncTask来加载图片。

 

3、  并发性:相册有大量图片,通常我们用gridview来显示,同时会用viewholder来复用view,但是由于我们的图片加载是在线程中并发操作的,快速滑动gridview时,会使得同一个view,同时有多个task在加载图片,会导致图片错位和view一直在变换图片,而且图片加载效率非常低(如果没看明白,不用着急,下面有例子展示)。

 

4、  图片缓存:为了提高效率,我们应该对图片做缓存,加载图片时,先从缓存读取,读取不到再去硬盘读取。一方面内存读取效率高,另一方面减少重复操作(硬盘读取时,我们是先做压缩,再读取)。

 

 

下面一一解决上面的问题。

 

1、取得相册图片路径。

public static List getImages(Context context){
    List list = new ArrayList();
    ContentResolver contentResolver = context.getContentResolver();
    Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA,};
    String sortOrder = MediaStore.Images.Media.DATE_ADDED + " desc";
    Cursor cursor = contentResolver.query(uri, projection, null, null, sortOrder);
    int iId = cursor.getColumnIndex(MediaStore.Images.Media._ID);
    int iPath = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
    cursor.moveToFirst();
    while (!cursor.isAfterLast()) {
        String id = cursor.getString(iId);
        String path = cursor.getString(iPath);
        ImageModel imageModel = new ImageModel(id,path);
        list.add(imageModel);
        cursor.moveToNext();
    }
    cursor.close();
    return list;
}


其中ImageModel为图片类:

public class ImageModel {

    private String id;//图片id
    private String path;//路径
    private Boolean isChecked = false;//是否被选中

    public ImageModel(String id, String path, Boolean isChecked) {
        this.id = id;
        this.path = path;
        this.isChecked = isChecked;
    }

    public ImageModel(String id, String path) {
        this.id = id;
        this.path = path;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public Boolean getIsChecked() {
        return isChecked;
    }

    public void setIsChecked(Boolean isChecked) {
        this.isChecked = isChecked;
    }
}

别忘了在AndroidManifest.xml中加上权限:


2、使用AsyncTask加载图片


由于从硬盘读取照片是耗时操作,我们不能直接在ui主线程里面去操作,这里用AsyncTask来进行图片读取。

class BitmapWorkerTask extends AsyncTask {
    private String mPath;
    private final WeakReference imageViewReference;

    public BitmapWorkerTask(String path, ImageView imageView) {
        mPath = path;
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected BitmapDrawable doInBackground(String... params) {

        BitmapDrawable drawable = null;
        Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);

        //Bitmap转换成BitmapDrawable
        if (bitmap != null) {
            drawable = new BitmapDrawable(mResources, bitmap);
        }
        return drawable;
    }

    @Override
    protected void onPostExecute(BitmapDrawable value) {
        if (imageViewReference != null && value != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageDrawable(value);
            }
        }
    }
}


从上面的代码中可以看出,我们在构造函数中把图片路径和要显示图片的ImageView引入进来,图片读取并压缩完成后,ImageView显示该图片。这里我们并没有直接强引用ImageView,而是使用了弱引用(WeakReference),原因在于读取图片是耗时操作,有可能在图片未读取完成时,我们的ImageView已经被划出屏幕,这时候如果AsyncTask仍持有ImageView的强引用,那会阻止垃圾回收机制回收该ImageView,使用弱引用就不会阻止垃圾回收机制回收该ImageView,可以有效避免OOM。

 

定义好task后,我们可以这么来使用:


public void loadImage(String path, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);
    task.execute();
}


上面定义的AsyncTask中,decodeBitmapFromDisk(mPath, mImageWidth,mImageHeight)是压缩图片并读取出来的方法。


/**
 * 根据路径从硬盘中读取图片
 * @param path 图片路径
 * @param reqWidth 请求宽度(显示宽度)
 * @param reqHeight 请求高度(显示高度)
 * @return 图片Bitmap
 */
public Bitmap decodeBitmapFromDisk(String path, int reqWidth, int reqHeight) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, options);
    //初始压缩比例
    options.inSampleSize = calculateBitmapSize(options, reqWidth, reqHeight);
    options.inJustDecodeBounds = false;
    Bitmap bmp = BitmapFactory.decodeFile(path, options);
    return bmp;
}

/**
 * 计算压缩率
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public static int calculateBitmapSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

calculateBitmapSize这个方法的主要功能就是根据要显示的图片大小,计算压缩率,压缩原始图片并读出压缩后的图片,在上一篇博客里面有详细介绍。

 

接下来就是在gridview中,定义adapter,把图片显示出来,具体代码这里不贴出来,不是这篇博客的主要内容,后面我会把源码上传,博客中没有贴出来的代码,可以在源码中查看。

 

下面看我们做到这一步之后的效果。




可以看到效果很不流畅,滑动屏幕时,ImageView显示的图片一直在变换,原因一开始就讲过了,这是并发导致的。复用ImageView导致同个ImageView对应了多个AsyncTask,每个AsyncTask完成时都会改变ImageView显示的图片。而且AsyncTask完成顺序是不确定的,所以也会导致图片错位,本来应该显示1位置的图片的ImageView结果显示的21位置的图片。

3、处理并发性

 

         要解决上面的问题,我们就应该让一个ImgeView只对应一个AsyncTask,当有新的AsyncTask进入时,先看ImgeView上是否有AsyncTask正在执行,如果有,则取消该AsyncTask,然后把新的AsyncTask加入进来,这样不止解决了图片错位问题,同时也减少了没必要的AsyncTask,提高了加载效率。

 

定义一个持有AsyncTask弱引用的BitmapDrawable类


static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
                         BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
                new WeakReference(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

然后用imageView.setImageDrawable(asyncDrawable)把ImageView和AsyncDrawable绑定,这样就可以把ImageView与AsyncTask对应起来。

 

         为什么使用弱引用我们上面讲了,为什么AsyncTask要持有ImageView的引用我们上面也讲了,那么这里为什么要ImageView持有AsyncTask的引用呢?

 

         ImageView持有AsyncTask的引用,就可以通过ImageView找到其当前对应的AsyncTask,如果有新的AsyncTask进来,先比较是否和当前的AsyncTask一样,如果一样,则不把新的AsyncTask加入,如果不一样,先把当前对应的AsyncTask取消,再把新的AsyncTask与ImageView对应起来。

 

         这里还有一个问题,为什么不和前面AsyncTask持有ImageView弱引用一样,也在ImageView构造函数中让ImageView持有AsyncTask的弱引用就行,不用拐弯抹角的让ImageDrable持有AsyncTask的弱引用。这里要注意一下,我们的ImageView是复用的,也就是一般情况下,ImageView只构造了一次,如果ImageView直接持有AsyncTask的弱引用,那么只会持有ImageView刚构造时的那一个,而不会随着界面的滑动而更新AsyncTask。但是界面滑动时,ImageView的setImageDrawable方法却随着被触发,所以这里在ImageDrawable中持有AsyncTask的弱引用,然后ImageView通过getImageDrawable获得ImageDrawable,再通过ImageDrawable获得AsyncTask。

 

修改loadImage方法

public void loadImage(String path, ImageView imageView) {
    if (path == null || path.equals("")) {
        return;
    }
    BitmapDrawable bitmapDrawable = null;
   
    if (bitmapDrawable != null) {
        imageView.setImageDrawable(bitmapDrawable);
    } else if (cancelPotentialWork(path,imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);
        final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute();
    }
}

public static boolean cancelPotentialWork(String path, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final String bitmapData = bitmapWorkerTask.mPath;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == null|| !bitmapData.equals(path)) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
    if (imageView != null) {
        final Drawable drawable = imageView.getDrawable();
        if (drawable instanceof AsyncDrawable) {
            final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
            return asyncDrawable.getBitmapWorkerTask();
        }
    }
    return null;
}

修改BitmapWorkerTask 方法

class BitmapWorkerTask extends AsyncTask {
    private String mPath;
    private final WeakReference imageViewReference;

    public BitmapWorkerTask(String path, ImageView imageView) {
        mPath = path;
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected BitmapDrawable doInBackground(String... params) {

        BitmapDrawable drawable = null;
        Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);

        //Bitmap转换成BitmapDrawable
        if (bitmap != null) {
            drawable = new BitmapDrawable(mResources, bitmap);
        }
        return drawable;
    }

    @Override
    protected void onPostExecute(BitmapDrawable value) {
        if (isCancelled()) {
            value = null;
        }

        if (imageViewReference != null && value != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageDrawable(value);
            }
        }
    }
}

再看效果




已经运行比较流畅了。


4、图片缓存

 

         图片缓存,老的做法是使用SoftReference 或者WeakReference bitmap缓存,但是不推荐使用这种方式。因为从Android 2.3 (API Level 9) 开始,垃圾回收开始强制的回收掉soft/weak 引用从而导致这些缓存没有任何效率的提升。另外,在 Android 3.0 (API Level 11)之前,这些缓存的Bitmap数据保存在底层内存(nativememory)中,并且达到预定条件后也不会释放这些对象,从而可能导致程序超过内存限制并崩溃(OOM)。

 

         现在常用的做法是使用LRU算法来缓存图片,把最近使用到的Bitmap对象用强引用保存起来(保存到LinkedHashMap中),当缓存数量达到预定的值的时候,把不经常使用的对象删除。Android提供了LruCache类(在API 4之前可以使用SupportLibrary 中的类),里面封装了LRU算法,因此我们不需要自己实现,只要分配好内存空间就可以。

 

定义ImageCache类用于图片缓存。


public class ImageCache {

    private LruCache mMemoryCache;

    public ImageCache() {

        // 获取应用最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //用最大内存的1/4来缓存图片
        final int cacheSize = maxMemory / 4;
        mMemoryCache = new LruCache(cacheSize) {
            /**
             * Measure item size in kilobytes rather than units which is more practical
             * for a bitmap cache
             */
            @Override
            protected int sizeOf(String key, BitmapDrawable value) {
                Bitmap bitmap = value.getBitmap();
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    /**
     * Adds a bitmap to both memory and disk cache.
     * @param data Unique identifier for the bitmap to store
     * @param value The bitmap drawable to store
     */
    public void addBitmapToMemCache(String data, BitmapDrawable value) {

        if (data == null || value == null) {
            return;
        }

        // Add to memory cache
        if (mMemoryCache != null) {
            mMemoryCache.put(data, value);
        }

    }

    /**
     * Get from memory cache.
     *
     * @param data Unique identifier for which item to get
     * @return The bitmap drawable if found in cache, null otherwise
     */
    public BitmapDrawable getBitmapFromMemCache(String data) {

        BitmapDrawable memValue = null;

        if (mMemoryCache != null) {
            memValue = mMemoryCache.get(data);
        }
        return memValue;
    }

}

接下来就是修改获取图片的方法:先从内存缓存查找图片,找不到再开启task去硬盘读取。


public void loadImage(String path, ImageView imageView) {
    if (path == null || path.equals("")) {
        return;
    }
    BitmapDrawable bitmapDrawable = null;
    
    //先从缓存读取
    if(mImageCache != null){
        bitmapDrawable = mImageCache.getBitmapFromMemCache(path);
    }
    //读取不到再开启任务去硬盘读取
    if (bitmapDrawable != null) {
        imageView.setImageDrawable(bitmapDrawable);
    } else if (cancelPotentialWork(path,imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);
        final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute();
    }
}


task中读取到图片之后,同时把该图片加入到缓存中

class BitmapWorkerTask extends AsyncTask {
    private String mPath;
    private final WeakReference imageViewReference;

    public BitmapWorkerTask(String path, ImageView imageView) {
        mPath = path;
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected BitmapDrawable doInBackground(String... params) {

        BitmapDrawable drawable = null;
        Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);

        //Bitmap转换成BitmapDrawable
        if (bitmap != null) {
            drawable = new BitmapDrawable(mResources, bitmap);
            //缓存
            if(mImageCache!=null){
                mImageCache.addBitmapToMemCache(mPath, drawable);
            }
        }
        return drawable;
    }

    @Override
    protected void onPostExecute(BitmapDrawable value) {
        if (isCancelled()) {
            value = null;
        }

        if (imageViewReference != null && value != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageDrawable(value);
            }
        }
    }
}

再看效果

会发现第一次加载完图片之后,再往回滑动查看时,图片很快就显示出来。做到这一步其实就已经可以了,不过还可以继续优化。

 

Android 3.0 (API level 11)之后, BitmapFactory.Options提供了一个属性 inBitmap,该属性使得Bitmap解码器去尝试重用已有的bitmap,这样就可以减少内存的分配和释放,提高效率。

 

需要注意的是,在Android4.4之前,重用的bitmap大小必须一样,4.4之后,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小,所以实际上这个属性4.4之后的作用才比较明显

 

bitmapLruCache被移出时,将移出的bitmap以软引用的形式放进HashSet,用于后面的重用。


private LruCache mMemoryCache;
private Set> mReusableBitmaps;
if (Utils.hasHoneycomb()) {
    mReusableBitmaps = Collections.synchronizedSet(new HashSet>());
}

// 获取应用最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//用最大内存的1/4来缓存图片
final int cacheSize = maxMemory / 4;
mMemoryCache = new LruCache(cacheSize) {
    /**
     * Measure item size in kilobytes rather than units which is more practical
     * for a bitmap cache
     */
    @Override
    protected int sizeOf(String key, BitmapDrawable value) {
        Bitmap bitmap = value.getBitmap();
        return bitmap.getByteCount() / 1024;
    }

    /**
     * Notify the removed entry that is no longer being cached
     */
    @Override
    protected void entryRemoved(boolean evicted, String key,
                                BitmapDrawable oldValue, BitmapDrawable newValue) {

        // The removed entry is a standard BitmapDrawable
        if (Utils.hasHoneycomb()) {
            // We're running on Honeycomb or later, so add the bitmap
            // to a SoftReference set for possible use with inBitmap later
            mReusableBitmaps.add(new SoftReference(oldValue.getBitmap()));
        }
    }
};

在解码的时候,尝试使用 inBitmap。

public Bitmap decodeBitmapFromDisk(String path, int reqWidth, int reqHeight) {
    // BEGIN_INCLUDE (read_bitmap_dimensions)
    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateBitmapSize(options, reqWidth, reqHeight);
    // END_INCLUDE (read_bitmap_dimensions)

    // If we're running on Honeycomb or newer, try to use inBitmap
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options);
    }

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(path, options);
}

private  void addInBitmapOptions(BitmapFactory.Options options) {

    // inBitmap only works with mutable bitmaps so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (mImageCache != null) {
        // Try and find a bitmap to use for inBitmap
        Bitmap inBitmap = mImageCache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            options.inBitmap = inBitmap;
        }
    }
}

下面的方法从ReusableSet查找是否有可以重用的inBitmap

/**
 * @param options - BitmapFactory.Options with out* options populated
 * @return Bitmap that case be used for inBitmap
 */
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    Bitmap bitmap = null;
    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator> iterator = mReusableBitmaps.iterator();
            Bitmap item;
            while (iterator.hasNext()) {
                item = iterator.next().get();
                if (null != item && item.isMutable()) {
                    // Check to see it the item can be used for inBitmap
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;
                        // Remove from reusable set so it can't be used again
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    }

    return bitmap;

}

最后,下面方法确定候选位图尺寸是否满足inBitmap

/**
 * @param candidate - Bitmap to check
 * @param targetOptions - Options that have the out* value populated
 * @return true if candidate can be used for inBitmap re-use with
 *      targetOptions
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (!Utils.hasKitKat()) {
        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
        return candidate.getWidth() == targetOptions.outWidth
                && candidate.getHeight() == targetOptions.outHeight
                && targetOptions.inSampleSize == 1;
    }

    // From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap
    // is smaller than the reusable bitmap candidate allocation byte count.
    int width = targetOptions.outWidth / targetOptions.inSampleSize;
    int height = targetOptions.outHeight / targetOptions.inSampleSize;
    int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
    return byteCount <= candidate.getAllocationByteCount();

}

/**
 * Return the byte usage per pixel of a bitmap based on its configuration.
 * @param config The bitmap configuration.
 * @return The byte usage per pixel.
 */
private static int getBytesPerPixel(Bitmap.Config config) {
    if (config == Bitmap.Config.ARGB_8888) {
        return 4;
    } else if (config == Bitmap.Config.RGB_565) {
        return 2;
    } else if (config == Bitmap.Config.ARGB_4444) {
        return 2;
    } else if (config == Bitmap.Config.ALPHA_8) {
        return 1;
    }
    return 1;
}

自此就把本地相册图片加载到我们自己定义的gridview中,这个gridview要怎么设计,单选,多选,混合选,就随各位喜欢了。

 

其他更细致的优化都在源码里面

 

以上内容参考自官方文档。





你可能感兴趣的:(android+java)