Android 高级UI设计笔记06:仿微信图片选择器(转载)

仿微信图片选择器:

一、项目整体分析:

1. Android加载图片的3个目标:

(1)尽可能的去避免内存溢出。

  a. 根据图片的显示大小压缩图片

  b. 使用缓存对我们图片进行管理(LruCache)

(2)用户操作UI控件必须充分的流畅。

  a. getView里面尽可能不去做耗时的操作(异步加载 + 回调显示)

(3)用户预期显示的图片尽可能的快(图片的加载策略的选择,一般选择是LIFO)。

  a. LIFO

2. 定义一个Imageloader完成上面1中的3个目标:

Imageloader

getView()

{

    url   -> Bitmap

    url   -> LruCache 查找

                           ->找到返回

         ->找不到 url -> Task -> TaskQueue且发送一个通知去提醒后台轮询线程。

}

 •Task ->run() {根据url加载图片:

                  1. 获得图片显示的大小

                  2. 使用Options对图片进行压缩

                  3. 加载图片且放入LruCache

           }

   后台轮询线程

    TaskQueue ->Task ->将Task交给线程池去执行(执行run方法)

      一般情况下:(我们没有采用,效率低)

     new  Thread() {

                    run() {

                             while(true) {}

                     }

      }.start();

     这里这种场景,采用Handler + looper + Message

    

3. 项目最终的效果:

(1)默认显示图片最多的文件夹图片,以及底部显示图片总数量。如下图:

