Android相册实现
主要原理:
- LruCache缓存技术
- ImageLoad异步加载技术
- ContentProvider获取手机所有图片
- GridView显示图片
- CommonAdapter实现Adapter的通用适配
ContentProvider获取手机所有图片
- 使用ContentProvider获取所有图片
MediaStore.Images.Media.MIME_TYPE为Image格式的MIME类型
private void getImages() { imgs = new ArrayList<String>(); Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String key_MIME_TYPE = MediaStore.Images.Media.MIME_TYPE; String key_DATA = MediaStore.Images.Media.DATA; ContentResolver mContentResolver = getContentResolver(); Cursor cursor = mContentResolver.query(mImageUri, new String[] { key_DATA }, key_MIME_TYPE + "=? or " + key_MIME_TYPE + "=? or " + key_MIME_TYPE + "=?", new String[] { "image/jpg", "image/jpeg", "image/png" }, MediaStore.Images.Media.DATE_MODIFIED); if (cursor != null) { if (cursor.moveToLast()) { imgs = new ArrayList<String>(); while (true) { String path = cursor.getString(0); imgs.add(path); if (imgs.size() >= 10000 || !cursor.moveToPrevious()) { break; } } } cursor.close(); } }
使用LruCache缓存技术
LruCache类是该技术的核心,他的创建方法如下:
int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 8; LruCache lc = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } };
cacheSize为使用的最大内存,一般使用应用程序所允许的最大内存的1/8
- 当内存满了的时候他能自动地删除最近不常使用的资源
- 他的主要方法和Map类似为 put()和get(),保存图片资源时可以使用图片的路径作为key,Bitmap作为value。当想要使用某一张图片时,先通过路径索引lc,若没有获得则根据路径创建bitmap然后再把bitmap存入lc中
Bitmap的压缩技术
- 用一个小分辨率的ImageView来显示大分辨率的Bitmap不会有什么改变反而浪费了宝贵的内存,所以可以先把Bitmap压缩一定大小就可以节省这一部分内存
实现Bitmap压缩的API是BitmapFactory.Options,它可以在Bitmap加载的时候传进去,当我们把option.inJustDecodeBounds = true时,BitmapFactory加载Bitmap时不会生成Bitmap对象,但是可以获得Bitmap的宽高,我们就是根据这个宽高来进行压缩。option还有一个参数inSampleSize,当它是1时Bitmap大小不会改变,它是2时,Bitmap大小减少一倍,以此类推。这样,我们就可以得到压缩Bitmap的方法,下面是实现任意比例压缩的方法
public static int calculateInSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) { int inSampleSize = 1; int width = option.outWidth; int height = option.outHeight; if (width > reqWidth || height > reqHeight) { int halfWidth = width / 2; int halfHeight = height / 2; while ((halfWidth / inSampleSize) > reqWidth && (halfHeight / inSampleSize) > reqHeight) { inSampleSize *= 2; } } return inSampleSize; } public static Bitmap decodeSampleBitmapFromPath(String path, int reqWidth, int reqHeight) { Bitmap bitmap = null; BitmapFactory.Options option = new Options(); //获取Bitmap的宽高 option.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, option); int inSampleSize = calculateInSampleSize(option, reqWidth, reqHeight); option.inJustDecodeBounds = false; option.inSampleSize = inSampleSize; bitmap = BitmapFactory.decodeFile(path, option); return bitmap; }
结合Lrucache实现异步缓存加载Bitmap的ImageLoad
- 异步加载就是开启一个线程去加载bitmap,实现一个回调接口在加载完成时调用便达到异步加载
- 关键的API为 Handler的异步回调机制
我们先来定义一个接口用来回调时调用
public interface ImageCallback { public void imageLoaded(Bitmap imageDrawable); }
当需要加载Bitmap时,我们先在lc中找
if (lc.get(item) != null) { return lc.get(item); }
如果没有找到就开启一个线程在通过路径加载
new Thread() { public void run() { try { final Bitmap bmp = BitmapUtils.decodeSampleBitmapFromPath(item, 100, 100); lc.put(item, bmp); mHander.post(new Runnable() {//这个运行在主线程 @Override public void run() { callback.imageLoaded(bmp);//加载完成后回调 } }); } catch (Exception e) { e.printStackTrace(); } } }.start();
当然要先实现回调接口
new ImageCallback() { @Override public void imageLoaded(Bitmap bmp) { if (imageView.getTag().equals(filePath)) { if (bmp != null) { imageView.setImageBitmap(bmp); } else { Log.d("asd", "imageLoaded"); imageView.setImageResource(R.drawable.empty_photo); } } } }
下面是所有的代码
public class ImageLoader { private LruCache<String, Bitmap> lc; private int screenW, screenH; private Handler mHander = new Handler(); public ImageLoader() { int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 8; lc = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; } private Bitmap loadDrawable(final String item, final ImageCallback callback) { if (lc.get(item) != null) { return lc.get(item); } new Thread() { public void run() { try { final Bitmap bmp = BitmapUtils.decodeSampleBitmapFromPath(item, 100, 100); lc.put(item, bmp); mHander.post(new Runnable() { @Override public void run() { callback.imageLoaded(bmp); } }); } catch (Exception e) { e.printStackTrace(); } } }.start(); return null; } public void loadImage(final String filePath, final ImageView imageView) { Bitmap bmp = loadDrawable(filePath, new ImageCallback() { @Override public void imageLoaded(Bitmap bmp) { if (imageView.getTag().equals(filePath)) { if (bmp != null) { imageView.setImageBitmap(bmp); } else { Log.d("asd", "imageLoaded"); imageView.setImageResource(R.drawable.empty_photo); } } } }); if (bmp != null) { if (imageView.getTag().equals(filePath)) { imageView.setImageBitmap(bmp); } } else { imageView.setImageResource(R.drawable.empty_photo); } } public interface ImageCallback { public void imageLoaded(Bitmap imageDrawable); } }
到处,异步加载图片的方法就介绍完成了,用的时候只要创建ImageLoad对象,然后调用它的
public void loadImage(final String filePath, final ImageView imageView)方法就可以了
实现一个通用的ViewHolder
- 在编写Adapter的时候,一般都会用到ViewHolder的方法,所以,实现一个通用的ViewHolder可以减少开发时间
因为每个ViewHolder的控件不同,所以我们需要一个集合在保存这些控件,又为了能获得响应的控件,我们有需要可以根据标记提取控件,所以使用map的机制能很好的实现这一点。在key的选择上,我们可以选择控件的id,这样可以做到key的唯一,也能根据id来获取控件。此外,ViewHolder应该和view相联系,所以需要使用view.setTag()方法保存ViewHolder,以下是全部代码
public class ViewHolder { private View mConverView; private SparseArray<View> mView;//使用整形数据来作为key private ViewHolder(Context context, ViewGroup parent, int layoutId, int position) { mView = new SparseArray<View>(); mConverView = LayoutInflater.from(context).inflate(layoutId, parent, false); mConverView.setTag(this); } public static ViewHolder getViewHolder(Context context, ViewGroup parent, View convertView, int layoutId, int position) { if(convertView == null){ return new ViewHolder(context, parent, layoutId, position); }else{ return (ViewHolder) convertView.getTag(); } } public <T extends View> T getView(int viewId) { View view = mView.get(viewId); if(view == null){ view = mConverView.findViewById(viewId); mView.put(viewId, view); } return (T) view; } public View getConvertView() { return mConverView; } }
使用通用的ViewHolder的方法就是在Adapter的getview中使用getViewHolder方法获得viewholder,在通过getview获得要控件,然后就可以执行了
实现一个通用的Adapter
在构造方法中传入context、布局id和list,因为要适配所有的bean,所以要使用泛型。这样就可以得到一个初步的通用Adapter
public abstract class CommontAdapter<T> extends BaseAdapter { private Context mContext; private List<T> mList; private int id; private LayoutInflater inflater; public CommontAdapter(Context context, List<T> list, int layouId) { mContext = context; mList = list; id = layouId; inflater = LayoutInflater.from(context); } @Override public int getCount() { return mList.size(); } @Override public T getItem(int position) { return mList.get(position); } @Override public long getItemId(int position) { return position; } }
可以看到我们的通用Adapter是个抽象类,我们并没有实现getView()方法,因为我们在使用Adapter的时候他的其他三个方法通常都是固定的,只有getview方法需要实现多一点的逻辑,所以我们把他留给创建对象的时候使用,这样我们就可以直接创建一个内部类实现getview方法就可以轻松地实现一个Adapter。
接下来我们再看看getview还是有可以改进的部分,一般的getview基本上都是这样:
public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position);// 获得Fruit对象 View view;// 加载布局 ViewHolder viewHolder = new ViewHolder(); if(convertView == null){ view = LayoutInflater.from(getContext()).inflate(resourceId, null); viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_iv); viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_tv); view.setTag(viewHolder); }else{ view=convertView; viewHolder=(ViewHolder) view.getTag(); } viewHolder.fruitImage.setImageResource(fruit.getImageId()); viewHolder.fruitName.setText(fruit.getName()); return view; }
我们可以看到获得bean对象和viewholder和return都是固定不需要改变的,所以我们可以写死这一点,我们再创建一个convert的抽象方法,然后在getview中调用就可以减少很多代码
public View getView(int position, View convertView, ViewGroup parent) { final ViewHolder holder = ViewHolder.getViewHolder(mContext, parent, convertView, id, position); convert(holder, getItem(position), position); return holder.getConvertView(); }
这样我们可以直接在使用通用Adapter的时候直接内部类实现convert方法就可以了
mListView.setAdapter(mAdapter = new CommonAdapter<String>( getApplicationContext(), mDatas, R.layout.item_single_str) { @Override public void convert(ViewHolder c, String item) { TextView view = viewHolder.getView(R.id.id_tv_title); view.setText(item); } });
实现相片选择器
- 在经过一系列的准备后,我们现在已经可以去实现一个相片选择器了
我们先获得手机的所有相片和做一些初始化,如ImageLoad
getImages(); imageLoader = new ImageLoader();
然后我们获得GridView并设置适配器,我们可以选择通用的适配器
gv_photo.setAdapter(new CommontAdapter<String>(getApplicationContext(), imgs, R.layout.item_photo){ @Override public void convert(ViewHolder holder, String item, int position) { if (position == 0) { ImageView iv = holder.getView(R.id.iv_photo); CheckBox cb = holder.getView(R.id.cb_photo); ImageView iv_cramer = holder.getView(R.id.iv_cramer_photo); iv_cramer.setVisibility(View.VISIBLE); cb.setVisibility(View.INVISIBLE); iv.setVisibility(View.INVISIBLE); iv.setOnClickListener(null); iv_cramer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(intent, 200); } }); return; } ImageView iv = holder.getView(R.id.iv_photo); CheckBox cb = holder.getView(R.id.cb_photo); ImageView iv_cramer = holder.getView(R.id.iv_cramer_photo); iv_cramer.setVisibility(View.INVISIBLE); cb.setVisibility(View.VISIBLE); iv.setVisibility(View.VISIBLE); cb.setTag(R.id.cb_photo, position); cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { sparesMap.put((Integer) buttonView.getTag(R.id.cb_photo), isChecked); } }); cb.setChecked(sparesMap.get(position)); iv.setTag(item); imageLoader.loadImage(item, iv); } });
这样我们就可以实现一个相片选择器了