这是一个基于RecyclerView实现的本地读取图片的简单demo,具有切换布局,重命名和删除图片的功能,读取图片的效果还不错,在机器上插入U盘读取了几千张图片速度很不错,也没有OOM。那么,首先摆上效果图:
读取图片的功能非常简单,首先炮台架好,写好RecyclerView相关的布局和自定义适配器,那么接下来就需要寻找炮弹(图片),怎样来寻找炮弹,这里就需要一个专业的探测仪,而探测仪找炮弹,首先得在自家的弹药库里找下,如果有炮弹那肯定直接拿来用了,没有才去别地再找呗。好吧,老铁别着急。其实我是想说,这里我使用了软引用(SoftReference)这个技术-弹药库,softReference会在读取图片的时候以cache的形式保存在本地,当再次需要这个图片的资源数据时就不需要再去读取,可以通过softReference的get()方法直接拿到。但是软引用也是有一个坏处,它不管你手机有多大的内存空间,你读取多少图片,它就缓存多少图片,不可控制,只有当内存触发OOM时,GC会去回收这些图片,从而避免OOM。当然老铁你可以去试下LRU算法(最近最少使用算法),可控制最大缓存大小,一旦超出容量马上回收,这样就不用担心控制不了它了。
关于RecyclerView有3个布局管理器,分别为:
LinearLayoutManager、
GridLayoutManager、
StaggeredGridLayoutManager,并且这个组件需要自己写接口回调来实现点击、长按事件。。。。另外需要在build.gradle中添加相关的依赖包才能使用RecyclerView。万事俱备,只欠撸码,那么开始撸吧。
RecyclerView的基本使用
recyclerView = (RecyclerView) findViewById(R.id.picture_recyclerView);
this.registerForContextMenu(recyclerView);//切换布局
pictureAdapter = new PictureAdapter(this, mFileList, imageLoader);
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));//布局管理器
recyclerView.setAdapter(pictureAdapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());//使用默认的动画效果
自定义适配器:PictureAdapter
public class PictureAdapter extends RecyclerView.Adapter<PictureAdapter.PictureViewHolder> {
/**
* 接口回调 点击、长按事件
*/
public interface OnItemClickListener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
private OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
@Override
public PictureViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
PictureViewHolder holder = new PictureViewHolder(mInflater.inflate(R.layout.activity_file_grid_item, viewGroup, false));
return holder;
}
@Override
public void onBindViewHolder(final PictureViewHolder holder, final int position) {
if (mOnItemClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getLayoutPosition();
mOnItemClickListener.onItemClick(holder.itemView, position);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
int postion = holder.getLayoutPosition();
mOnItemClickListener.onItemLongClick(holder.itemView, postion);
return false;
}
});
}
}
@Override
public int getItemCount() {
return mFileList.size();
}
class PictureViewHolder extends RecyclerView.ViewHolder{
public PictureViewHolder(View itemView) {
super(itemView);
}
}
}
到这里已经自定义适配器和接口回调事件定义完成,RecyclerView的基本框架已经实现,接下来实现布局切换功能:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.picture_staggered, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.id_action_gridView:
recyclerView.setLayoutManager(new GridLayoutManager(PictureRecyclerActivity.this, 4));
break;
case R.id.id_action_listView:
recyclerView.setLayoutManager(new LinearLayoutManager(this));
break;
case R.id.id_action_horizontalGridView:
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.HORIZONTAL));
break;
}
return super.onOptionsItemSelected(item);
}
另外关于menu中item的一些属性,比如showAsAction有三个可选项:
always:总是显示在界面上
never:不显示在界面上,只让出现在右边的三个点中
ifRoom:如果有位置才显示,不然就出现在右边的三个点中
orderInCategory:设置优先级,值越大优先级越低。
android中有四大常用的线程池,接下来会用到其中的2种:
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务
那么到这里炮台搭建已经完成,接下来制作探测仪来寻找图片,
首先获取所有挂载设备的路径,如果没有就在本机中匹配图片:
/**
* 系统隐藏该方法,通过反射调用
* @return 挂载的所以存储设备
*/
public static String[] getVolumePaths(Context context) {
StorageManager manager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
try {
Method method = StorageManager.class.getMethod("getVolumePaths");
return (String[]) method.invoke(manager);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
返回当前所有存储设备的路径
public static void updateRootList(Context context) {
mRootFile.clear();
String[] paths = FileUtils.getVolumePaths(context);
if (paths == null || paths.length == 0) {
mRootFile.add(new File(Constants.SDCARD_PATH));
return;
}
for (String path : paths) {
File file = new File(path);
File[] files = file.listFiles();
if (files != null && files.length != 0) {
mRootFile.add(file);
}
}
if (mRootFile.isEmpty()) {
mRootFile.add(new File(Constants.SDCARD_PATH));
}
}
根据返回的存储设备的路径,去过滤设备文件匹配出所有的图片资源:
private void listAllFiles(File dir) {
if (dir == null)
return;
//列出所以文件过滤
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return FileUtils.isQualified(pathname);
}
});
if (files == null || files.length < 1) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
listAllFiles(file);
} else if (FileUtils.isImageFile(file)) {
File parent = file.getParentFile();
if (!mImageDirList.contains(parent)) {
mImageDirList.add(parent);
}
}
}
}
在读取图片的时候考虑到避免二次加载,所以使用了软引用,需要将获取的所有图片路径去和本地缓存的图片路径去匹配,如果之前缓存过的图片就不需要二次加载,可以直接获取。而没有缓存的图片就需要重新加载,在这个加载过程里有些路径下可能并不能找到图片,或者图片错误,都需要具现,比如说显示一张错误图片的方式显示出来:
public void loadImage(final String pathName, final ImageCallback callback) {
if (TextUtils.isEmpty(pathName)) {
callback.onError(pathName);
return;
}
if (mCaches.containsKey(pathName)) {
Bitmap bitmap = mCaches.get(pathName).get();
if (bitmap != null && !bitmap.isRecycled()) {
callback.onSuccess(bitmap, pathName);
return;
}
}
mExecutorService.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap;
if (!pathName.contains(Constants.CACHE_PATH)) {
File cache = FileUtils.getCacheFile(pathName);
if (cache.exists()) {
bitmap = FileUtils.getSizedBitmap(cache.getAbsolutePath(), 200, 150);
} else {
bitmap = FileUtils.getSizedBitmap(pathName, 200, 150);
if (bitmap != null) {
FileUtils.saveBitmap(bitmap, Constants.CACHE_PATH, cache.getName());
}
}
} else {
bitmap = FileUtils.getSizedBitmap(pathName, 200, 150);
}
final Bitmap bitmap1 = bitmap;
mHandler.post(new Runnable() {
@Override
public void run() {
if (bitmap1 == null) {
callback.onError(pathName);
} else {
callback.onSuccess(bitmap1, pathName);
}
}
});
}
});
}
到了这里就进入真正的核心部分了,前面虽然说了这么多,但是更多的是原理和业务逻辑,真正需要的是如果将路径转换为位图,显示在组件中:
public static Bitmap getSizedBitmap(String pathName, int width, int height) {
width = (width < 0) ? 1280 : width;
height = (height < 0) ? 720 : height;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//设置为true时不返回bitmap,只返回尺寸
Bitmap bitmap = BitmapFactory.decodeFile(pathName, options);//将文件路径解码为位图
float imageWidth = options.outWidth;
float imageHeight = options.outHeight;
int scaleX = (int) Math.ceil(imageWidth / width);//返回大于参数的最小整数
int scaleY = (int) Math.ceil(imageHeight / height);
int scale = 1;
if (scaleX >= scaleY && scaleX >= 1) {
scale = scaleX;
} else if (scaleY >= scaleX && scaleY >= 1) {
scale = scaleY;
}
options.inJustDecodeBounds = false;
options.inSampleSize = scale;//根据比例缩小bitmap
if (bitmap != null)
bitmap.recycle();
return BitmapFactory.decodeFile(pathName, options);
}
关于BitmaoFactory:对象的引用类,这个类主要是用来解码创建位图。
拿到所有图片资源后,将这个图片集合传入前面定义的适配器中,图片就可以显示出来,大致的流程就是这个样子,具体的细节我会在后面将相关的代码上传,大家可以之后再看。到这里剩下的就是重命名和删除功能:
public static File renameFile(File file, String newName) {
String newPath = file.getParent();
String name = file.getName();
if (file.isFile()) {
String suffix = name.substring(name.lastIndexOf("."));
if (newName.endsWith(suffix)) {
newPath += "/" + newName;
} else {
newPath += "/" + newName + suffix;
}
} else {
newPath += "/" + newName;
}
File newFile = new File(newPath);
if (newFile.exists()) {
return null;
}
return file.renameTo(newFile) ? newFile : null;
}
if (file.exists()) {
file.delete();
}
这两个功能很简单,重命名的话更改之后需要生成一个新的文件传入适配器相应的位置来更新数据。同理,删除文件后也需要删除相应的item刷新适配器。另外因为我是封装的一个对话框,所有界面不好看,我实在懒得改啦,大家可以根据自己的需求再去改下。需要注意在退出activity时要回收释放资源,避免内存泄漏:
public void clearCache() {
Set<String> keys = mCaches.keySet();
for (String key : keys) {
Bitmap bitmap = mCaches.get(key).get();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
mCaches.clear();
if (!mExecutorService.isShutdown()) {
mExecutorService.shutdown();
}
}
另外需要注意一下的是,因为图片这里分为文件夹和文件,所有在退出的时候需要判断下当前的文件类型:
@Override
public void onBackPressed() {
File file = null;
try {
file = mFileList.get(0);
// 图片文件夹,结束
if (file.isDirectory()) {
super.onBackPressed();
}
// 图片文件,返回图片文件夹
else {
mFileList.clear();
mFileList.addAll(mImageDirList);
pictureAdapter.notifyDataSetChanged();
}
} catch (IndexOutOfBoundsException e) {
finish();
}
super.onBackPressed();
}
补充
发现有些设备有适配不良的问题,所以增加了屏幕适配
在图片退出到图片文件夹发现没有记录位置,需要重新滚动,所以修改了下这个BUG
recyclerView.scrollToPosition(lastPosition);
我只添加了这个属性,至于在当前屏幕的那个位置,就看心情了,要完美的控制显示位置,可以使用scrollToPosition + scrollBy .scrollBy这个属性是用来控制移动的距离,而scrollToPosition用来显示当前屏幕的某个位置,所以二者结合就可以完美的精确到详细的位置,感兴趣的可以尝试下。
OK,写完了,请大家多多指点!
项目代码