(2)点击底部,弹出popupWindow,popupWindow包含所有含有图片的文件夹,以及显示每个文件夹中图片数量。如下图:

      (注:此时Activity变暗

(3)选择任何文件夹,进入该文件夹图片显示,可以点击选择图片,当然了,点击已选择的图片则会取消选择。如下图:

    (注:选中图片变暗

二、代码实践 - 图片缓存、获取、展示

1.  打开Eclipse,新建一个Android工程,命名为"Imageloader",如下:

2. 新建一个包"com.himi.imageloader.util",编写一个图片加载工具类,如下:

ImageLoader.java,如下:

  1 package com.himi.imageloader.util;
  2 
  3 import java.lang.reflect.Field;
  4 import java.util.LinkedList;
  5 import java.util.concurrent.ExecutorService;
  6 import java.util.concurrent.Executors;
  7 import java.util.concurrent.Semaphore;
  8 
  9 import android.annotation.SuppressLint;
 10 import android.graphics.Bitmap;
 11 import android.graphics.BitmapFactory;
 12 import android.graphics.BitmapFactory.Options;
 13 import android.os.Handler;
 14 import android.os.Looper;
 15 import android.os.Message;
 16 import android.util.DisplayMetrics;
 17 import android.util.LruCache;
 18 import android.view.ViewGroup.LayoutParams;
 19 import android.widget.ImageView;
 20 
 21 /**
 22  * 图片加载类
 23  * 这个类使用单例模式
 24  * @author hebao
 25  *
 26  */
 27 public class ImageLoader {
 28     private static ImageLoader mInstance;
 29     /**
 30      * 图片缓存的核心对象 
 31      *      管理我们所有图片加载的所需的内存
 32      */
 33     private LruCache<String, Bitmap> mLruCache;
 34     /**
 35      * 线程池
 36      *      执行一些我们加载图片的任务
 37      */
 38     private ExecutorService mThreadPool;
 39     /**
 40      * 线程池中默认线程数
 41      */
 42     private static final int DEAFULT_THREAD_COUNT = 1;
 43     
 44     /**
 45      * 队列的调度方式
 46      */
 47     private Type mType = Type.LIFO;
 48     /**
 49      * 任务队列
 50      *         任务队列提供给线程池取任务的
 51      */
 52     private LinkedList<Runnable> mTaskQueue;
 53     /**
 54      * 后台轮询线程
 55      */
 56     private Thread mPoolThread;
 57     /**
 58      * 后台轮询线程的handler
 59      */
 60     private Handler mPoolThreadHandler;
 61     /**
 62      * UI线程的handler
 63      * 用于:更新ImageView
 64      */
 65     private Handler mUIHandler;
 66     /**
 67      * mPoolThreadHandler的信号量,防止使用mPoolThreadHandler的时候其本身没有初始化完毕,报空指针异常
 68      */
 69     private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
 70     /**
 71      * 任务线程信号量,保证线程池真正做到LIFO
 72      */
 73     private Semaphore mSemaphoreThreadPool;
 74     
 75     /**
 76      * 
 77      * 调度方式
 78      *FIFO:先入先出
 79      *LIFO:后入先出
 80      */
 81     
 82     public enum Type  {
 83         FIFO,LIFO;
 84     }
 85     
 86     
 87     private ImageLoader(int threadCount, Type type) {
 88         init(threadCount, type);
 89     }
 90     
 91     /**
 92      * 初始化操作
 93      * @param threadCount
 94      * @param type
 95      */
 96     private void init(int threadCount, Type type) {
 97         //后台轮询线程初始化
 98         mPoolThread = new Thread() {
 99             @Override
100             public void run() {
101                 Looper.prepare();
102                 mPoolThreadHandler = new Handler() {
103                     @Override
104                     public void handleMessage(Message msg) {
105                         //线程池取出一个任务进行执行
106                         mThreadPool.execute(getTask());
107                         try {
108                             mSemaphoreThreadPool.acquire();
109                         } catch (InterruptedException e) {
110                             // TODO 自动生成的 catch 块
111                             e.printStackTrace();
112                         }
113                     }
114                 };
115                 //释放一个信号量
116                 mSemaphorePoolThreadHandler.release();
117                 //Looper不断进行轮询
118                 Looper.loop();
119             };
120         };
121         mPoolThread.start();
122         
123         //获取我们应用的最大可用内存
124         int maxMemory = (int) Runtime.getRuntime().maxMemory();
125         int cacheMemory = maxMemory / 8;
126         //图片缓存初始化
127         mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
128             /**
129              * 测量每一个Bitmap图片的大小
130              */
131             @Override
132             protected int sizeOf(String key, Bitmap value) {
133                 // 每一个Bitmap图片的大小 = 每一行字节数 * 高度
134                 return value.getRowBytes() * value.getHeight();
135             }
136         };
137         
138         //创建线程池
139         mThreadPool = Executors.newFixedThreadPool(threadCount);
140         mTaskQueue = new LinkedList<Runnable>();
141         mType = type;
142         
143         //初始化信号量
144         mSemaphoreThreadPool = new Semaphore(threadCount);
145     }
146     
147     /**
148      * 从任务队列中取出一个方法 
149      * @return
150      */
151     private Runnable getTask() {
152         if(mType == Type.FIFO) {
153             return mTaskQueue.removeFirst();
154         }else if(mType == Type.LIFO) {
155             return mTaskQueue.removeLast();
156         }
157         return null;
158     }
159     
160 
161     public static ImageLoader getInstance() {
162         if(mInstance == null) {
163             synchronized (ImageLoader.class) {
164                 if(mInstance == null) {
165                     mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO);
166                 }
167             }
168             
169         }
170         return mInstance;
171     }
172     
173     public static ImageLoader getInstance(int threadCount, Type type) {
174         if(mInstance == null) {
175             synchronized (ImageLoader.class) {
176                 if(mInstance == null) {
177                     mInstance = new ImageLoader(threadCount, type);
178                 }
179             }
180             
181         }
182         return mInstance;
183     }
184     
185     
186     /**
187      * 根据path为ImageView是设置图片
188      * @param path
189      * @param imageView
190      */
191     public void loadImage(final String path, final ImageView imageView ) {
192         imageView.setTag(path);//设置Tag主要是为了校验,防止图片的混乱
193         if(mUIHandler == null) {
194             mUIHandler = new Handler() {
195                 @Override
196                 public void handleMessage(Message msg) {
197                     //获取得到图片,为imageview回调设置图片
198                     ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
199                     Bitmap bm = holder.bitmap;
200                     ImageView imageview = holder.imageView;
201                     String path = holder.path;
202                     /**
203                      * 将path和getTag存储路径进行比较
204                      * 如果不比较,就会出现我们滑动到第二张图片,但是显示的还是第一张的图片
205                      * 这里我们绑定imageview和path就是为了防止这种情况
206                      */
207                     if(imageview.getTag().toString().equals(path)) {
208                         imageview.setImageBitmap(bm);
209                     }
210                     
211                 };
212             };
213         }
214         //根据path在缓存中获取bitmap
215         Bitmap bm = getBitmapFromLruCache(path);
216         if(bm != null) {
217             refreashBitmap(path, imageView, bm);    
218         } else {//内存中没有图片,加载图片到内存
219             addTasks(new Runnable() {
220                 public void run() {
221                     /**加载图片
222                      *  图片的压缩
223                      */
224                     //1. 获得图片需要显示的大小
225                     ImageSize imageSize = getImageViewSize(imageView);
226                     //2. 压缩图片
227                     Bitmap bm = decodeSampleBitmapFromPath(path,imageSize.width,imageSize.height);
228                     //3. 把图片加载到缓存 (一定要记得)
229                     addBitmapToLruCache(path,bm);        
230                     refreashBitmap(path, imageView, bm);    
231                     //每次线程任务加载完图片,之后释放一个信号量,即:信号量-1,此时就会寻找下一个任务(根据FIFO/LIFO不同的策略取出任务)
232                     mSemaphoreThreadPool.release();
233                 }
234 
235             });
236         }
237     }
238     
239     
240     public void refreashBitmap(final String path,
241             final ImageView imageView, Bitmap bm) {
242         Message message = Message.obtain();    
243         ImgBeanHolder holder = new ImgBeanHolder();
244         holder.bitmap = bm;
245         holder.path = path;
246         holder.imageView = imageView;
247         
248         message.obj = holder;
249         mUIHandler.sendMessage(message);
250     }                
251     
252     /**
253      * 将图片加入缓存LruCache
254      * @param path
255      * @param bm
256      */
257     private void addBitmapToLruCache(String path, Bitmap bm) {
258         if(getBitmapFromLruCache(path) == null) {
259             if(bm != null) {
260                 mLruCache.put(path, bm);
261             }
262         }
263         
264     }
265 
266     
267     /**
268      * 根据图片需要显示的宽和高,对图片进行压缩
269      * @param path
270      * @param width
271      * @param height
272      * @return
273      */
274     private Bitmap decodeSampleBitmapFromPath(String path,
275             int width, int height) {
276         //获取图片的宽和高,但是不把图片加载到内存中
277         BitmapFactory.Options options = new BitmapFactory.Options();
278         options.inJustDecodeBounds =true;//不把图片加载到内存中
279         BitmapFactory.decodeFile(path, options);
280         
281         options.inSampleSize = caculateInSampleSize(options,width, height);//计算获取压缩比
282         //使用获取到的inSampleSize再次解析图片
283         options.inJustDecodeBounds =false;//加载图片到内存
284         Bitmap bitmap = BitmapFactory.decodeFile(path, options);
285         
286         
287         return bitmap;
288     }
289 
290     
291     /**
292      *根据需求的宽和高,以及图片实际的宽和高,计算inSampleSize
293      * @param options
294      * @param width
295      * @param height
296      * @return inSampleSize 压缩比
297      */
298     private int caculateInSampleSize(Options options, int reqWidth, int reqHeight) {
299         int width = options.outWidth;
300         int height = options.outHeight;
301         
302         int inSampleSize = 1;
303         if(width>reqWidth || height > reqHeight) {
304             int widthRadio = Math.round(width*1.0f / reqWidth);
305             int heightRadio = Math.round(height*1.0f / reqHeight);
306             
307             inSampleSize = Math.max(widthRadio, heightRadio);    
308         }
309         
310         return inSampleSize;
311     }
312 
313     /**
314      * 根据ImageView获取适当的压缩的宽和高
315      * @param imageView
316      * @return
317      */
318     protected ImageSize getImageViewSize(ImageView imageView) {
319         ImageSize imageSize = new ImageSize();
320         DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
321         LayoutParams lp = imageView.getLayoutParams();
322         
323         int width = imageView.getWidth();//获取imageview的实际宽度
324         if(width<=0) {
325             width = lp.width;//获取imageview在layout中声明的宽度
326         }
327         if(width<=0) {
328             width = getImageViewFieldValue(imageView, "mMaxWidth");//利用反射,检测获得最大值
329         }
330         if(width<=0) {
331             width = displayMetrics.widthPixels;
332         }
333         
334         
335         int height = imageView.getHeight();//获取imageview的实际高度
336         if(height<=0) {
337             height = lp.height;//获取imageview在layout中声明的高度
338         }
339         if(height<=0) {
340             height = getImageViewFieldValue(imageView, "mMaxHeight");//利用反射,检测获得最大值
341         }
342         if(height<=0) {
343             height = displayMetrics.heightPixels;
344         }
345         
346         imageSize.width = width;
347         imageSize.height = height;
348         return imageSize;
349     };
350     
351 /**
352  * 
353  * 通过反射获取imageview的某个属性值
354  * @param object
355  * @param fieldName
356  * @return
357  * 由于方法getMaxHeight是API16以上的才能使用,这里我们用反射使用这个方法
358  */
359 private static int getImageViewFieldValue(Object object, String fieldName) {
360     int value=0;
361         try {
362             Field field = ImageView.class.getDeclaredField(fieldName);
363             field.setAccessible(true);
364 
365             int fieldValue = field.getInt(object);
366             if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
367                 value = fieldValue;
368             }
369         } catch (Exception e) {
370             // TODO 自动生成的 catch 块
371             e.printStackTrace();
372         }
373     return value;
374 }
375     
376 /**
377  * 添加任务到任务队列,交给线程池执行
378  * @param runnable
379  */
380     @SuppressLint("NewApi")
381     private synchronized void addTasks(Runnable runnable) {//synchronized同步代码,防止多个线程进来出现死锁
382         mTaskQueue.add(runnable);
383         //if(mPoolThreadHandler == null) wait();
384         //确保我们在使用mPoolThreadHandler之前,我们初始化完毕mPoolThreadHandler(不为空),这里引入信号量
385         try {
386             if(mPoolThreadHandler == null) {
387                 mSemaphorePoolThreadHandler.acquire();
388             }    
389         } catch (InterruptedException e) {
390             // TODO 自动生成的 catch 块
391             e.printStackTrace();
392         }
393         mPoolThreadHandler.sendEmptyMessage(0x110);
394         
395         
396     }
397     
398 
399     /**
400      * 根据path在缓存中获取bitmap
401      * @param key
402      * @return
403      */
404     private Bitmap getBitmapFromLruCache(String key) {
405         // TODO 自动生成的方法存根
406         return mLruCache.get(key);
407     }
408     
409     /**
410      * 压缩图片之后的宽和高
411      * @author Administrator
412      *
413      */
414     private class ImageSize {
415         int width;
416         int height;
417     }
418     
419     private class ImgBeanHolder {
420         Bitmap bitmap;
421         ImageView imageView;
422         String path;
423     }
424 
425 }

