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,Bitmap> mLruCache; //线程池 private ExecutorService mThreadPool; private static final int DEAFULT_THREAD_COUNT=1; //队列的调度方式 private Type mType=Type.LIFO; //任务队列 private LinkedList 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 ,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 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); SetmDirPaths=new HashSet (); 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 List3.到这一步图片就已经显示在GridView中,下一步就是我们的popupWindowmImgPaths; private LayoutInflater mInflater; private static List mSelectImg=new LinkedList<>(); public ImageAdapter(Context context, List 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 selectPhoto(){ if (!mSelectImg.isEmpty()){ return mSelectImg; } return null; } private class ViewHolder{ ImageView mImg; ImageButton mSelect; } }
要实现的效果是:点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;
首先我们创建了一个popupWindow使用的类,和Activity类似:
public class ListImageDirPopupWindow extends PopupWindow { private int mWidth; private int mHeight; private View mConvertView; private List然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片。在这里我们创建一个接口OnDirSelectedListener ,对Activity设置回调;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 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 { private LayoutInflater mInflater; private List mDatas; public ListDirAdapter(@NonNull Context context, List 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; } } }
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 ListselectPhoto(){ 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{ private LayoutInflater mInflater; private Context mContext; protected List mDatas; public SimpleAdapter(Context mContext, List 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); } } }
如果大家想学习RecyclerView的使用,可以看一下我的博客
https://blog.csdn.net/wen_haha/article/details/80775056
Demo
CSDN地址:
https://download.csdn.net/download/wen_haha/10499827
Github地址:
https://github.com/kongkongdaren/SelectPhotoDemo