之前在工作项目的时候遇到过要获取手机上所有图片信息的需求,也就是要在自己应用内部做一个图片选择器的功能,当产品提出这个问题的时候我当时的想法就很怀疑这个需要合理,后来我就在github上搜索到了一个挺好的图片选择的库:https://github.com/learnNcode/MediaChooser,后来集成到项目中的时候发现居然系统的相册的功能差不多的,都可以扫描出手机上的图片,而且毫无遗漏的。刚好最近有时间无聊了就看看各种的源代码的具体实现。
这些日子仔细的看了一下里面的代码发现其实不管是微信相册还是QQ相片选择器,还是市面上各种各样的相册软件最后都是通过调用系统的contentProvider的uri来获取手机上图片的信息,因为这些信息都保存在数据库里,并且记录了图片的种种信息,比如图片的存储路径,经纬度,mimeType,大小,文件时间,修改时间等等一系列的信息都保存在数据库中,我们只要去读取该数据库的信息,然后再使用一个图片加载的框架去显示这些图片就可以了。但是系统为了安全处理并没有让我们直接去访问数据库而且通过ContentProvider暴漏出Uri来供外部调用的。
首先首先看看微信相册选择框架的效果图:
除了能多选多张图片之外我们可以选择不同文件夹的下的图片和视频的,我们就按照它的样式自己动手实现一个,因为之前用习惯了universal-image-loader,而且感觉用的也不错的。Facebook的fresco虽然比较省内存,但是体积非常的大,所以我感觉并不怎么适合使用的。其实你也可以使用别的框架,都差不多的。
代码实现
Gradle
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
接下来我们需要自定义Application,并且重写onCreate()方法,然后配置好ImageLoader的信息,这里我们还使用了StickyGridHeader来进行分配展示图片
private void initImageLoader() {
DisplayImageOptions.Builder builder = new DisplayImageOptions.Builder();
builder.cacheInMemory(true).cacheOnDisk(true).bitmapConfig(Bitmap.Config.RGB_565);
builder.imageScaleType(ImageScaleType.IN_SAMPLE_INT);
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(sInstance)
.defaultDisplayImageOptions(builder.build())
.threadPoolSize(3).build();
ImageLoader.getInstance().init(configuration);
}
我们重新写一个集成BaseAdapter的基类,所有有关图片展示的类都继承该类
public abstract class CommonAdapter<T> extends BaseAdapter {
protected List<T> mList;
protected Context mContext;
protected ImageLoader mImageLoader;
protected DisplayImageOptions mOptions;
protected DisplayImageOptions.Builder mBuilder;
public CommonAdapter(Context context) {
this.mContext = context;
this.mList = new ArrayList<>();
mImageLoader = ImageLoader.getInstance();
mBuilder = new DisplayImageOptions.Builder();
mBuilder.bitmapConfig(Bitmap.Config.RGB_565);
mBuilder.cacheInMemory(true);
mBuilder.cacheOnDisk(true);
}
public void setItems(List<T> datas) {
if(datas != null && datas.size() > 0) {
mList.clear();
mList.addAll(datas);
notifyDataSetChanged();
}
}
public void addItem(T data) {
if(mList != null) {
mList.add(data);
notifyDataSetChanged();
}
}
public void addItems(List<T> datas) {
if(datas != null && datas.size() > 0) {
mList.addAll(datas);
notifyDataSetChanged();
}
}
public void clearAll(boolean refresh) {
mList.clear();
if(refresh) {
notifyDataSetChanged();
}
}
public List<T> getList() {
return mList;
}
public ImageLoader getImageLoader() {
return mImageLoader;
}
@Override
public int getCount() {
return mList == null ? 0 : mList.size();
}
@Override
public T getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
abstract public View getView(int position, View convertView, ViewGroup parent);
}
当基础工作都做好了之后我们首先来获取相册文件夹的信息,因为我们知道图片可以存放在不同的文件夹中的,然后文件夹的话也是有名字的,同时每个文件夹下肯定会存放了不同数量的图片的。
获取所有的图片文件夹信息
//查询相册文件夹的单位信息
private static final String PROJECTION_BUCKET[] = {
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATA,
};
private void loaData() {
//根据文件创建的时间降序排序
final String orderBy = MediaStore.Images.Media.DATE_TAKEN;
List<BucketInfo> list = new ArrayList();
Cursor cursor = null;
try {
/**
* EXTERNAL_CONTENT_URI 这是本次代码最核心的uri了,我们就可以通过contentProvider
* 来读取系统的数据库来获取图片文件夹信息了。然后就是我们需要获取哪些数据库列,以及条件等等
*/
cursor = mContext.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION_BUCKET, null, null, orderBy + " DESC");
if (cursor != null) {
while (cursor.moveToNext()) {
BucketInfo entry = new BucketInfo();
//获取文件夹的id
entry.setBucketId(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID)));
//该名字是图片的名字,是从该图片路径上所截取下来的
entry.setDisplayName(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)));
//获取相册文件夹显示的名字
entry.setBucketName(cursor.getString(
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)));
//获取第一张图片用于文件夹封面展示的
entry.setBucketUrl(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)));
/**
* 这里需要对重复的bucketInfo进行判断,因为我们在查找bucket的时候,
* 其实也是在图片信息表中进行查询的,所以这里会有很多重复的bucketInfo的,
* 所以需要判断重复的问题,后面我们会讲明理由的
*/
if (!list.contains(entry)) {
list.add(entry);
}
}
}
} catch (Exception e) {
if (cursor != null) {
cursor.close();
}
}
上述代码就是本次获取图片文件夹最核心的,其中EXTERNAL_CONTENT_URI
也是本文中最关键的一部分,因为我们只有通过该uri来获取手机上存放的图片信息。但是我们本次获取的仅仅只是图片文件夹的一些信息的。因为在查找的过程中肯定是比较耗时的我们需要把这段代码放到线程中去执行的。
上面我们仅仅获取了所有图片的文件夹信息的,接下来我们需要获取各个图片文件夹下的对应图片的信息。我们根据bucketName为查找条件,然后查找所有的对应的图片信息
根据图片文件夹名字查找对应的信息
//格式化字符串
private String mFormatType = "yyyy-MM-dd";
/**
* 查询的信息列,Media。DATA表示图片的存放路径,Media.DATE_TAKEN表示图片创建的时间
*/
Media._ID是图片的一个唯一标识
private String[] PROJECTION_BUCKET = {
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media._ID
};
private void loadData(String bucket) {
Cursor cursor = null;
List<MediaInfo> list = null;
try {
//根据文件创建时间进行降序排序
final String orderBy = MediaStore.Images.Media.DATE_TAKEN;
//根据bucketName进行查找
String searchParams = "bucket_display_name = \"" + bucket + "\"";
//现在我们还是通过 EXTERNAL_CONTENT_URI进行查询的,由此我们可知该地址专门访问图片的
cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION_BUCKET, searchParams, null, orderBy + " DESC");
list = getDataFromCursor(cursor);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
Message msg = mHandler.obtainMessage();
msg.what = 1;
msg.obj = list;
mHandler.sendMessage(msg);
}
private List<MediaInfo> getDataFromCursor(Cursor cursor) {
List<MediaInfo> list = new ArrayList<>();
if (cursor != null && cursor.getCount() > 0) {
MediaInfo mediaInfo;
while (cursor.moveToNext()) {
mediaInfo = new MediaInfo();
//根据Media.DATA字段获取图片的存放路径
mediaInfo.setFilePath(cursor.getString(
cursor.getColumnIndexOrThrow((MediaStore.Images.Media.DATA))));
//根据Media.DATE_TAKEN获取图片的创建时间
mediaInfo.setDisplaytime(cursor.getLong(
cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)));
//对时间进行格式化为"yyyyMMdd"格式,根据时间进行排序操作
mediaInfo.setDisplaytime(TimeUtils.parseTime(mFormatType,
TimeUtils.formatTime(mFormatType, mediaInfo.getDisplaytime())));
list.add(mediaInfo);
}
}
return list;
}
上述代码就是根据bucketName为搜索条件查找对应的图片信息。也没有什么难度的,只是查找的条件不同而已的,其实最核心的东西就是这些的。下面我们就来查找所有的图片信息。
获取手机上所有图片信息
private void loadData() {
//根据时间的降序来获取图片信息
final String orderBy = MediaStore.Images.Media.DATE_TAKEN + " DESC";
ContentResolver contentResolver = mContext.getContentResolver();
List<ImageInfo> imageInfos = null;
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION_BUCKET, null, null, orderBy);
if (cursor != null && cursor.getCount() > 0) {
ImageInfo imageInfo;
File file;
imageInfos = new ArrayList();
while (cursor.moveToNext()) {
//获取图片存储的实际路径
String filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
/**
* 这里需要判断该路径的图片是否存在,因为有时候图片删除了,
* 但是系统并没有立刻就发现,所以数据库中的信息也并没有删除的。所以就会存在这种问题
*/
if(TextUtils.isEmpty(filePath)) continue;
file = new File(filePath);
if (!file.exists()) continue;
//获取图片大小
long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE));
imageInfo = new ImageInfo();
imageInfo.setFilePath(filePath);
//获取图片的创建时间
imageInfo.setCreateTime(cursor.getLong(
cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN)));
imageInfo.setFileSize(fileSize);
//格式化时间,省略时分秒,只记录日期的秒数
imageInfo.setDisplaytime(TimeUtils.parseTime(mFormatType,
TimeUtils.formatTime(mFormatType, imageInfo.getCreateTime())));
imageInfos.add(imageInfo);
}
}
if(cursor != null) {
cursor.close();
}
//对所有的图片根据创建时间进行降序排序
if (imageInfos != null && imageInfos.size() > 1) {
Collections.sort(imageInfos, new Comparator<ImageInfo>() {
@Override
public int compare(ImageInfo imageInfo1, ImageInfo imageInfo2) {
if (imageInfo1.getDisplaytime() < imageInfo2.getDisplaytime()) {
return 1;
} else if (imageInfo1.getDisplaytime() > imageInfo2.getDisplaytime()) {
return -1;
}
return 0;
}
});
}
Message msg = mHandler.obtainMessage();
if (imageInfos != null && imageInfos.size() > 0) {
msg.obj = imageInfos;
msg.what = 1;
mHandler.sendMessage(msg);
} else {
msg.what = -1;
mHandler.sendMessage(msg);
}
}
从上面的代码中我们发现获取手机上”所有”图片是非常简单的,并没有涉及到什么高深的东西,只是调用一下系统的contentProvider的uri来查找系统数据库中的信息封装好数据之后就使用ListView或者是GridView来进行显示,在展示ImageView的时只要注意不要内存溢出就行了,这个时候我们就找一个开源的图片加载框架来进行展示一下就可以了。注意: 在获取图片信息具体路径的时候,这个时候我们需要对该路径是否存在图片应该先检测一下,因为有时候用户可能将系统中的图片删除了,如果这个时候用户没有主动的告诉系统删除了某张图片的话,系统并不会马上之情的,因为系统是在某些特定的情况去扫描整个SD目录的,并不会时时的去扫描的,如果进行时时扫描的话这个对于系统的开销是非常大的,所以会出现图片删除了,但是系统中记录的的图片信息并没有马上删除,所以这个时候就要进行判断一下。
总结
上面就是获取系统图片的方案,其实市面上所有的相册选择框架都是基于该原理进行做的,只不过是他们把那些界面做的更好看了,然后加入了更多好看的动画效果,但是其最核心最关键的获取手机图片信息的原理就是这样子的。其实获取手机图片信息有以下两种方法:1. 就是不断的去扫描SD卡的各个目录,然后将后缀名为.png或者是其他格式的文件的信息保存起来,然后存放到数据库里面去, 2. 还有就是我们在保存一张图片的时候主动的发送一个广播出去告诉系统自己图片的信息。但是如果我们的程序不断的去扫描SD卡的各个目录然后获取图片信息的话显然是非常不现实的,所以这个时候系统就为我们做了很多我们做不了的事情,没个一段的时间去扫描或者是其他程序主动告诉系统图片存放的信息然后保存起来,而我们只要去调用系统的api查询数据库就行了,这样子岂不是一举两得呢?