三、代码实践 - UI、UI适配器

1. 布局文件设计,首先我们从美工那边获得布局设计需要的图片,如下:

来到activity_main.xml,如下:

 1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     tools:context="com.himi.imageloader.MainActivity" >
 6 
 7     <!--
 8      android:numColumns="3" 设置显示的列数
 9      android:stretchMode="columnWidth" 缩放与列宽大小同步
10      android:cacheColorHint="@android:color/transparent" 自定义GridView拖动背景色
11      android:listSelector="@android:color/transparent" 选中item,item显示透明
12     -->
13 
14     <GridView
15         android:id="@+id/id_gridView"
16         android:layout_width="match_parent"
17         android:layout_height="match_parent"
18         android:cacheColorHint="@android:color/transparent"
19         android:horizontalSpacing="3dp"
20         android:listSelector="@android:color/transparent"
21         android:numColumns="3"
22         android:stretchMode="columnWidth"
23         android:verticalSpacing="3dp" />
24     <RelativeLayout 
25         android:layout_width="match_parent"
26         android:layout_height="50dp"
27         android:layout_alignParentBottom="true"
28         android:background="#ee000000"
29         android:clipChildren="true"
30         android:id="@+id/id_bottom_ly"
31         >
32         <TextView 
33             android:id="@+id/id_dir_name"
34             android:layout_width="wrap_content"
35             android:layout_height="wrap_content"
36             android:layout_alignParentLeft="true"
37             android:layout_centerVertical="true"
38             android:paddingLeft="10dp"
39             android:text="所有图片"
40             android:textColor="@android:color/white"
41             />
42          <TextView 
43             android:id="@+id/id_dir_count"
44             android:layout_width="wrap_content"
45             android:layout_height="wrap_content"
46             android:layout_alignParentRight="true"
47             android:layout_centerVertical="true"
48             android:paddingRight="10dp"
49             android:text="100张"
50             android:textColor="@android:color/white"
51             />
52         
53     </RelativeLayout>
54     
55 
56 </RelativeLayout>

