视频教程原址:http://www.imooc.com/learn/489

这次,小编我学习了一个仿照微信的图片选择器的实例,接下来,就将贴上我的code,当然,里面的注解有的是根据老师的原话总结的,不过更多的,是根据我自己的理解


首先,先将这次实例的关键类先贴出来:ImageLoader
关于这个类的作用呢,是用来加载图片的,下面是该类要实现的目标

1、尽可能的去避免内存溢出a、根据图片的显示大小去压缩图片b、使用缓存对我们的图片进行管理(LruCache)2、用户操作UI控件必须充分的流畅
适配器的getView方法里面尽可能不去做耗时的操作(异步加载+回调显示)3、用户预期显示的图片尽可能的快(图片的加载策略选择 LIFO后进先出)

接着是关于该类的实现流程(因为该类是在适配器中的getView方法中调用的):

getView() {url -> Bitmap(在getView要实现的是根据url得到bitmap然后设置给GridView中的item)(下面的是在ImageLoader大致实现:url -> LruCache查找(LruCache用来缓存图片)-> 找到返回,利用一个Handler设置回调为ImageView设置图片-> 找不到 url -> 创建一个Task -> 将Task放入TaskQueue且发送一个通知去提醒后台轮询线程Task -> run() {根据url加载图片1、获得图片显示的大小2、使用Options对图片进行压缩3、加载图片且放入LruCache})
}

关于后台轮询线程:
从TaskQueue -> 取出Task -> 交给线程池去执行
采用Handler+Looper+Message(Android异步执行处理框架)去实现
(Looper主要作用:
1、与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
2、loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。
好了,我们的异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,现在缺的就是发送消息的对象了,于是乎:Handler登场了。)

启发:安卓中Handler如果在某个线程中初始化,则属于该线程(切记需要在初始化前需要调用Looper.prepare(),初始化后需要调用Looper.loop()启动消息队列)。

