Android图片选择
大家都知道网上有很多第三方的图片选择器,但是到了自己真正的项目中,可能会有不同的需求,需要自己去修改。因此我自己根据鸿洋大神的慕课网视频写了一个图片选择器,又对代码进行了修改,方便大家进行使用。
本项目主要设计思路就是:一个图片加载类(单例)+利用ContentProvider扫描手机的图片+GridView显示图片 +RecyclerView在界面上显示图片。
本项目的主要步骤有以下几步:
1.图片加载类
2.扫描手机中的图片
3.选择图片展示在recyclerview中
当然最重要的是:
1.尽可能的去避免内存溢出
2.用户操作UI控件必须充分的流畅
3.用户预期显示的图片尽可能的快(图片的加载策略的选择)LIFO
本项目的主要功能:
1.扫描手机中的图片,默认显示图片最多的文件夹,在底部显示文件夹的名字以及图片的数量。
2.点击底部,弹出popupWindow,此popupWindow显示所有包含图片的文件夹以及文件夹的名字。
3.选择文件夹,进入图片选择界面,点击选择图片,再次点击取消。
4.点击右上方的“选择”按钮,将选择的图片呈现在recyclerView中。
展示一下效果:
代码展示及讲解
1.图片加载类 ImageLoader(核心类)
本类是图片选择器的核心类,该类为单例,可以设置图片加载打方式:
1.FIFO(先进先加载)
2.LILO(后进先加载)。
调用该方法:ImageLoader.getInstance(最大图片加载并发数,ImageLoader.Type.FIFO(图片加载的方式))
.LoadImage(图片的本地存放路径(绝对路径), 要显示图片的ImageView布局);
public class ImageLoader {
private static ImageLoader mInStance;
//图片缓存的核心对象
private LruCache<String,Bitmap> mLruCache;
//线程池
private ExecutorService mThreadPool;
private static final int DEAFULT_THREAD_COUNT=1;
//队列的调度方式
private Type mType=Type.LIFO;
//任务队列
private LinkedList<Runnable> mTaskQueue;
//后台轮询线程
private Thread mPoolThread;
private Handler mPoolThreadHandler;
//UI线程中的Handler
private Handler mUIHandler;
private Semaphore mSemaphorePoolThreadHandler=new Semaphore(0);
private Semaphore mSemaphoreThreadPool;
public enum Type{
FIFO,LIFO;
}
public ImageLoader(int threadCount,Type type) {
init(threadCount,type);
}
/**
* 初始化
* @param threadCount
* @param type
*/
private void init(int threadCount, Type type) {
//后台轮询线程
mPoolThread=new Thread(){
@Override
public void run() {
Looper.prepare();
mPoolThreadHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
//线程池取出一个任务进行执行
mThreadPool.execute(getTask());
try {
mSemaphoreThreadPool.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//释放一个信号量
mSemaphorePoolThreadHandler.release();
Looper.loop();
}
};
mPoolThread.start();
//获取我们应用的最大可用内存
int maxMemory= (int) Runtime.getRuntime().maxMemory();
int cacheMemory = maxMemory / 8;
mLruCache=new LruCache<String,Bitmap>(cacheMemory){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight();
}
};
mThreadPool= Executors.newFixedThreadPool(threadCount);
mTaskQueue=new LinkedList<>();
mType=type==null?Type.LIFO:type;
mSemaphoreThreadPool=new Semaphore(threadCount);
}
/**
* 从任务队列取出一个方法
* @return
*/
private Runnable getTask() {
if (mType==Type.FIFO){
return mTaskQueue.removeFirst();
}else if (mType==Type.LIFO){
return mTaskQueue.removeLast();
}
return null;
}
public static ImageLoader getInStance(){
if (mInStance==null){
synchronized (ImageLoader.class){
if (mInStance==null){
mInStance=new ImageLoader(DEAFULT_THREAD_COUNT,Type.LIFO);
}
}
}
return mInStance;
}
public static ImageLoader getInStance(int threadCount,Type type){
if (mInStance==null){
synchronized (ImageLoader.class){
if (mInStance==null){
mInStance=new ImageLoader(threadCount,type);
}
}
}
return mInStance;
}
/**
* 根据path为imageview设置图片
* @param path
* @param imageView
*/
public void loadImage(final String path, final ImageView imageView){
imageView.setTag(path);
if (mUIHandler==null){
mUIHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
//获取得到的图片,为imageview回调设置图片
ImgBeanHolder holder= (ImgBeanHolder) msg.obj;
Bitmap bitmap = holder.bitmap;
ImageView imageview= holder.imageView;
String path = holder.path;
if (imageview.getTag().toString().equals( path)){
imageview.setImageBitmap(bitmap);
}
}
};
}
//根据path在缓存中获取bitmap
Bitmap bm=getBitmapFromLruCache(path);
if (bm!=null){
refreshBitmap(bm, path, imageView);
}else{
addTask(new Runnable(){
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void run() {
//加载图片
//图片的压缩
//1.获得图片需要显示的大小
ImageSize imageSize= getImageViewSize(imageView);
//压缩图片
Bitmap bm=decodeSampledBitmapFromPath(imageSize.width,imageSize.height,path);
//把图片加入到缓存
addBitmapToLruCache(path,bm);
//
refreshBitmap(bm, path, imageView);
mSemaphoreThreadPool.release();
}
});
}
}
private void refreshBitmap(Bitmap bm, String path, ImageView imageView) {
Message message = Message.obtain();
ImgBeanHolder holder=new ImgBeanHolder();
holder.bitmap=bm;
holder.path=path;
holder.imageView=imageView;
message.obj=holder;
mUIHandler.sendMessage(message);
}
/**
* 将图片加入到LruCache
* @param path
* @param bm
*/
private void addBitmapToLruCache(String path, Bitmap bm) {
if (getBitmapFromLruCache(path)==null){
if (bm!=null){
mLruCache.put(path,bm);
}
}
}
/**
* 根据图片需要显示的宽和高进行压缩
* @param width
* @param height
* @param path
* @return
*/
private Bitmap decodeSampledBitmapFromPath(int width, int height, String path) {
//获得图片的宽和高,并不把图片加载到内存中
BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFile(path,options);
options.inSampleSize=caculateInSampleSize(options,width,height);
//使用获得到的InSampleSize再次解析图片
options.inJustDecodeBounds=false;
Bitmap bitmap=BitmapFactory.decodeFile(path,options);
return bitmap;
}
/**
* 根据需求的宽和高以及图片实际的宽和高计算SampleSize
* @param options
* @param width
* @param height
* @return
*/
private int caculateInSampleSize(BitmapFactory.Options options, int width, int height) {
int outWidth = options.outWidth;
int outHeight = options.outHeight;
int inSampleSize=1;
if (outWidth>width||outHeight>height){
int widthRadio=Math.round(outWidth*1.0f/width);
int heightRadio=Math.round(outHeight*1.0f/height);
inSampleSize=Math.max(widthRadio,heightRadio);
}
return inSampleSize;
}
/**
*根据ImageView获得适当的压缩的宽和高
* @param imageView
*/
private ImageSize getImageViewSize(ImageView imageView) {
ImageSize imageSize=new ImageSize();
DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
// int width=(lp.width== ViewGroup.LayoutParams.WRAP_CONTENT?0:imageView.getWidth());
int width = imageView.getWidth();//获取imageview的实际宽度
if (width <=0){
width=lp.width;//获取imageview再layout声明的宽度
}
if (width<=0){
width= getImageViewFieldValue(imageView,"mMaxWidth");//检查最大值
}
if (width<=0){
width=displayMetrics.widthPixels;
}
//int height = lp.height == ViewGroup.LayoutParams.WRAP_CONTENT ? 0 : imageView.getHeight();
int height = imageView.getHeight();//获取imageview的实际高度
if (height <=0){
height=lp.height;//获取imageview再layout声明的高度
}
if (height<=0){
height=getImageViewFieldValue(imageView,"mMaxHeight");;//检查最大值
}
if (height<=0){
height=displayMetrics.heightPixels;
}
imageSize.width=width;
imageSize.height=height;
return imageSize;
}
/**
* 通过反射获得imageView的某个属性值
* @return
*/
private static int getImageViewFieldValue(Object object,String fieldName){
int value=0;
try {
Field field=ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true);
int fieldValue = field.getInt(object);
if (fieldValue>0&&fieldValue<Integer.MAX_VALUE){
value=fieldValue;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return value;
}
private synchronized void addTask(Runnable runnable) {
mTaskQueue.add(runnable);
try {
if (mPoolThreadHandler==null){
mSemaphorePoolThreadHandler.acquire();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
mPoolThreadHandler.sendEmptyMessage(0x110);
}
/**
* 根据path为imageview设置图片
* @param key
* @return
*/
private Bitmap getBitmapFromLruCache(String key) {
return mLruCache.get(key);
}
private class ImageSize{
int width;
int height;
}
private class ImgBeanHolder{
Bitmap bitmap;
ImageView imageView;
String path;
}
}
2.图片的列表
首先扫描手机上的所有图片信息,拿到图片数量最多的文件夹,直接显示在GridView上;并且扫描结束,得到一个所有包含图片的文件夹信息的List;
对于文件夹信息,我们单独创建了一个Bean:
public class FolderBean {
private String dir;//当前文件夹路径
private String firstImamgPath;//第一张图片的路径
private String name;//文件夹的名字
private int count; //图片数量
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
int indexOf = this.dir.lastIndexOf("/")+1;
this.name=this.dir.substring(indexOf);
}
public String getFirstImamgPath() {
return firstImamgPath;
}
public void setFirstImamgPath(String firstImamgPath) {
this.firstImamgPath = firstImamgPath;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
其次是扫描手机中的图片:
注意:在6.0系统以上,要手动开启读取内存卡的权限,否则程序是运行不起来的。
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
if (ContextCompat.checkSelfPermission(SelectPhotoActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(SelectPhotoActivity.this, new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}else{
aboutScanPhoto();//未开启权限,先开启权限。以开启权限后,直接扫描图片
}
}else{
aboutScanPhoto();//扫描图片的方法
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
aboutScanPhoto();
}else {
Toast.makeText(this, "请打开权限!", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
扫描图片的方法
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
Toast.makeText(this,"当前存储卡不可用!",Toast.LENGTH_LONG);
return;
}
mProgressDialog= ProgressDialog.show(this,null,"正在加载。。。");
new Thread(){
@Override
public void run() {
Uri mImgUri= MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver cr=SelectPhotoActivity.this.getContentResolver();
Cursor mCursor = cr.query(mImgUri, null,
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=?",
new String[] { "image/jpeg", "image/png" },
MediaStore.Images.Media.DATE_MODIFIED);
Set<String> mDirPaths=new HashSet<String>();
while (mCursor.moveToNext()){
String path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA));
File parentFile=new File(path).getParentFile();
if (parentFile==null) {
continue;
}
String dirPath = parentFile.getAbsolutePath();
FolderBean folderBean=null;
if (mDirPaths.contains(dirPath)){
continue;
}else{
mDirPaths.add(dirPath);
folderBean=new FolderBean();
folderBean.setDir(dirPath);
folderBean.setFirstImamgPath(path);
}
if (parentFile.list()==null){
continue;
}
int picSize=parentFile.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".jpg")||name.endsWith("jpeg")||name.endsWith("png")){
return true;
}
return false;
}
}).length;
folderBean.setCount(picSize);
mFolderBeans.add(folderBean);
if (picSize>mMaxCount){
mMaxCount=picSize;
mCurrentDir=parentFile;
}
}
mCursor.close();
handler.sendEmptyMessage(0x110);
}
}.start();
然后我们通过handler发送消息,在handleMessage里面:
1、创建GridView的适配器,为我们的GridView设置适配器,显示图片;
2、创建我们的popupWindow了
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
if (msg.what==0x110){
mProgressDialog.dismiss();
dataToView();
initPopupWindow();
}
}
};
dataToView()就是我们为view设置数据
private void dataToView() {
if (mCurrentDir==null){
Toast.makeText(this,"未扫描到任何图片",Toast.LENGTH_LONG).show();
return;
}
mImgs= Arrays.asList(mCurrentDir.list());
adapter = new ImageAdapter(this,mImgs,mCurrentDir.getAbsolutePath());
mGridView.setAdapter(adapter);
mTvDirName.setText(mCurrentDir.getName());
mTvDirCount.setText(mMaxCount+"");
}
我们还用到了一个GridView的adapter
public class ImageAdapter extends BaseAdapter {
private String mDirPath;
private List<String> mImgPaths;
private LayoutInflater mInflater;
private static List<String> mSelectImg=new LinkedList<>();
public ImageAdapter(Context context, List<String> mDatas, String dirPath) {
this.mDirPath=dirPath;
this.mImgPaths=mDatas;
mInflater=LayoutInflater.from(context);
}
@Override
public int getCount() {
return mImgPaths.size();
}
@Override
public Object getItem(int position) {
return mImgPaths.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder vh=null;
if (convertView==null){
convertView= mInflater.inflate(R.layout.item,parent,false);
vh=new ViewHolder();
vh.mImg=convertView.findViewById(R.id.iv_item);
vh.mSelect=convertView.findViewById(R.id.ib_select);
convertView.setTag(vh);
}else {
vh = (ViewHolder) convertView.getTag();
}
vh.mImg.setImageResource(R.mipmap.default_error);
vh.mSelect.setImageResource(R.mipmap.btn_unselected);
vh.mImg.setColorFilter(null);
final String filePath=mDirPath+"/"+mImgPaths.get(position);
// new ImageLoader(3, ImageLoader.Type.LIFO).loadImage(mDirPath + "/" + mImgPaths.get(position),vh.mImg);
ImageLoader.getInStance(3, ImageLoader.Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position),vh.mImg);
final ViewHolder finalVh = vh;
vh.mImg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//已经被选择
if (mSelectImg.contains(filePath)){
mSelectImg.remove(filePath);
finalVh.mImg.setColorFilter(null);
finalVh.mSelect.setImageResource(R.mipmap.btn_unselected);
}else{
//未被选中
mSelectImg.add(filePath);
finalVh.mImg.setColorFilter(Color.parseColor("#77000000"));
finalVh.mSelect.setImageResource(R.mipmap.btn_selected);
}
}
});
if (mSelectImg.contains(filePath)){
vh.mImg.setColorFilter(Color.parseColor("#77000000"));
vh.mSelect.setImageResource(R.mipmap.btn_selected);
}
return convertView;
}
public List<String> selectPhoto(){
if (!mSelectImg.isEmpty()){
return mSelectImg;
}
return null;
}
private class ViewHolder{
ImageView mImg;
ImageButton mSelect;
}
}
3.到这一步图片就已经显示在GridView中,下一步就是我们的popupWindow
要实现的效果是:点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;
首先我们创建了一个popupWindow使用的类,和Activity类似:
public class ListImageDirPopupWindow extends PopupWindow {
private int mWidth;
private int mHeight;
private View mConvertView;
private List<FolderBean> mDatas;
private ListView mListView;
public interface OnDirSelectedListener{
void onSelected(FolderBean folderBean);
}
public OnDirSelectedListener mListener;
public void setOnDirSelectedListener(OnDirSelectedListener mListener) {
this.mListener = mListener;
}
public ListImageDirPopupWindow(Context context, List<FolderBean> datas) {
calWidthAndHeight(context);
mConvertView= LayoutInflater.from(context).inflate(R.layout.popup_main,null);
mDatas=datas;
setContentView(mConvertView);
setWidth(mWidth);
setHeight(mHeight);
setFocusable(true);
setTouchable(true);
setOutsideTouchable(true);
setBackgroundDrawable(new BitmapDrawable());
setTouchInterceptor(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction()==MotionEvent.ACTION_OUTSIDE){
dismiss();
return true;
}
return false;
}
});
initViews(context);
initEvent();
}
private void initViews(Context context) {
mListView= mConvertView.findViewById(R.id.lv_dir);
mListView.setAdapter(new ListDirAdapter(context,mDatas));
}
private void initEvent() {
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mListener!=null){
mListener.onSelected(mDatas.get(position));
}
}
});
}
/**
* 计算popupWindow的宽度和高度
* @param context
*/
private void calWidthAndHeight(Context context) {
WindowManager wm= (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics=new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mWidth= outMetrics.widthPixels;
mHeight= (int) (outMetrics.heightPixels*0.7);
}
private class ListDirAdapter extends ArrayAdapter<FolderBean>{
private LayoutInflater mInflater;
private List<FolderBean> mDatas;
public ListDirAdapter(@NonNull Context context, List<FolderBean> datas) {
super(context, 0, datas);
mInflater= LayoutInflater.from(context);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewHolder vh=null;
if (convertView==null){
vh=new ViewHolder();
convertView= mInflater.inflate(R.layout.popup_item,parent,false);
vh.mDirName=(TextView) convertView.findViewById(R.id.tv_dir_item_name);
vh.mDirCount=(TextView) convertView.findViewById(R.id.tv_dir_item_count);
vh.mImg= (ImageView) convertView.findViewById(R.id.iv_dir_image);
convertView.setTag(vh);
}else{
vh= (ViewHolder) convertView.getTag();
}
FolderBean bean = getItem(position);
//重置
vh.mImg.setImageResource(R.mipmap.default_error);
ImageLoader.getInStance(3, ImageLoader.Type.LIFO).loadImage(bean.getFirstImamgPath(),vh.mImg);
vh.mDirName.setText(bean.getName());
vh.mDirCount.setText(bean.getCount()+"");
return convertView;
}
private class ViewHolder{
ImageView mImg;
TextView mDirName;
TextView mDirCount;
}
}
}
然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片。在这里我们创建一个接口OnDirSelectedListener
,对Activity设置回调;
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mListener!=null){
mListener.onSelected(mDatas.get(position));
}
}
});
4.选择不同的文件夹
前面我们handleMessage中初始化调用popupWindow,通过activity的回调,实现选择文件夹显示图片
mImageDirPopupWindow=new ListImageDirPopupWindow(this,mFolderBeans);
mImageDirPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
lightOn();
}
});
mImageDirPopupWindow.setOnDirSelectedListener(new ListImageDirPopupWindow.OnDirSelectedListener() {
@Override
public void onSelected(FolderBean folderBean) {
mCurrentDir=new File(folderBean.getDir());
mImgs= Arrays.asList(mCurrentDir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".jpg")||name.endsWith("jpeg")||name.endsWith("png")){
return true;
}
return false;
}
}));
adapter=new ImageAdapter(SelectPhotoActivity.this,mImgs,mCurrentDir.getAbsolutePath());
mGridView.setAdapter(adapter);
mTvDirCount.setText(mImgs.size()+"");
mTvDirName.setText(folderBean.getName());
mImageDirPopupWindow.dismiss();
}
});
5.图片展示在RecyclerView中
在ImageAdapter中定义了一个方法,获得选择图片的路径,点击“选择”按钮的时候,把这个路径的集合传过去
public List<String> selectPhoto(){
if (!mSelectImg.isEmpty()){
return mSelectImg;
}
return null;
}
然后通过ImageLoader这个类将图片展示出来,我们在recyclerview设置图片展示方式为网格方式。
ImageLoader.getInStance(3, ImageLoader.Type.LIFO).loadImage(mDatas.get(position),holder.iv);
if (photoSelect!=null) {
final SimpleAdapter mAdapter = new SimpleAdapter(this, photoSelect);
mListView.setAdapter(mAdapter);
mListView.setLayoutManager(new GridLayoutManager(this,3));
}
其中涉及到了一个adapter,是recyclerview所需要的
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.MyViewHolder> {
private LayoutInflater mInflater;
private Context mContext;
protected List<String> mDatas;
public SimpleAdapter(Context mContext, List<String> mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
mInflater=LayoutInflater.from(mContext);
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.list_item, parent, false);
MyViewHolder viewHolder=new MyViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
ImageLoader.getInStance(3, ImageLoader.Type.LIFO).loadImage(mDatas.get(position),holder.iv);
}
@Override
public int getItemCount() {
return mDatas.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
ImageView iv;
public MyViewHolder(View itemView) {
super(itemView);
iv= itemView.findViewById(R.id.iv_photo);
}
}
}