显示布局效果如下:

来到item_gridview.xml,如下:

 1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     tools:context="com.himi.imageloader.MainActivity" >
 6 
 7     <!-- android:scaleType="centerCrop" 防止图片变形 -->
 8 
 9     <ImageView
10         android:id="@+id/id_item_image"
11         android:layout_width="match_parent"
12         android:layout_height="100dp"
13         android:scaleType="centerCrop"
14         android:src="@drawable/pictures_no" />
15 
16     <ImageButton 
17         android:id="@+id/id_item_select"
18         android:clickable="false"
19         android:layout_width="wrap_content"
20         android:layout_height="wrap_content"
21         android:layout_alignParentRight="true"
22         android:layout_alignParentTop="true"
23         android:layout_marginTop="3dp"
24         android:layout_marginRight="3dp"
25         android:background="@null"
26         android:src="@drawable/picture_unselected"
27         />
28 
29 </RelativeLayout>

布局效果如下:

2. 这里我们首先对手机中图片进行扫描,拿到图片数量最多的,直接显示在GridView上;并且扫描结束,得到一个所有包含图片的文件夹信息的集合。为了便于存储手机中所有文件夹信息,我们单独创建一个Bean实体类,命名为"FolderBean",新建包com.himi.imageloader.bean,将这个类放在里面,如下:

 1 package com.himi.imageloader.bean;
 2 
 3 /**
 4  * FolderBean :图片的文件夹信息类
 5  * 
 6  * 注意:
 7  *    用来存储当前文件夹的路径,当前文件夹包含多少张图片,以及第一张图片路径用于做文件夹的图标;
 8  *    注:文件夹的名称,我们在set文件夹的路径的时候,自动提取,仔细看下setDir这个方法.
 9  * 
10  * @author hebao
11  * 
12  */
13 
14 public class FolderBean {
15     /**
16      * 图片的文件夹路径
17      */
18     private String dir;
19 
20     /**
21      * 第一张图片的路径
22      */
23     private String firstImgPath;
24 
25     /**
26      * 文件夹的名称
27      */
28     private String name;
29 
30     /**
31      * 图片的数量
32      */
33     private int count;
34 
35     public String getDir() {
36         return dir;
37     }
38 
39     public void setDir(String dir) {
40         this.dir = dir;
41         int lastIndexOf = this.dir.lastIndexOf("/");
42         this.name = this.dir.substring(lastIndexOf);
43     }
44 
45     public String getFirstImgPath() {
46         return firstImgPath;
47     }
48 
49     public void setFirstImgPath(String firstImgPath) {
50         this.firstImgPath = firstImgPath;
51     }
52 
53     public String getName() {
54         return name;
55     }
56 
57     public int getCount() {
58         return count;
59     }
60 
61     public void setCount(int count) {
62         this.count = count;
63     }
64 
65 }

 3. 接下来自然要说到扫描手机图片的代码,在MainActivity中,如下:

  1     @Override
  2     protected void onCreate(Bundle savedInstanceState) {
  3         super.onCreate(savedInstanceState);
  4         setContentView(R.layout.activity_main);
  5         initView();
  6         initDatas();
  7         initEvent();
  8     }
  9 
 10     private void initView() {
 11         mGridView = (GridView) findViewById(R.id.id_gridView);
 12         mBottomLy = (RelativeLayout) findViewById(R.id.id_bottom_ly);
 13         mDirName = (TextView) findViewById(R.id.id_dir_name);
 14         mDirCount = (TextView) findViewById(R.id.id_dir_count);
 15 
 16     }
 17 
 18     /**
 19      * 利用ContentProvider扫描手机中的图片,此方法在运行在子线程中 完成图片的扫描,最终获得jpg最多的那个文件夹 
 20      */
 21     private void initDatas() {
 22 
 23         if (!Environment.getExternalStorageState().equals(
 24                 Environment.MEDIA_MOUNTED)) {
 25             Toast.makeText(this, "当前存储卡不可用", Toast.LENGTH_SHORT).show();
 26             return;
 27         }
 28         /**
 29          * 显示进度条
 30          */
 31         mProgressDialog = ProgressDialog.show(this, null, "正在加载……");
 32         /**
 33          * 扫描手机中所有的图片,很明显这是一个耗时的操作,所以我们不能在UI线程中,采用子线程.
 34          * 扫描得到的文件夹及其图片信息 在 List<FolderBean> mFolderBeans存储.
 35          */
 36         new Thread() {
 37             public void run() {
 38                 Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
 39                 ContentResolver cr = MainActivity.this.getContentResolver();
 40                 //只查询jpeg和png的图片  
 41                 Cursor cursor = cr.query(mImgUri, null,
 42                         MediaStore.Images.Media.MIME_TYPE + "? or"
 43                                 + MediaStore.Images.Media.MIME_TYPE + "?",
 44                         new String[] { "image/jpeg", "image/png", },
 45                         MediaStore.Images.Media.DATE_MODIFIED);
 46 
 47                 /**
 48                  * 存放已经遍历的文件夹路径,防止重复遍历
 49                  */
 50                 Set<String> mDirPaths = new HashSet<String>();
 51                 /**
 52                  * 遍历手机图片
 53                  */
 54                 while (cursor.moveToNext()) {
 55                     // 获取图片的路径  
 56                     String path = cursor.getString(cursor
 57                             .getColumnIndex(MediaStore.Images.Media.DATA));
 58                     // 获取该图片的父路径名 
 59                     File parentFile = new File(path).getParentFile();
 60                     if (parentFile == null) {
 61                         continue;
 62                     }
 63                     String dirPath = parentFile.getAbsolutePath();
 64 
 65                     FolderBean folderBean = null;
 66                      // 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~)  
 67                     if (mDirPaths.contains(dirPath)) {
 68                         continue;
 69                     } else {
 70                         mDirPaths.add(dirPath);
 71                         // 初始化imageFloder  
 72                         folderBean = new FolderBean();
 73                         
 74                         //图片的文件夹路径
 75                         folderBean.setDir(dirPath);
 76                         //第一张图片的路径
 77                         folderBean.setFirstImgPath(path);
 78                     }
 79                     //有些图片比较诡异~~;无法显示,这里加判断,防止空指针异常
 80                     if (parentFile.list() == null) {
 81                         continue;
 82                     }
 83 
 84                     int picSize = parentFile.list(new FilenameFilter() {
 85 
 86                         public boolean accept(File dir, String filename) {
 87                             if (filename.endsWith(".jpg")
 88                                     || filename.endsWith(".jpeg")
 89                                     || filename.endsWith(".png")) {
 90                                 return true;
 91                             }
 92                             return false;
 93                         }
 94                     }).length;
 95                     //图片的数量
 96                     folderBean.setCount(picSize);
 97                     mFolderBeans.add(folderBean);
 98                     /**
 99                      * 如果此时扫描到图片文件夹中图片数量最多,则赋值给mMaxCount,mCurrentDir
100                      */
101                     if (picSize > mMaxCount) {
102                         mMaxCount = picSize;
103                         mCurrentDir = parentFile;
104                     }
105 
106                 }
107                 //关闭游标
108                 cursor.close();
109                 // 通知handler扫描图片完成
110                 mHandler.sendEmptyMessage(DATA_LOADED);
111 
112             };
113         }.start();
114 
115     }