import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;/*** 图片加载类 (采用单例模式)* * @author Just* */
public class ImageLoader {private static ImageLoader mInstance;// 成员变量/*** 图片缓存的核心对象*/private LruCache<String, Bitmap> mLruCache;// String表示图片的路径/*** 线程池,用于统一处理Task (ImageLoader中的存在一个后台线程来取任务然后加入到线程池中)*/private ExecutorService mThreadPool;private static final int DEAFULT_THREAD_COUNT = 1;// 线程池的默认线程数/*** 队列的调度方式 (即用来记录图片的加载策略,默认为LIFO)*/private Type mType = Type.LIFO;/*** 任务队列,供线程池取任务*/private LinkedList<Runnable> mTaskQueue;/*** 后台轮询线程*/private Thread mPoolThread;private Handler mPoolThreadHandler;// 是与上面定义的线程绑定在一起的,用于给线程发送消息/*** UI线程中的Handler 用于传入一个Task以后,当图片获取成功以后,* 用mUIHandler发送消息,为图片设置回调(回调显示图片的Bitmap)*/private Handler mUIHandler;/*** Semaphore(信号量)通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。* 通过信号量控制mPoolThread(后台轮询线程)中初始化mPoolThreadHandler与* 使用的loadImage(loadImage中的调用了addTask(addTask中使用了mPoolThraedhandler))的线程的顺序*/private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);// 初始化时设置可用的许可证数量为0,使得当addTask在mPoolThreadHandler初始化之前执行时处于等待状态private Semaphore mSemaphoreThreadPool;// 再init()中根据线程数量指定信号量并初始化public enum Type {FIFO, LIFO;}private ImageLoader(int mThreadCount, Type type) {// 可以由用户决定线程池的线程数以及队列的调度方式init(mThreadCount, type);}/*** 初始化* @param mThreadCount* @param type*/private void init(int mThreadCount, Type type) {// 后台轮询线程,这里涉及的Looper可以阅读这篇博客 [http://blog.csdn.net/lmj623565791/article/details/38377229]mPoolThread = new Thread() {@Overridepublic void run() {Looper.prepare();//Looper.prepare()是在后台轮询线程中调用的// Looper用于封装了android线程中的消息循环,默认情况下一个线程是不存在消息循环(message loop)的,// 需要调用Looper.prepare()来给线程创建一个消息循环,调用Looper.loop()来使消息循环起作用,// 从消息队列里取消息,处理消息。// "找不到 url -> 创建一个Task -> 将Task放入TaskQueue且发送一个通知去提醒后台轮询线程"// 如果来了一个任务,mPoolThreadHandler会发送一个Message到Looper中,最终会调用handleMessagemPoolThreadHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 线程池取出任务进行执行mThreadPool.execute(getTask());try {mSemaphoreThreadPool.acquire();} catch (InterruptedException e) {e.printStackTrace();}}};// 发布一个许可证,从而可以唤醒addTask的等待mSemaphorePoolThreadHandler.release();Looper.loop();}};mPoolThread.start();int maxMemory = (int) Runtime.getRuntime().maxMemory();// 获取应用最大可用内存int cacheMemory = maxMemory / 8;// 用于初始化mLruCachemLruCache = new LruCache<String, Bitmap>(cacheMemory) {@Overrideprotected int sizeOf(String key, Bitmap value) {// 测量每个Bitmap所占据的内存并返回return value.getRowBytes() * value.getHeight();// 每行占据的字节数*高度}};// 创建线程池mThreadPool = Executors.newFixedThreadPool(mThreadCount);mTaskQueue = new LinkedList<Runnable>();mType = type;mSemaphoreThreadPool = new Semaphore(mThreadCount);}/*** 从任务队列取出一个Runnable* * @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初始化后基本上if体里面的代码就不要执行了// 但是在刚开始mInstance未初始化的时候,因未作同步的处理,可能会有一两个线程同时到达if体里面// 等到达里面后再做synchronized处理,这样会使得需要做同步的线程减少,只有一开始的几个if (mInstance == null) {// 就是这个点可能会有几个线程同时到达synchronized (ImageLoader.class) {// 里层的if判断是必要的,因为在外层if判断之后的那个点可能会有几个线程同时到达,// 接着在进行synchronized,如果不加判断可能会new出几个实例if (mInstance == null)mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO);}}// 上面的这种处理相比于public synchronized static ImageLoader getInstance()提高了效率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);// 防止item复用的时候,imageView复用造成图片错乱,imageView设置图片时会根据Tag对比path//这里可以参考一下[http://blog.csdn.net/lmj623565791/article/details/24333277]这篇博客if (mUIHandler == null) {mUIHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 获取得到的图片,为imageView回调设置图片ImgBeanHolder holder = (ImgBeanHolder) msg.obj;Bitmap b = holder.bitmap;ImageView iv = holder.imageView;String p = holder.path;// 将path与getTag存储路径进行比较if (iv.getTag().toString().equals(p)) {iv.setImageBitmap(b);}}};}// 根据path在缓存中获取bitmapBitmap bm = getBitmapFromLruCache(path);if (bm != null) {refreashBitmap(path, imageView, bm);} else {// Task的任务就是压缩图片,然后将图片加入到缓存之中,然后回调图片(即将图片载入到指定的imageView中)addTask(new Runnable() {@Overridepublic void run() {// 加载图片,涉及图片的压缩// 1、获得图片需要显示的大小(即刚好为imageView的大小)ImageSize imageSize = getImageViewSize(imageView);// 2、压缩图片Bitmap b = decodeSampledBitmapFromPath(path,imageSize.width, imageSize.height);// 将图片加入到缓存中addBitmapToLruCache(path, b);refreashBitmap(path, imageView, b);mSemaphoreThreadPool.release();// 当任务一旦执行完,就释放一个许可证}});}}private void refreashBitmap(final String path, final ImageView imageView,Bitmap b) {Message message = Message.obtain();// 从整个Messge池中返回一个新的Message实例,避免分配新的对象,减少内存开销ImgBeanHolder holder = new ImgBeanHolder();holder.bitmap = b;holder.path = path;// loadImage的形参holder.imageView = imageView;// loadImage的形参message.obj = holder;mUIHandler.sendMessage(message);}/*** 将图片加入到LruCache* * @param path* @param b*/private void addBitmapToLruCache(String path, Bitmap b) {if (getBitmapFromLruCache(path) == null) {// 需要判断缓存中是否已经存在if (b != null) {mLruCache.put(path, b);}}}/*** 根据图片需要的显示的宽和高进行压缩* * @param path* @param width* @param height* @return*/private Bitmap decodeSampledBitmapFromPath(String path, int width,int height) {// 获取图片实际的宽和高,并不把图片加载到内存中BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;// 使得图片不加载到内存中,但是options.outWidth和options.outHeight会存在值BitmapFactory.decodeFile(path, options);// options中会包含图片实际的宽和高(即options.outWidth和options.outHeight)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(Options options, int reqWidth,int reqHeight) {int width = options.outWidth;int height = options.outHeight;int inSampleSize = 1;// 这儿只是一个普通的压缩策略,当然也可以自己定制特别的压缩策略if (width > reqWidth || height > reqHeight) {int widthRadio = Math.round(width * 1.0f / reqWidth);// 将比例四舍五入int heightRadio = Math.round(height * 1.0f / reqHeight);inSampleSize = Math.max(widthRadio, heightRadio);// 为了图片不失帧,保持原比例一般取小值,但是得到的图片会始终比显示区域大一些,这里为了节省内存,所以尽量的压缩}return inSampleSize;}/*** 根据ImageVIew获取适当的压缩的宽和高* * @param imageView* @return*/// @SuppressLint("NewApi")// 在Android代码中,我们有时会使用比我们在AndroidManifest中设置的android:minSdkVersion版本更高的方法,// 此时编译器会提示警告,解决方法是在方法上加上@SuppressLint("NewApi")或者@TargetApi()// SuppressLint("NewApi")作用仅仅是屏蔽android lint错误,所以在方法中还要判断版本做不同的操作private ImageSize getImageViewSize(ImageView imageView) {ImageSize imageSize = new ImageSize();DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();// imageView在布局中的大小可能是固定大小的,也有可能是相对的LayoutParams lp = imageView.getLayoutParams();int width = imageView.getWidth();// 获取imageView的实际宽度// 有可能ImageView刚被new出来而没有添加到容器中,或者其他原因,导致无法获取真正的width,因此需要判断一下if (width <= 0) {width = lp.width;// 设置为在布局中声明的宽度,但是有可能在布局中是wrap_content(-1),match_parent(-2),所以需要下一步的判断}if (width <= 0) {// 赋值为最大值,但是如果没有设置的话,width依然是获取不到的理想的值的,因此还需要下一步判断// getMaxWidth方法是在API16中才有,还需要处理一下,利用反射获取,以便兼容到16以下的版本// width=imageView.getMaxWidth();width = getImageViewFieldValue(imageView, "mMaxWidth");// "mMaxWidth"可以去ImageView的源码中查看}if (width <= 0) {width = displayMetrics.widthPixels;// 最后没办法,只能等于屏幕的宽度}int height = imageView.getHeight();if (height <= 0) {height = lp.height;}if (height <= 0) {// height=imageView.getMaxHeight();height = getImageViewFieldValue(imageView, "mMaxHeight");}if (height <= 0) {height = displayMetrics.heightPixels;}imageSize.width = width;imageSize.height = height;return imageSize;}/*** 通过反射获取ImageView的某个属性值* * @param object* @param fieldName* @return*/public static int getImageViewFieldValue(ImageView object, String fieldName) {int value = 0;try {Field field = ImageView.class.getDeclaredField(fieldName);// Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。// getDeclaredField返回指定字段的字段对象field.setAccessible(true);// 在反射使用中,如果字段是私有的,那么必须要对这个字段设置int fieldValue = field.getInt(object);// 因为包括指定类的所有实例的指定字段(除静态字段),所以在这里需要指定实例对象if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {value = fieldValue;}} catch (Exception e) {}return value;}/*** addTask需要同步(synchronized),避免多个线程造成mSemaphorePoolThreadHandler.acquire()* 从而导致死锁的状态 且mTaskQueue.add(runnable)本身也需要同步* * @param runnable*/private synchronized void addTask(Runnable runnable) {mTaskQueue.add(runnable);try {if (mPoolThreadHandler == null)mSemaphorePoolThreadHandler.acquire();} catch (InterruptedException e) {e.printStackTrace();}mPoolThreadHandler.sendEmptyMessage(0x110);}/*** 根据path在缓存中获取bitmap* * @param key* @return*/private Bitmap getBitmapFromLruCache(String key) {return mLruCache.get(key);}private class ImageSize {public int width;public int height;}private class ImgBeanHolder {public Bitmap bitmap;public ImageView imageView;public String path;}
}

