现在大多数app都有选择图片上传的功能,自己开发的SDK有类似的功能,所以就参考些许资料自己实现了下,虽然不是太复杂的功能,但是涉及到的知识点还是可圈可点的,所以博主在此总结下来供自己以后参阅,算是个学习笔记。前后下来博主一共实现了三个版本才算满意。
实现效果如下:
点击上面图片listView的一个item后会跳转到另外一个页面:
自己的SDK实现的功能简单些就是点击ListView的一个item,然后跳转到RecycleView来显示该item对应文件夹下的所有图片,然后点击RecycleView的一个图片进行裁剪然后进行图片上传(文件上传用的是自己封装的Okhttp工具来完成)。
单单从图片上来看没什么难处,就是一个两个RecycleView对象添加和显示数据而已,相信大家对这两个控件的用法都很了解(此乃废话),但是其中涉及到如下几个问题可以思考:
1、怎么在手机里众多的文件夹中扫描获取图片集合?
2、如果手机里图片过大的情况下怎么减少内存的开销?能否向网页哪样在数据多的情况下分页展示?如果可以怎么实现?这又涉及到RecycleView的上拉刷新问题。
3、android 6.0的权限管理问题
4、图片选择器的图片使用框架的可插拔性:就是说可以使用Glide,Fresco的等图片框架加载图片来进行切换图片库。
在博主拿到这个设计图的时候,参考了鸿洋大神的博客,然后取其核心逻辑实现了一版,但是问题就出来了,鸿洋大神的博客在点击ListView的item时候具体思路就是根据item代表的文件夹对其进行过滤:
public class ImageFileFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String fileName) {
return fileName.endsWith(".jpg")
|| fileName.endsWith(".png")
|| fileName.endsWith(".jpeg");
}
}
并没有进行分页处理,如果该文件夹下图片量过大的话,内存消耗厉害且可能会OOM,于是博主第一个版本就此推翻。
第二个版本借鉴了RxGalleryFinal这个库,对其核心代码提取修改来实现了上图的功能。
该版本相对于第一个版本来说主要是三大改进:
1、实现了分页加载逻辑
2、分页加载就避免不了RecycleView的上拉刷新功能
3、为每一次查询得到的图片开启线程设置一个缩略图
分页加载的核心代码是:
public static List getImages(Context context, String floderId, int page, int pageSize) {
int offset = (page - 1) * pageSize;//计算偏移量
/*将查询到的图片信息封装成ImageBean对象*/
List imageBeanList = new ArrayList<>();
ContentResolver contentResolver = context.getContentResolver();
List projection = new ArrayList<>();
projection.add(MediaStore.Images.Media._ID);
projection.add(MediaStore.Images.Media.DATA);
projection.add(MediaStore.Images.Media.BUCKET_ID);
String selection = null;
String[] selectionArgs = null;
if (!TextUtils.equals(floderId, String.valueOf(Integer.MIN_VALUE))) {
selection = MediaStore.Images.Media.BUCKET_ID + "=?";
selectionArgs = new String[]{floderId};
}
/**分页加载的核心查询DESC LIMIT**/
Cursor cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection.toArray(new String[projection.size()]), selection,
selectionArgs, MediaStore.Images.Media.DATE_ADDED + " DESC LIMIT " + pageSize + " OFFSET " + offset);
if (cursor != null) {
int count = cursor.getCount();
if (count > 0) {
cursor.moveToFirst();
do {
ImageBean imageBean = parseImageCursorAndCreateThumImage(context, cursor);
if (imageBean != null) {
imageBeanList.add(imageBean);
}
} while (cursor.moveToNext());
}
}
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
return imageBeanList;
}
/*将图片数据信息转换成ImageBean对象*/
private static ImageBean parseImageCursorAndCreateThumImage(Context context, Cursor cursor) {
String originalPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
if (TextUtils.isEmpty(originalPath) || !new File(originalPath).exists()) {
return null;
}
ImageBean imageBean = new ImageBean();
imageBean.id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
imageBean.originalPath = originalPath;
imageBean.floderId = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID));
File thumbNailPathFile = createThumbnailFileName(context, originalPath);
imageBean.thumbnailPath = thumbNailPathFile.getAbsolutePath();
return imageBean;
}
通过上面的代码可以看出其实分页加载的核心原理也很简单:
1、通过拼接查询语句的DESC LIMIT来限制当前页和每页加载的图片数目:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection.toArray(new String[projection.size()]), selection,
selectionArgs, MediaStore.Images.Media.DATE_ADDED + " DESC LIMIT " + pageSize + " OFFSET " + offset);
2、将图片信息通过parseImageCursorAndCreateThumImage方法封装成ImageBean对象来交给RecycleView来使用
RecycleView在每次上拉刷新的时候,调用上述代码的getImages方法传入合适的页码和每页显示的图片个数即可。然后将查询到的数据集合拼接到总的List集合里面,调用RecycleView对应Adapter的imageFloderAdapter.notifyDataSetChanged();方法即可。
实现RecycleView上拉刷新的核心代码如下:
private class RecyclerViewOnScrollListener extends OnScrollListener {
/**
* 最后一个的位置
*/
private int[] lastPositions;
/**
* 最后一个可见的item的位置
*/
private int lastVisibleItemPosition;
/**
* 当前滑动的状态
*/
private int currentScrollState = 0;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
if (lastPositions == null) {
lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
}
staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
lastVisibleItemPosition = findMax(lastPositions);
} else {
throw new RuntimeException(
"Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
currentScrollState = newState;
LayoutManager layoutManager = recyclerView.getLayoutManager();
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
if ((visibleItemCount > 0 && currentScrollState == RecyclerView.SCROLL_STATE_IDLE && (lastVisibleItemPosition) >= totalItemCount - 1)) {
if (mHasMore) {
/**实现下拉刷新的操作,具体的就是调用getImages方法**/
executeLoadMore();
}
}
}
/**
* 取数组中最大值
*/
private int findMax(int[] lastPositions) {
int max = lastPositions[0];
for (int value : lastPositions) {
if (value > max) {
max = value;
}
}
return max;
}
}
如果想看具体的运行效果,可以参考RxGalleryFinal这个库,里面既有下拉刷新的RecycleView也有其实现效果,因为这是博主参考RxGalleryFinal实现的第二个图片选择器版本,不是最终版本,所以代码就提供了,更完善的功能参考RxGalleryFinal即可:因为博主就是参考这个库取其核心逻辑来完善的自己的功能。
因为是实现分页的加载的逻辑以及创建了缩略图,使得内存消耗降低了不少,但是问题出现了,就是因为分页加载的逻辑导致着图片选择器没有微信图片选择器使用的那么顺畅:因为分页加载的时候页面总会卡顿,没法嗖的一下子滑动的爽快感。再者每次查询的时候都要将图片封装成大量的ImageBean对象,随着页数的增加ImageBean的集合也会越来越多,内存也会上升。所以继续第二个版本就推翻了要重新设计。
在这种背景下,版本三问世了,该版本参考了github的一个图片选择库Matisse的实现理念,博主分析了其原有核心实现原理之后,发挥了拿来注意精神,改吧改吧就完成了第三版。
该框架的原理很简单:
使用了LoaderManager来完成图片的加载,当load 完成后会执行LoaderManager.LoaderCallbacks 接口的onLoadFinished(Loader loader, Cursor data)方法,然后将第二个参数data这个Cursor对象交给RecycleView即可。
前两个版本的加载核心是,获取到Cursor对象之后进行一个循环:
cursor.moveToFirst();
do {
ImageBean imageBean = parseImageCursorAndCreateThumImage(context, cursor);
if (imageBean != null) {
imageBeanList.add(imageBean);
}
} while (cursor.moveToNext());
当前循环结束后cursor.moveToNext()来获取下一个图片的信息,然后将图片信息封装成一个List集合交给RecycleView来使用。
而第三个版本主要是调用了cursor.moveToPosition(position)方法来完成,其Adapter的onBindViewHolder代码如下:
protected abstract void onBindViewHolder(VH holder, Cursor cursor);
@Override
public void onBindViewHolder(VH holder, int position) {
if (!mCursor.moveToPosition(position)) {
return;
}
onBindViewHolder(holder, mCursor);
}
protected void onBindViewHolder(RecyclerView.ViewHolder holder, Cursor cursor) {
GridViewHolder gridViewHolder = (GridViewHolder) holder;
final String imageUrl = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
gridViewHolder.mImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onClick(imageUrl);
}
}
});
VenvyImageInfo venvyImageInfo =
new VenvyImageInfo.Builder().setUrl(imageUrl)
.setResizeWidth(70)
.setResizeHeight(70)
.setPlaceHolderImage(placeHolderDrawable)
.isLocalMedia(true).build();
gridViewHolder.mImageView.loadImage(venvyImageInfo);
}
以上就是图片选择器的核心业务逻辑,如果看到此处感觉模棱两可的话,不用着急除了参考RxGalleryFinal,Matisse两个实现之外,博文后面还会贴上第三个版本的代码源码供参考。
上面的两个View是用Dialog来完成的,没有用到Activity,使用如下调用就可以出来:
/*使用Glide加载方式*/
RegisterLibsManager.registerImageLoaderLib(GlideImageLoader.class);
/*使用Fresco加载方式*/
RegisterLibsManager.registerImageLoaderLib(FrescoImageLoader.class);
RegisterLibsManager.registerImageViewLib(FrescoImageView.class);
ImageScannerDialog imageScannerDialog = new ImageScannerDialog(this);
imageScannerDialog.show();
别忘了添加权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
因为本片着重讲的是图片选择的功能,所以点击GridView的item事件,进行图片的裁减能功能就没有做说明。
到此位置图片选择器就完成了,本文只提供第三个版本的源码实现,源码奉上