initView就不看了,都是些findViewById;

initDatas主要就是扫描图片的代码,我们开启了一个Thread进行扫描,扫描完成以后,我们得到了图片最多文件夹路径(mCurrentDir),手机中图片数量(totalCount);以及所有包含图片文件夹信息(mFolderBeans)

然后在MainActivity,我们通过handler发送消息,在handleMessage里面:

1)创建GridView的适配器,为我们的GridView设置适配器,显示图片;

2)有了mFolderBeans,就可以创建我们的popupWindow了;

 1 private Handler mHandler = new Handler() {
 2 
 3         public void handleMessage(android.os.Message msg) {
 4             if (msg.what == DATA_LOADED) {
 5                 mProgressDialog.dismiss();
 6                 // 绑定数据到GridView
 7                 data2View();
 8                 // 初始化PopupWindow
 9                 initDirPopupWindow();
10             }
11         }
12     };

可以看到分别干了上述的两件事:

1)在MainActivity中,data2View如下:

data2View就是我们当前Activity上所有的View设置数据了。

 1 /**
 2      * 为View绑定数据 
 3      */
 4     private void data2View() {
 5         if (mCurrentDir == null) {
 6             Toast.makeText(this, "未扫描到任何图片", Toast.LENGTH_SHORT).show();
 7             return;
 8         }
 9 
10         mImgs = Arrays.asList(mCurrentDir.list());
11         
12         /** 
13          * 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗; 
14          */  
15         mImgAdapter = new ImageAdapter(this, mImgs,
16                 mCurrentDir.getAbsolutePath());
17         mGridView.setAdapter(mImgAdapter);
18 
19         mDirCount.setText(mMaxCount + "");
20         mDirName.setText(mCurrentDir.getName());
21 
22     };