下面是关于这段代码需要注意的几点(也许我根据视频原话的理解有错误,欢迎指正):
视频3-1的4:40起
在mUIHandler中的handleMessage中不能直接用msg.obj来取bitmap(前提是在sendMessage的时候只在msg.obj中绑定bitmap)然后根据loadImage方法参数path和imageView来设置图片,因为这里有一个重要的前提,那就是ImageLoader采用的是单例模式,在整个程序中只有一个实例,所以会导致mUIHandler中的handleMessage在主线程中执行的时候所获得的bitmap不一定是当前的(与之对应的)path和imageView(也就是当前loadImage方法的参数path和imageView,因为mUIHandler是在loadImage中初始化的,所以mUIHandler的handleMessage可以直接调用loadImage方法的参数path和imageView),因为我们会根据path用getBitmapFromLruCache方法在缓存中获取图片,或者异步去加载(开启新的线程去加载),加载完成后会回调mUIHandler中的handleMessage。
在显示GridView的时候,适配器的getView方法会被多次调用,因此loadImage也会被多次调用,而每次调用loadImage都会通过refreashBitmap方法来mUIHandler.sendMessage,而Task中refreashBitmap会新的线程中去执行,因此也许会有多个线程并发执行,也许当执行某一个loadImage的时候,正好mUIHandler的handleMessage也刚好在处理一个非该loadImage而sendMessage的message(注意:mUIHandler的handleMessage是在主线程中),如果不用ImgBeanHolder,就可能导致用msg.obj来取bitmap且调用刚才说的loadImage的参数的path和imageView而发生错乱
解决方法:设置一个内部类ImgBeanHolder,将对应的imageView,bitmap和path绑定成ImgBeanHolder实例的成员

视频3-4的3:55起
后台轮询的线程的执行理论上和loadImage方法的执行理论上应该是并行的
但是实际中可能在使用mPoolThraedhandler的时候mPoolThraedhandler还没有初始化
当用户new一个ImageLoader的以后有可能会直接调用loadImage方法,而loadImage中的调用了addTask(addTask中使用了mPoolThraedhandler)
解决方法:利用信号量,mSemaphorePoolThreadHandler
总结:当一个类中使用了两个线程,且一个线程使用的另外一个线程的变量时,一定要保证该变量已经初始化了

