• 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

  • 相关阅读:
    Python:遍历文件目录及子目录,并批量改变文件名称
    python:利用递归遍历文件夹所有文件(包括子文件夹里的文件)
    python文件,文件夹操作
    Ubuntu安装vim报错的的处理
    Centos7安装Chrome
    Centos7更换阿里yum源
    CentOS7设置启动默认界面方法
    Vim快捷键学习---记性不行了,留这里备查
    第7-9章作业汇总
    第四次作业---第三题陈岩岩
  • 原文地址:https://www.cnblogs.com/hebao0514/p/4910190.html
Copyright © 2020-2023  润新知