2)看到上面(1)还用到了一个Adapter(for GridView),我们自定义一个适配器ImageAdapter继承自BaseAdapter它和MainActivity所处一个包下,如下

  1 package com.himi.imageloader;
  2 
  3 import java.util.HashSet;
  4 import java.util.List;
  5 import java.util.Set;
  6 
  7 import android.content.Context;
  8 import android.graphics.Color;
  9 import android.view.LayoutInflater;
 10 import android.view.View;
 11 import android.view.View.OnClickListener;
 12 import android.view.ViewGroup;
 13 import android.widget.BaseAdapter;
 14 import android.widget.ImageButton;
 15 import android.widget.ImageView;
 16 
 17 import com.himi.imageloader.util.ImageLoader;
 18 import com.himi.imageloader.util.ImageLoader.Type;
 19 
 20 public class ImageAdapter extends BaseAdapter {
 21         /** 
 22          * 用户选择的图片,存储为图片的完整路径 
 23          */  
 24         private static Set<String> mSelectedImg = new HashSet<String>();
 25         /** 
 26          * 文件夹路径 
 27          */  
 28         private String mDirPath;
 29         private List<String> mImgPaths;
 30         private LayoutInflater mInflater;
 31         //分开存储文件目录,和文件名。节省内存
 32         public ImageAdapter(Context context, List<String> mDatas, String dirPath) {
 33             this.mDirPath = dirPath;
 34             this.mImgPaths = mDatas;
 35             mInflater = LayoutInflater.from(context);
 36         }
 37 
 38         public int getCount() {
 39             return mImgPaths.size();
 40         }
 41 
 42         public Object getItem(int position) {
 43             return mImgPaths.get(position);
 44         }
 45 
 46         public long getItemId(int position) {
 47             return position;
 48         }
 49 
 50         public View getView(final int position, View convertView, ViewGroup parent) {
 51             final ViewHolder viewHolder;
 52             if(convertView == null) {
 53                 convertView = mInflater.inflate(R.layout.item_gridview, parent,false);
 54                 
 55                 viewHolder = new ViewHolder();
 56                 viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_item_image);
 57                 viewHolder.mSelect = (ImageButton) convertView.findViewById(R.id.id_item_select);
 58                 convertView.setTag(viewHolder);        
 59             } else {
 60                 viewHolder = (ViewHolder) convertView.getTag();
 61             }
 62             
 63             /**
 64              * 重置状态,如果不重置第一次选中,第二次还会复用之前的,这样就会产生错乱
 65              */
 66             viewHolder.mImg.setImageResource(R.drawable.pictures_no);
 67             viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
 68             viewHolder.mImg.setColorFilter(null);
 69             
 70             ImageLoader.getInstance(3, Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position),
 71                     viewHolder.mImg);
 72             final String filePath = mDirPath+"/"+mImgPaths.get(position);
 73             
 74             // 设置ImageView的点击事件  
 75             viewHolder.mImg.setOnClickListener(new OnClickListener() {    
 76                 // 选择,则将图片变暗,反之则反之  
 77                 public void onClick(View v) {
 78                     //已经被选择
 79                     if(mSelectedImg.contains(filePath)) {
 80                         mSelectedImg.remove(filePath);
 81                         //改变Item状态,没有必要刷新显示
 82                         viewHolder.mImg.setColorFilter(null);
 83                         viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
 84                     }else {//未被选择
 85                         mSelectedImg.add(filePath);
 86                         //改变Item状态,没有必要刷新显示
 87                         viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
 88                         viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
 89                     }
 90                     //notifyDataSetChanged();不能使用,会出现闪屏
 91                     
 92                 }
 93             });
 94             
 95              /** 
 96              * 已经选择过的图片,显示出选择过的效果 
 97              */  
 98             if(mSelectedImg.contains(filePath)) {
 99                 viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
100                 viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
101             }
102            
103             return convertView;
104         }
105         
106         private class ViewHolder {
107             ImageView mImg;
108             ImageButton mSelect;
109         }
110         
111     }

图片策略我们使用的是LIFO后进先出。

到此我们的第一个Activity的所有的任务就完成了~~~

四、展现文件夹的PopupWindow

在我们要实现,点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;

不急着贴代码,我们先考虑下PopupWindow怎么用最好,我们的PopupWindow需要设置布局文件,需要初始化View,需要初始化事件,还需要和Activity交互~~