视频3-5起
Task -> TaskQueue -> 通知后台线程池取线程 -> 线程池把Task取出放入到自己内部的任务队列中(这里只是放到自己的任务队列中,至于要多久才能完成任务就没有管了,即使没有执行完,如果收到了通知,还是会从TaskQueue取任务)
(这一系列到放入线程池的内部队列为止的执行都是瞬间的,从而导致mTaskQueue中始终不大于一个任务,使得FIFO或LIFO没有区别)
修改:使得只有当目前的Task执行完成后才会去取新的Task
解决方法:利用信号量,mSemaphoreThreadPool

视频3-6起
在getImageViewSize方法中用到了一个API 16的方法getMaxWidth以及getMaxHeight
解决方法:利用反射获取 自定义方法 public static int getImageViewFieldValue();

还有我这里将要导的包也贴出来,是希望大家注意别导错包了,不然有些实例的方法是实现不了的


然后是主布局中的GridView所要用到的适配器:ImageAdapter

public class ImageAdapter extends BaseAdapter {/*** 记录图片是否被选中* 当改变文件夹的时候,会重新new一个ImageAdapter* 所以在这里用static,在不同的对象间共享数据* 使得即使是改变了文件夹,当回到原来的文件夹时,已经被选中的图片不会被取消*/private static Set<String> mSelectedImg=new HashSet<String>();private String mDirPath;private List<String> mImagePaths;private LayoutInflater mInflater;//用于加载item的布局private int mScreenWidth;//屏幕的宽度/*** @param context* @param mDatas:传入的文件夹下所有图片的文件名* @param dirPath:图片所在的文件夹的路径* List存储的是图片的文件名而非图片的路径,如果图片比较多,存储路径的话浪费内存*/public ImageAdapter(Context context,List<String> mDatas,String dirPath) {       this.mDirPath=dirPath;mImagePaths=mDatas;mInflater=LayoutInflater.from(context);WindowManager wm=(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics=new DisplayMetrics();wm.getDefaultDisplay().getMetrics(outMetrics);mScreenWidth=outMetrics.widthPixels;}@Overridepublic int getCount() {return mImagePaths.size();}@Overridepublic Object getItem(int position) {return mImagePaths.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {final ViewHolder viewHolder;if(convertView==null) {convertView=mInflater.inflate(R.layout.item_griview, parent,false);viewHolder=new ViewHolder();viewHolder.mImg=(ImageView) convertView.findViewById(R.id.id_item_image);viewHolder.mSelect=(ImageButton) convertView.findViewById(R.id.id_item_select);convertView.setTag(viewHolder);//使得下次可以直接getTag;}else {viewHolder=(ViewHolder) convertView.getTag();}//重置状态,防止第一屏的图片以及表示已经选择状态图标影响第二屏图片的显示//因为第二屏的item有可能是第一屏item的复用,而复用的item中的imageView还设置的是第一屏显示的图案//这里与imageView去setTag还是有所区别的viewHolder.mImg.setImageResource(R.drawable.pictures_no);viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);viewHolder.mImg.setColorFilter(null);viewHolder.mImg.setMaxWidth(mScreenWidth/3);//因为已经在布局中设置一行显示三个图片,所以在这里可以设置mImg的MaxWidth(这行代码可选)//这样就可以优化ImageLoader中获取ImageView的宽度的那段代码(在某些情况下优化效果是比较明显的)ImageLoader.getInstance(3, Type.LIFO).loadImage(mDirPath + "/" + mImagePaths.get(position),viewHolder.mImg);final String filePath=mDirPath+"/"+mImagePaths.get(position);viewHolder.mImg.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {       //如果已经被选择if(mSelectedImg.contains(filePath)) {mSelectedImg.remove(filePath);viewHolder.mImg.setColorFilter(null);viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);}else {mSelectedImg.add(filePath);viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);}
//              notifyDataSetChanged();//如果调用该方法会使得在点击屏幕的时候会出现闪屏的情况,所以直接在onClick中用set方法设置比较好,就不要调用该方法了//notifyDataSetChanged方法通过一个外部的方法控制如果适配器的内容改变时需要强制调用getView来刷新每个Item的内容//可以实现动态的刷新列表的功能}});//每次GridView的状态改变时(如滑动GridView),或者选择另外一个文件夹,就需要调用getView来重绘视图//如果不加下面这段代码,GridView的状态改变时或者选择另外一个文件夹,原来已经被选择的图片就不会显示被选择的效果//但是实际上该图片已经被选择了if (mSelectedImg.contains(filePath)) {viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);}return convertView;//这里返回的就是GridView的item为item_griview.xml中布局的关键}private class ViewHolder {ImageView mImg;ImageButton mSelect;}
}

接着是选择不同文件夹要用到的弹窗(PopupWindow):自定义的ListImageDirPopupWindow