那么肯定的,我们使用独立的类,这个类和Activity很相似,在里面initView(),initEvent()之类的。

1.  自定义PopupWindow,命名为"ListImageDirPopupWindow ",如下:

  1 package com.himi.imageloader;
  2 
  3 import java.util.List;
  4 
  5 import android.content.Context;
  6 import android.graphics.drawable.BitmapDrawable;
  7 import android.util.DisplayMetrics;
  8 import android.view.LayoutInflater;
  9 import android.view.MotionEvent;
 10 import android.view.View;
 11 import android.view.View.OnTouchListener;
 12 import android.view.ViewGroup;
 13 import android.view.WindowManager;
 14 import android.widget.AdapterView;
 15 import android.widget.AdapterView.OnItemClickListener;
 16 import android.widget.ArrayAdapter;
 17 import android.widget.ImageView;
 18 import android.widget.ListView;
 19 import android.widget.PopupWindow;
 20 import android.widget.TextView;
 21 
 22 import com.himi.imageloader.bean.FolderBean;
 23 import com.himi.imageloader.util.ImageLoader;
 24 
 25 /**
 26  * 自定义的PopupWindow
 27  * 作用:展现文件夹信息
 28  * @author hebao
 29  *
 30  */
 31 public class ListImageDirPopupWindow extends PopupWindow {
 32     private int mWidth;
 33     private int mHeight;
 34     private View mConvertView;
 35     private ListView mListView;
 36     
 37     
 38     private List<FolderBean> mDatas;
 39     
 40     
 41     /**
 42      * 文件夹选中的监听器(接口)
 43      * @author hebao
 44      *
 45      */
 46     public interface OnDirSelectedListener {
 47         void onSelected(FolderBean folderBean);
 48     }
 49     public OnDirSelectedListener mListener;
 50     public void setOnDirSelectedListener (OnDirSelectedListener mListener) {
 51         this.mListener = mListener;
 52     }
 53     
 54     
 55 
 56     public ListImageDirPopupWindow(Context context, List<FolderBean> datas) {
 57         calWidthAndHeight(context);
 58         
 59         mConvertView = LayoutInflater.from(context).inflate(R.layout.popup_main, null);
 60         setContentView(mConvertView);
 61         
 62         setWidth(mWidth);
 63         setHeight(mHeight);
 64         
 65         //设置可触摸
 66         setFocusable(true);
 67         setTouchable(true);
 68         setOutsideTouchable(true);
 69         setBackgroundDrawable(new BitmapDrawable());
 70         
 71         setTouchInterceptor(new OnTouchListener() {
 72             
 73             public boolean onTouch(View v, MotionEvent event) {
 74                 if(event.getAction() == MotionEvent.ACTION_OUTSIDE){
 75                     dismiss();
 76                     return true;
 77                 }
 78                 return false;
 79             }
 80         });
 81         
 82         initViews(context);
 83         initEvent();
 84         
 85     }
 86 
 87     private void initViews(Context context) {
 88         mListView = (ListView) mConvertView.findViewById(R.id.id_list_dir);
 89         mListView.setAdapter(new ListDirAdapter(context, mDatas));
 90     }
 91     
 92     /**
 93      * 设置监听事件
 94      */
 95     private void initEvent() {
 96         mListView.setOnItemClickListener(new OnItemClickListener() {
 97 
 98             public void onItemClick(AdapterView<?> parent, View view,
 99                     int position, long id) {
100                 if(mListener != null) {
101                     mListener.onSelected(mDatas.get(position));
102                 }
103                 
104             }
105             
106         });
107         
108     }
109 
110     
111 
112     /**
113      * 计算popupWindow的宽度和高度
114      * @param context
115      */
116     private void calWidthAndHeight(Context context) {
117         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
118         //Andorid.util 包下的DisplayMetrics 类提供了一种关于显示的通用信息,如显示大小,分辨率和字体。
119         DisplayMetrics outMetrics = new DisplayMetrics();
120         wm.getDefaultDisplay().getMetrics(outMetrics);
121         
122         
123         mWidth = outMetrics.widthPixels;
124         mHeight = (int) (outMetrics.heightPixels * 0.7);
125     }
126     
127     
128     private class ListDirAdapter extends ArrayAdapter<FolderBean> {
129         private LayoutInflater mInflater;
130         private List<FolderBean> mDatas;
131         
132         public ListDirAdapter(Context context,
133                 List<FolderBean> objects) {
134             super(context, 0, objects);
135             mInflater = LayoutInflater.from(context);
136         }
137         
138         @Override
139         public View getView(int position, View convertView, ViewGroup parent) {
140             ViewHolder holder = null;
141             if(convertView == null) {
142                 holder = new ViewHolder();
143                 convertView = mInflater.inflate(R.layout.item_popup_main, parent, false);
144                 
145                 holder.mImg = (ImageView) convertView.findViewById(R.id.id_id_dir_item_image);
146                 holder.mDirName = (TextView) convertView.findViewById(R.id.id_dir_item_name);
147                 holder.mDirCount = (TextView) convertView.findViewById(R.id.id_dir_item_count);
148                 
149                 convertView.setTag(holder);
150             } else {
151                 holder =(ViewHolder) convertView.getTag();
152             }
153             FolderBean bean  =getItem(position);
154             //重置
155             holder.mImg.setImageResource(R.drawable.pictures_no);
156             
157             //回调加载图片
158             ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg);    
159             holder.mDirCount.setText(bean.getCount()+"");
160             holder.mDirName.setText(bean.getName());
161             return convertView;
162         }
163         
164         private class ViewHolder {
165             ImageView mImg;
166             TextView mDirName;
167             TextView mDirCount;
168         }
169     }
170 
171 }

好了,现在就是我们正在的popupWindow咯,布局文件夹主要是个ListView,所以在initViews里面,我们得设置它的适配器;当然了,这里的适配器依然用我们的ListDirAdapter。

 然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片;

关于交互,我们从Activity的角度去看弹出框,Activity想知道什么,只想知道选择了别的文件夹来告诉我,所以我们创建一个接口OnDirSelectedListener ,对Activity设置回调;initEvent初始化事件,如果有人设置了回调,我们就调用。

2.  接下来到MainActivity,完成MainActivity和PopupWindow的交互,如下:

上面说道,当扫描图片完成,拿到包含图片的文件夹信息列表;这个列表就是我们popupWindow所需的数据,所以我们的popupWindow的初始化在handleMessage(上面贴了handler的代码)里面:

在handleMessage里面调用 initDirPopupWindow

 1 /**
 2      * 初始化展示文件夹的popupWindw 
 3      */
 4     private void initDirPopupWindow() {
 5         mDirPopupWindow = new ListImageDirPopupWindow(this, mFolderBeans);
 6 
 7         mDirPopupWindow.setOnDismissListener(new OnDismissListener() {
 8 
 9             public void onDismiss() {
10                 lightOn();
11 
12             }
13         });
14 
15         /**
16          *  设置选择文件夹的回调  
17          */
18         mDirPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() {
19 
20             public void onSelected(FolderBean folderBean) {
21                 mCurrentDir = new File(folderBean.getDir());
22                 mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() {
23 
24                     public boolean accept(File dir, String filename) {
25                         if (filename.endsWith(".jpg")
26                                 || filename.endsWith(".jpeg")
27                                 || filename.endsWith(".png")) {
28                             return true;
29                         }
30                         return false;
31                     }
32                 }));
33 
34                 mImgAdapter = new ImageAdapter(MainActivity.this, mImgs,
35                         mCurrentDir.getAbsolutePath());
36                 mGridView.setAdapter(mImgAdapter);
37 
38                 mDirCount.setText(mImgs.size() + "");
39                 mDirName.setText(folderBean.getName());
40 
41                 mDirPopupWindow.dismiss();
42             }
43         });
44 
45     }
46 
47     /**
48      * 内容区域变亮
49      */
50 
51     protected void lightOn() {
52         WindowManager.LayoutParams lp = getWindow().getAttributes();
53         lp.alpha = 1.0f;
54         getWindow().setAttributes(lp);
55     }
56 
57     /**
58      * 内容区域变暗
59      */
60     protected void lightOff() {
61         WindowManager.LayoutParams lp = getWindow().getAttributes();
62         lp.alpha = .3f;
63         getWindow().setAttributes(lp);
64 
65     }

我们初始化我们的popupWindow,设置了关闭对话框的回调,已经设置了选择不同文件夹的回调;
这里仅仅是初始化,下面看我们合适将其弹出的,其实整个Activity也就一个事件,点击弹出该对话框,所以看Activity的initEvent方法:

 1 /**
 2      * 添加点击事件
 3      */
 4     private void initEvent() {
 5         mBottomLy.setOnClickListener(new OnClickListener() {
 6 
 7             public void onClick(View v) {
 8                 // 设置PopupWindow动画
 9                 mDirPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim);
10 
11                 // 设置PopupWindow的出现
12                 mDirPopupWindow.showAsDropDown(mBottomLy, 0, 0);
13                 lightOff();
14 
15             }
16         });
17 
18     }

动画的文件就不贴了,大家自己看源码;

我们改变了GridView的适配器,以及底部的控件上的文件夹名称,文件数量等等;

好了,到此结束;整篇由于篇幅原因没有贴任何布局文件,大家自己通过源码查看;

五、总结:

1. Imageloader:

(1)Handler + Loop + Message(new Thread().start():这种方式效率低

(2) 图片的压缩

    获取图片应当显示的尺寸---> 使用options进行压缩

(3) 图片显示避免错乱

           setTag(url);

2. PopupWindow:

单独自定义一个PopupWindow继承自系统的PopupWindow。

然后处理自己的子View事件,把一些关键的回调接口和方法进行返回,让MainActivity进行设置

3. 注意:

ps:请真机测试,反正我的模拟器扫描不到图片~

ps:运行出现空指针的话,在getImages中添加判断,if(parentFile.list()==null)continue , 切记~~~具体位置,上面有说; 

源码下载:

 https://github.com/PocketBoy/hebao

原文地址:https://www.cnblogs.com/hebao0514/p/4910190.html