public class ListImageDirPopupWindow extends PopupWindow {private int mWidth;private int mHeight;private View mConVertView;private ListView mListView;private List<FolderBean> mDatas;public OnDirSelectedListener mListener;public void setOnDirSelectedListener(OnDirSelectedListener mListener) {this.mListener = mListener;}public interface OnDirSelectedListener {void onSeleted(FolderBean folderBean);}public ListImageDirPopupWindow(Context context, List<FolderBean> mDatas) {calWidthAndHeight(context);mConVertView = LayoutInflater.from(context).inflate(R.layout.popup_main, null);this.mDatas = mDatas;setContentView(mConVertView);setWidth(mWidth);setHeight(mHeight);setFocusable(true);// 使得窗口可以从当前焦点小部件中抓取焦点setTouchable(true);// 可以点击setOutsideTouchable(true);// 可以点击范围之外的地方setBackgroundDrawable(new BitmapDrawable());setTouchInterceptor(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {dismiss();return true;}return false;}});initViews(context);initEvent();}private void initEvent() {mListView.setOnItemClickListener(new OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view,int position, long id) {if(mListener!=null) {mListener.onSeleted(mDatas.get(position));}}});}private void initViews(Context context) {mListView = (ListView) mConVertView.findViewById(R.id.id_list_dir);mListView.setAdapter(new ListDirAdapter(context,mDatas));}/*** 计算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(Context context,List<FolderBean> objects) {super(context, 0, objects);mInflater = LayoutInflater.from(context);}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if (holder == null) {holder = new ViewHolder();convertView = mInflater.inflate(R.layout.item_popup_main,parent, false);holder.mImg = (ImageView) convertView.findViewById(R.id.id_id_dir_item_image);holder.mDirName = (TextView) convertView.findViewById(R.id.id_dir_item_name);holder.mDirCount = (TextView) convertView.findViewById(R.id.id_dir_item_count);convertView.setTag(holder);} elseholder = (ViewHolder) convertView.getTag();FolderBean bean=getItem(position);//重置holder.mImg.setImageResource(R.drawable.pictures_no);ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg);holder.mDirCount.setText(""+bean.getCount());holder.mDirName.setText(bean.getName());return convertView;}private class ViewHolder {ImageView mImg;TextView mDirName;TextView mDirCount;}}
}

为了解耦,在ListImageDirPopupWindow的initEvent()方法中设置LIstView的item的onClick事件,然后设置一个接口,用一个接口进行回调,在MainActivity中实现该接口,并且编写需要的逻辑代码

以及PopupWindow中涉及的FolderBean

public class FolderBean {/*** 当前文件夹的路径*/private String dir;/*** 当前文件夹下第一张图片的路径*/private String firstImgPath;/*** 当前文件夹的名称*/private String name;/*** 当前文件夹中图片的数量*/private int count;public FolderBean() {}public String getDir() {return dir;}public void setDir(String dir) {this.dir = dir;int lastIndexOf=this.dir.lastIndexOf("/");this.name=this.dir.substring(lastIndexOf+1);}public String getFirstImgPath() {return firstImgPath;}public void setFirstImgPath(String firstImgPath) {this.firstImgPath = firstImgPath;}public int getCount() {return count;}public void setCount(int count) {this.count = count;}public String getName() {return name;}
}

最后就是 MainActivity

public class MainActivity extends Activity {    private static final int DATA_LOADED=0x110;private GridView mGridView;private List<String> mImgs;private ImageAdapter mImgAdapter;private RelativeLayout mBottonLy;private TextView mDirName;private TextView mDirCount;private File mCurrentDir;private int mMaxCount;private List<FolderBean> mFolderBeans = new ArrayList<FolderBean>();private ProgressDialog mProgressDialog;//实现LIstView的item的onClick事件相关的接口private ListImageDirPopupWindow mPopupWindow;private Handler mHandler=new Handler() {public void handleMessage(android.os.Message msg) {if(msg.what==DATA_LOADED) {     //绑定数据到View中data2View();initPopuWindow();}}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();initDatas();initEvent();}protected void data2View() {if(mCurrentDir==null) {Toast.makeText(this, "未扫描到任何图片", Toast.LENGTH_SHORT).show();return;}mImgs=Arrays.asList(mCurrentDir.list(new FilenameFilter() {@Overridepublic boolean accept(File dir, String filename) {if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))return true;return false;}}));mImgAdapter=new ImageAdapter(this, mImgs, mCurrentDir.getAbsolutePath());mGridView.setAdapter(mImgAdapter);mDirCount.setText(""+mMaxCount);mDirName.setText(mCurrentDir.getName());}private void initEvent() {mBottonLy.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {mPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim);//设置popupWindow显示与收起来的动画(如果不想实现动画可以注释掉,无任何影响,但是要实现动画一定要加上文章末尾的几步)mPopupWindow.showAsDropDown(mBottonLy, 0, 0);//设置显示PopupWindow的位置位于指定View的左下方,x,y表示坐标偏移量lightOff();}});}private void initPopuWindow() {mPopupWindow=new ListImageDirPopupWindow(this, mFolderBeans);//在popupWindow出现 时候,会有屏幕变暗的特效,所以要在这里设置popupWindow消失的时候的事件,使屏幕变回原来的亮度mPopupWindow.setOnDismissListener(new OnDismissListener() {@Overridepublic void onDismiss() {lightOn();}});mPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() {@Overridepublic void onSeleted(FolderBean folderBean) {mCurrentDir=new File(folderBean.getDir());//由于mImgs每次都是根据已有文件夹来搜索图片然后传入GridView的适配器中,//所以在已有文件夹下增删图片之后重新在popupWindow中打开该文件夹是能够刷新数据的(也就是文件夹下的所有图片的新的总数)//而由于所有有图片的文件夹是在程序一开启的时候扫描完成的,//所以之后如果不重启程序而新增文件夹且在文件夹下增加图片是不会被扫描的,除非重启程序//或者在未关闭程序的时候删除已经扫描的到的文件夹,然后再程序中从PopupWindow中选择该文件夹(因为此时ListView的数据还没刷新所以还暂时存在于PopupWindow中)会因异常退出mImgs=Arrays.asList(mCurrentDir.list(new FilenameFilter() {@Overridepublic boolean accept(File dir, String filename) {if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))return true;return false;}}));                mImgAdapter=new ImageAdapter(MainActivity.this, mImgs, folderBean.getDir());mGridView.setAdapter(mImgAdapter);mDirCount.setText(mImgs.size()+"");mDirName.setText(folderBean.getName());mPopupWindow.dismiss();//如果将程序挂在后台,然后在程序打开已经存在的目录进行增删图片,重进程序GridView显示的内容是不会更新的,//如果重新打开该目录所对应的文件,GridView显示内容会更新,但是PopupWindow中的ListView的文件夹下图片的数量不会更新//所以为了更新ListView的数据,需要加上下面的代码//且因为ListDirAdapter适配器中的getView方法(ListView一旦有变动就会调用getView重绘)有设置ListView的文件夹下图片的数量,//所以不需要另外调用ListDirAdapter实例的notifyDataSetChanged方法刷新数据for(FolderBean fb:mFolderBeans) {               if(fb.getDir().substring(fb.getDir().lastIndexOf("/")+1).equals(mCurrentDir.getName())) {if(fb.getCount()!=mImgs.size())fb.setCount(mImgs.size());}}}});}/*** 内容区域变暗*/private void lightOff() {WindowManager.LayoutParams lp=getWindow().getAttributes();//Attributes-属性lp.alpha=0.3f;getWindow().setAttributes(lp);}/*** 内容区域变亮*/private void lightOn() {WindowManager.LayoutParams lp=getWindow().getAttributes();//Attributes-属性lp.alpha=1.0f;getWindow().setAttributes(lp);}/*** 利用ContentProvider扫描手机中的所有图片*/private void initDatas() {if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {Toast.makeText(this, "当前存储卡不可用!", Toast.LENGTH_SHORT).show();return;}mProgressDialog = ProgressDialog.show(this, null, "Loading...");new Thread() {public void run() {         Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;//Media.EXTERNAL_CONTENT_URI——The content:// style URI for the "primary" external storage volume,primary:原始的,第一位//MediaStore这个类是android系统提供的一个多媒体数据库ContentResolver cr = MainActivity.this.getContentResolver();//内容提供器//这里需要注意"=? or "的空格不能丢Cursor cursor=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(cursor.moveToNext()) {String path=cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));File parentFile=new File(path).getParentFile();if(parentFile==null) continue;//这里需要判断一下,有时候parentFile会出现为null的情况,虽然不知道具体原因,有可能是因为图片被隐藏的原因String dirPath=parentFile.getAbsolutePath();//得到绝对路径if(mDirPaths.contains(dirPath)) continue;//防止重复遍历相同文件夹下的图片else {mDirPaths.add(dirPath);FolderBean folderBean=new FolderBean();folderBean.setDir(dirPath);folderBean.setFirstImgPath(path);if(parentFile.list()==null) continue;//parentFile.list():返回这个文件所代表的目录中的文件名的字符串数组。如果这个文件不是一个目录,结果是空的int picSize=parentFile.list(new FilenameFilter() {//设置过滤,防止非图片被计算@Overridepublic boolean accept(File dir, String filename) {if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))return true;return false;}}).length;folderBean.setCount(picSize);mFolderBeans.add(folderBean);if(picSize>mMaxCount) {mMaxCount=picSize;mCurrentDir=parentFile;}}       }cursor.close();//通知Handler扫描图片完成mHandler.sendEmptyMessage(DATA_LOADED);}}.start();}private void initView() {mGridView = (GridView) findViewById(R.id.id_gridview);mBottonLy = (RelativeLayout) findViewById(R.id.id_bottom_ly);mDirName = (TextView) findViewById(R.id.id_dir_name);mDirCount = (TextView) findViewById(R.id.id_dir_count);}
}

当然,还有相关的布局文件和要添加的权限:
1.activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent" ><GridView
        android:id="@+id/id_gridview"android:layout_width="match_parent"android:layout_height="match_parent"android:cacheColorHint="@android:color/transparent"android:gravity="center"android:clipChildren="true"android:listSelector="@android:color/transparent"android:numColumns="3"android:stretchMode="columnWidth"android:verticalSpacing="3dp"android:horizontalSpacing="3dp" ></GridView><RelativeLayout
        android:id="@+id/id_bottom_ly"android:layout_width="match_parent"android:layout_height="50dp"android:layout_alignParentBottom="true"android:background="#e0000000"android:clipChildren="true" ><TextView
            android:id="@+id/id_dir_name"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_centerVertical="true"android:paddingLeft="10dp"android:text="所有图片"android:textColor="@android:color/white" /><TextView
            android:id="@+id/id_dir_count"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:paddingRight="10dp"android:text="未知张数"android:textColor="@android:color/white" /></RelativeLayout></RelativeLayout>

android:stretchMode=”columnWidth”
参考网址:http://blog.csdn.net/java2009cgh/article/details/34836967

android:cacheColorHint=”@android:color/transparent” //去除拖动时默认的黑色背景
参考网址:
http://zhidao.baidu.com/link?url=spOGBRiyb150CGoNPMdUVU3cLGqzF-_bjC0GOSEHYLp9AR17ywM0NaQGmWRYUHpTIBOn6baKK46Q-h-xOGRVpKTDkq4_0c38VkZu4zo9aKe
把cacheColorHint这个属性去掉的话,滑动ListView的话会看到item一闪一闪的变颜色,cacheColorHint从字面上就可以看出和缓存有关,一般是设置为null或者是#00000000(透明)也可以

android:listSelector=”@android:color/transparent” //防止在拖拽的时候闪现出黑色
参考网址:http://blog.csdn.net/gchk125/article/details/7586401

android:clipChildren的意思:是否限制子View在其范围内
参考网址:http://www.cnblogs.com/over140/p/3508335.html

2.item_griview.xml (GridView的item布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent" ><ImageView
        android:id="@+id/id_item_image"android:layout_width="match_parent"android:layout_height="100dp"android:scaleType="centerCrop"android:src="@drawable/pictures_no" /><!--android:scaleType是控制图片如何resized/moved来匹对ImageView的size centerCrop  按比例扩大图片的size居中显示,使得图片长(宽)等于或大于View的长(宽)--><ImageButton
        android:id="@+id/id_item_select"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_alignParentTop="true"android:layout_marginRight="3dp"android:layout_marginTop="3dp"android:background="@null"android:clickable="false"android:src="@drawable/picture_unselected" /><!-- android:clickable="false" 默认不被选择 --></RelativeLayout>

3.popup_main.xml (弹窗的布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff" ><ListView
        android:id="@+id/id_list_dir"android:layout_width="match_parent"android:layout_height="match_parent"android:divider="#eee3d9"android:dividerHeight="1px" ></ListView></RelativeLayout>

4.item_popup_main.xml (PopupWindow 中ListView的item的布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="5dp" ><ImageView
        android:id="@+id/id_id_dir_item_image"android:layout_width="100dp"android:layout_height="100dp"android:layout_alignParentLeft="true"android:layout_centerVertical="true"android:background="@drawable/pic_dir"android:paddingBottom="17dp"android:paddingLeft="12dp"android:paddingRight="12dp"android:paddingTop="9dp"android:scaleType="fitXY"android:src="@drawable/ic_launcher" /><LinearLayout
        android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:layout_marginLeft="10dp"android:layout_toRightOf="@+id/id_id_dir_item_image"android:orientation="vertical" ><TextView
            android:id="@+id/id_dir_item_name"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="所有图片"android:textSize="12sp" /><TextView
            android:id="@+id/id_dir_item_count"android:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="暂无"android:textSize="10sp"android:textColor="#444"/></LinearLayout><ImageView android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:layout_marginRight="20dp"android:src="@drawable/dir_choose"/></RelativeLayout>

要添加的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


哦,对了,如果要实现PopupWindow的弹出与收起的动画话,所以还需要小小的几步:
1.在res下新建文件夹,并且添加两个xml文件
slide_down.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translate android:fromXDelta="0"android:toXDelta="0"android:fromYDelta="0"android:toYDelta="100%"android:duration="200"/>
</set>

slide_up.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translate android:fromXDelta="0"android:toXDelta="0"android:fromYDelta="100%"android:toYDelta="0"android:duration="200"/>
</set>

2.在res/values下的styles.xml中添加下面几行代码:

<style name="dir_popupwindow_anim"><item name="android:windowEnterAnimation">@anim/slide_up</item><item name="android:windowExitAnimation">@anim/slide_down</item></style>

源码:http://download.csdn.net/download/qq_22804827/9420428


当然,这个程序还存在着可以改善的地方,比如:

1.如果将程序挂在后台,然后新建一个文件夹,在里面添加一些图片,然后重进程序,该文件夹不能被扫描到,除非重启程序2.在已有文件夹下增减图片,如果不在PopupWindow中重新点击该文件夹,增减的图片是不会刷新的
//必须在查找前进行全盘的扫描,否则新加入的图片是无法得到显示的(加入对sd卡操作的权限) 参考网址:http://www.2cto.com/kf/201305/214899.html//sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
//Uri.parse("file://"+ Environment.getExternalStorageDirectory())));

如果有想法的,可以去完善完善哦!


新知识点总结:
ImageLoader的getImageViewFieldValue方法中Field的使用

Android-仿微信图片选择器相关推荐

  1. Android 仿微信图片选择器

    版权声明:本文为博主原创文章,未经博主允许不得转载. 1.自我介绍 这是我写的第一篇博客,首先做一下自我介绍,我去年刚毕业,大学学的是计算机专业,期间也学了一门Android相关课程,但是你懂的,一个 ...

  2. android仿微信图片选择器

    最近根据项目需求,要做一个仿微信图片选择的功能.首先我们先来整理一下思路. 1.显示选择图片的界面 1.1选择的图片数量小于9 最后一张图片是一个加号. 1.2选择的图片数量等于9,加号消失. 2.选 ...

  3. android微信图片选择框架,Android仿微信图片选择器ImageSelector使用详解

    今天给大家介绍一个仿微信的图片选择器:ImageSelector.ImageSelector支持图片的单选.限数量的多选和不限数量的多选.支持图片预览和图片文件夹的切换.在上一篇文章 <Andr ...

  4. Android 仿微信图片选择器 PictureSelector3.0 的使用

    在做项目时经常会遇到图片选着,选择单张图片还好,但类似于微信发朋友圈时可以多图选择的时候,就有点手足无措.然后在网上看了很多类似的项目,也尝试过将他们用于自己的项目,比如 知乎开源图片选择库 Mati ...

  5. 【图片选择】Android 仿微信图片选择器疾风加载 (单选,多选,相机)--唯剑做伴且随疾风前行

    请下载必要的配置文件按里面的目录copy导入使用 效果图 配置 app graild加依赖:compile 'com.squareup.picasso:picasso:2.4.0' 权限: <u ...

  6. android仿微信图片编辑库,Android仿微信图片选择器

    很多项目要用到图片选择控件,每次都要写一大堆逻辑.于是基于图片选择组件(PhotoPicker)封装了一个控件PhotoUploadView.方便简易,一键集成,几句代码就可以添加类似微信的图片选择控 ...

  7. android仿微信图片上传进度,android高仿微信发布动态(选择图片)

    [实例简介]Android 超高仿微信图片选择器 [实例截图] [核心代码] public class MainActivity extends Activity implements OnImage ...

  8. android 点击图片动画效果,Android仿微信图片点击全屏效果

    废话不多说,先看下Android图片点击全屏效果: 先是微信的 再是模仿的 先说下实现原理,再一步步分析 这里总共有2个Activity一个就是主页,一个就是显示我们图片效果的页面,参数通过Inten ...

  9. android点击加号,Android仿微信图片上传带加号且超过最大数隐藏功能

    1.仿照微信空间上传图片,显示图片数量以及超过最大,上传按钮隐藏功能 2.上效果图 3.上代码,主要是Adapter类 /** * Created by zhangyinlei on 2018/3/2 ...

  10. android 仿微信 视频播放器,Android仿微信多媒体选择器 - SmartMediaPicker

    2019.5.27 更新 版本已更新至[1.1.1]详情参考GitHub. 多媒体选择器 SmartMediaPicker 好久没写简书了,这次带来的是自己封装的一个多媒体选择器.这是一款方便好用的仿 ...

最新文章

  1. 在家办公如何提高效率?
  2. icmp 报文中的进程号
  3. CSS:CSS 在工程中改变——面向对象的CSS (OO CSS)
  4. php 二维sort,php 二维数组排序
  5. vue安装教程及简介
  6. java学习(52):抽象类
  7. 【代码笔记】Web-CSS-CSS Text(文本)
  8. jstree中文api文档_还在用 Swagger(丝袜哥)生成接口文档?我推荐你试试它。。。...
  9. Java Web学习总结(42)——JavaEE常用的13种核心API与组件
  10. javaScript遍历对象
  11. Microsoft Caffe(msCaffe)无GPU快速配置
  12. python控制浏览器最小化_如何在selenium webdriver 3中最小化浏览器窗口
  13. MK808 vs Raspberry Pi
  14. python画地图模拟迁徙_python爬取百度地图迁徙-迁入地来源和迁出目的地
  15. NFT游戏有哪些?盘点当前热门的NFT游戏
  16. 信度效度难度区分度是什么意思_信度、效度、难度、区分度
  17. mysql koa2的分页查询_koa2 快速实现注册、登录+分页(一)
  18. 互联网金融风控大数据技术应用
  19. L1-norm (L1范数) L2-norm(L2范数)
  20. 探岳android auto,“本是同根生”它俩谁最优 柯迪亚克 VS 探岳

热门文章

  1. 单应性矩阵的求解过程及应用
  2. 火山小视频消重复技巧 怎么看视频md5
  3. sbm预测matlab,**matlab计算非期望产出sbm模型代码**
  4. 基于集成运放心电放大器设计(原创)
  5. 计算机硬件技术基础实验教程实验报告,计算机硬件技术基础实验报告精选.doc...
  6. Android10.0(Q) 默认应用设置(电话、短信、浏览器、主屏幕应用)
  7. windows API 第八篇 _tcsicmp _stricmp _wcsicmp _mbsicmp
  8. Visual C++ Tips: warning C4996: 'stricmp' was declared deprecated
  9. 告别CSDN,转去博客园(http://www.cnblogs.com/organic/)
  10. pydicom数据的读取