• Android之仿微信图片选择器


      先上效果图。第一张图显示的是“相机”文件夹中的所有图片;通过点击多张图片可以到第二张图所示的效果(被选择的图片会变暗,同时选择按钮变亮);点击最下面的那一栏可以到第三张图所示的效果(显示手机中所有包含图片文件的文件夹)。

       

    一、目标

    、尽可能避免内存溢出
        A、根据图片的显示大小去压缩图片
        B、使用缓存对图片进行管理(LruCache)
    、保证用户操作UI尽可能的流畅
        在getView()方法中尽量不做耗时操作(异步显示 + 回调显示)
    、用户预期显示的图片必须尽可能快的显示(图片加载策略的选择:LIFO后进先出 / FIFO先进先出),我们采用采用LIFO(后进先出的策略)

    二、思路

    1、图片的加载在Adapter的getView()方法中执行,我们根据一个图片的URL到LruCache缓存中寻找Bitmap图片,如果找到则返回图片,如果找不到,则会根据URL产生一个Task并放到TaskQueue中,同时发送一个通知提醒后台轮询线程,轮询线程从TaskQueue中取出一个Task到线程池去执行(执行的是Task的run()方法,具体为:获得图片显示的实际大小;使用Options对图片进行压缩;加载图片且放入LruCache)。我们需要在ImageLoader类中用到:LruCache缓存、线程池、任务列表TaskQueue、后台轮询线程、与轮询线程绑定的Handler和UI线程的Handler

    2、具体的实现:Handler + Looper + Message(Android异步消息处理框架)。当我们用Handler发送消息时,会把信息放到MessageQueue中,轮询线程会取出这条消息,交给Handler的handleMessage()方法进行处理

    3、后台轮询线程(Thread)不断访问任务队列(LinkList<Runnable>),如果任务队列中有加载图片的任务 (Runnable),就通过Handler发消息给线程池(ExecuterService),让线程池拿出一个子线程,然后根据调度任务的策略 (LIFO)从任务队列中取出一个任务去完成图片的获取,因为图片是异步的在子线程中获取到的,不能直接显示,所以需要通过一个UI相关的Handler把图片对象发送到UI线程中,最后完成图片的显示。

    三、代码和解释

    (一)目录结构图如下:

    (二)代码和解释(解释都在代码的注释中):

    MainActivity的布局文件activity_main.xml中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="fill_parent"
     4     android:layout_height="fill_parent"
     5     android:background="@color/cl_white">
     6 
     7     <RelativeLayout
     8         android:layout_width="match_parent"
     9         android:layout_height="50.0dip"
    10         android:background="#ee000000">
    11 
    12         <ImageView
    13             android:id="@+id/position_main_iv_icon"
    14             android:layout_width="35.0dip"
    15             android:layout_height="35.0dip"
    16             android:layout_centerVertical="true"
    17             android:layout_marginLeft="10.0dip"
    18             android:src="@mipmap/ic_launcher" />
    19 
    20         <TextView
    21             style="@style/Style_Main_TextView"
    22             android:layout_marginLeft="10.0dip"
    23             android:layout_toRightOf="@+id/position_main_iv_icon"
    24             android:text="图片选择器" />
    25     </RelativeLayout>
    26 
    27     <RelativeLayout
    28         android:id="@+id/find_main_rl_bottomlayout"
    29         android:layout_width="match_parent"
    30         android:layout_height="50.0dip"
    31         android:layout_alignParentBottom="true"
    32         android:background="#ee000000">
    33 
    34         <TextView
    35             android:id="@+id/find_main_tv_toall"
    36             style="@style/Style_Main_TextView"
    37             android:layout_marginLeft="10.0dip"
    38             android:text="所有图片" />
    39 
    40         <TextView
    41             android:id="@+id/find_main_tv_num"
    42             style="@style/Style_Main_TextView"
    43             android:layout_alignParentRight="true"
    44             android:layout_marginRight="10.0dip"
    45             android:text="共100张" />
    46     </RelativeLayout>
    47 
    48     <GridView
    49         android:id="@+id/find_main_gv_images"
    50         android:layout_width="fill_parent"
    51         android:layout_height="fill_parent"
    52         android:layout_marginBottom="50.0dip"
    53         android:layout_marginTop="50.0dip"
    54         android:cacheColorHint="@android:color/transparent"
    55         android:horizontalSpacing="3.0dip"
    56         android:listSelector="@android:color/transparent"
    57         android:numColumns="3"
    58         android:stretchMode="columnWidth"
    59         android:verticalSpacing="3.0dip" />
    60 
    61 </RelativeLayout>

    MainActivity.java中的代码:

      1 package com.itgungnir.activity;
      2 
      3 import android.app.Activity;
      4 import android.app.ProgressDialog;
      5 import android.content.ContentResolver;
      6 import android.database.Cursor;
      7 import android.net.Uri;
      8 import android.os.Bundle;
      9 import android.os.Environment;
     10 import android.os.Handler;
     11 import android.os.Message;
     12 import android.provider.MediaStore;
     13 import android.view.View;
     14 import android.view.WindowManager;
     15 import android.widget.GridView;
     16 import android.widget.PopupWindow;
     17 import android.widget.RelativeLayout;
     18 import android.widget.TextView;
     19 import android.widget.Toast;
     20 
     21 import com.itgungnir.entity.FolderModel;
     22 import com.itgungnir.tools.ImageAdapter;
     23 import com.itgungnir.view.DirsPopWindow;
     24 
     25 import java.io.File;
     26 import java.io.FilenameFilter;
     27 import java.util.ArrayList;
     28 import java.util.Arrays;
     29 import java.util.HashSet;
     30 import java.util.List;
     31 import java.util.Set;
     32 
     33 public class MainActivity extends Activity {
     34     private GridView imageGrid;
     35     private TextView dirName;
     36     private TextView dirCount;
     37     private ProgressDialog progressDialog; // 加载图片时出现的加载对话框
     38     private DirsPopWindow popWindow; // 可弹出的目录菜单
     39     private RelativeLayout bottomLayout;
     40 
     41     private List<String> imageList; // 图片的数据集
     42     private File currentDir; // 当前所在的文件目录
     43     private int totalCount; // 显示dirCount中的数据
     44     private List<FolderModel> folderModels = new ArrayList<FolderModel>();
     45     private ImageAdapter imageAdapter;
     46 
     47     private static final int DATA_LOADED = 0x110;
     48 
     49     private Handler handler = new Handler() {
     50         @Override
     51         public void handleMessage(Message msg) {
     52             if (msg.what == DATA_LOADED) {
     53                 progressDialog.dismiss();
     54                 bindDataToView();
     55 
     56                 initDirsPopWindow();
     57             }
     58         }
     59     };
     60 
     61     @Override
     62     protected void onCreate(Bundle savedInstanceState) {
     63         super.onCreate(savedInstanceState);
     64         setContentView(R.layout.activity_main);
     65         initView();
     66         initData();
     67         initEvent();
     68     }
     69 
     70     /**
     71      * 初始化弹出菜单
     72      */
     73     private void initDirsPopWindow() {
     74         popWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
     75             @Override
     76             public void onDismiss() {
     77                 lightOn();
     78             }
     79         });
     80     }
     81 
     82     /**
     83      * 弹出窗口消失之后,需要将后面的图片列表变亮
     84      */
     85     private void lightOn() {
     86         WindowManager.LayoutParams lp = getWindow().getAttributes();
     87         lp.alpha = 1.0f;
     88         getWindow().setAttributes(lp);
     89     }
     90 
     91     /**
     92      * 在现实PopUpWindow之后将后面的图片列表置黑
     93      */
     94     private void lightOff() {
     95         WindowManager.LayoutParams lp = getWindow().getAttributes();
     96         lp.alpha = 0.3f;
     97         getWindow().setAttributes(lp);
     98     }
     99 
    100     /**
    101      * 绑定数据到View中
    102      */
    103     private void bindDataToView() {
    104         if (currentDir == null) {
    105             Toast.makeText(MainActivity.this, "未扫描到任何图片!", Toast.LENGTH_SHORT).show();
    106             return;
    107         }
    108         imageList = Arrays.asList(currentDir.list());
    109         imageAdapter = new ImageAdapter(MainActivity.this, imageList, currentDir.getAbsolutePath());
    110         imageGrid.setAdapter(imageAdapter);
    111 
    112         dirCount.setText(totalCount + "");
    113         dirName.setText(currentDir.getName());
    114     }
    115 
    116     /**
    117      * 初始化数据(利用ContentProvider扫描手机中的所有图片)
    118      */
    119     private void initData() {
    120         if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    121             Toast.makeText(MainActivity.this, "当前存储卡不可用!", Toast.LENGTH_SHORT).show();
    122             return;
    123         }
    124         progressDialog = ProgressDialog.show(MainActivity.this, null, "正在加载...");
    125         new Thread() {
    126             @Override
    127             public void run() {
    128                 /**
    129                  * 初始化FolderModel,为PopUpWindow做准备
    130                  */
    131                 Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    132                 ContentResolver cr = MainActivity.this.getContentResolver();
    133                 Cursor cursor = cr.query(imageUri, null, MediaStore.Images.Media.MIME_TYPE + " = ? or " + MediaStore.Images.Media.MIME_TYPE + " = ? ",
    134                         new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED);
    135                 Set<String> dirPaths = new HashSet<String>();
    136                 while (cursor.moveToNext()) {
    137                     String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); // 获取图片的路径
    138                     File parentFile = new File(path).getParentFile(); // 获取该图片所在的父路径名
    139                     if (parentFile == null) {
    140                         continue;
    141                     }
    142                     String dirPath = parentFile.getAbsolutePath();
    143                     FolderModel folder = null;
    144                     // 放置重复扫描
    145                     if (dirPaths.contains(dirPath)) {
    146                         continue;
    147                     } else {
    148                         dirPaths.add(dirPath);
    149                         folder = new FolderModel();
    150                         folder.setDir(dirPath);
    151                         folder.setFirstImgPath(path);
    152                     }
    153                     if (parentFile.list() == null) {
    154                         continue;
    155                     }
    156                     int picSize = parentFile.list(new FilenameFilter() {
    157                         @Override
    158                         public boolean accept(File dir, String filename) {
    159                             if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png")) {
    160                                 return true;
    161                             }
    162                             return false;
    163                         }
    164                     }).length;
    165                     folder.setCount(picSize);
    166                     folderModels.add(folder);
    167                     if (picSize > totalCount) {
    168                         totalCount = picSize;
    169                         currentDir = parentFile;
    170                     }
    171                 }
    172                 cursor.close();
    173                 // 扫描完成,释放临时变量的内存
    174                 dirPaths = null;
    175                 handler.sendEmptyMessage(DATA_LOADED); // 通知Handler扫描图片完成
    176             }
    177         }.start();
    178     }
    179 
    180     /**
    181      * 初始化事件
    182      */
    183     private void initEvent() {
    184         bottomLayout.setOnClickListener(new View.OnClickListener() {
    185             @Override
    186             public void onClick(View v) {
    187                 popWindow.setAnimationStyle(R.style.Style_PopWindow_Anim);
    188                 popWindow.showAsDropDown(bottomLayout, 0, 0);
    189                 lightOff();
    190             }
    191         });
    192 
    193         popWindow.setOnDirSelectListener(new DirsPopWindow.OnDirSelectListener() {
    194             @Override
    195             public void onDirSelected(FolderModel folder) {
    196                 currentDir = new File(folder.getDir());
    197                 imageList = Arrays.asList(currentDir.list(new FilenameFilter() {
    198                     @Override
    199                     public boolean accept(File dir, String filename) {
    200                         if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png")) {
    201                             return true;
    202                         }
    203                         return false;
    204                     }
    205                 }));
    206                 imageAdapter = new ImageAdapter(MainActivity.this, imageList, currentDir.getAbsolutePath());
    207                 imageGrid.setAdapter(imageAdapter);
    208                 dirCount.setText(imageList.size() + "");
    209                 dirName.setText(folder.getName());
    210 
    211                 popWindow.dismiss();
    212             }
    213         });
    214     }
    215 
    216     /**
    217      * 初始化控件
    218      */
    219     private void initView() {
    220         imageGrid = (GridView) findViewById(R.id.find_main_gv_images);
    221         dirName = (TextView) findViewById(R.id.find_main_tv_toall);
    222         dirCount = (TextView) findViewById(R.id.find_main_tv_num);
    223         bottomLayout = (RelativeLayout) findViewById(R.id.find_main_rl_bottomlayout);
    224         popWindow = new DirsPopWindow(MainActivity.this, folderModels);
    225     }
    226 
    227     @Override
    228     protected void onDestroy() {
    229         progressDialog.dismiss();
    230         super.onDestroy();
    231     }
    232 }

    图片压缩加载类ImageLoader.java中的代码:

      1 package com.itgungnir.tools;
      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.graphics.Bitmap;
     10 import android.graphics.BitmapFactory;
     11 import android.os.Handler;
     12 import android.os.Looper;
     13 import android.os.Message;
     14 import android.support.v4.util.LruCache;
     15 import android.util.DisplayMetrics;
     16 import android.util.Log;
     17 import android.view.ViewGroup.LayoutParams;
     18 import android.widget.ImageView;
     19 
     20 /**
     21  * ********图片加载类*********
     22  * ***思路:图片的加载在Adapter的getView()方法中执行,我们根据一个图片的URL到LruCache缓存中寻找Bitmap图片,如果找到则返回图片,
     23  * 如果找不到,则会根据URL产生一个Task并放到TaskQueue中,同时发送一个通知提醒后台轮询线程,轮询线程从TaskQueue中取出一个Task到线程池去执行(执行的是Task的run()方法,
     24  * 具体为:获得图片显示的实际大小;使用Options对图片进行压缩;加载图片且放入LruCache)
     25  * ***核心:Handler + Looper + Message(Android异步消息处理框架)
     26  * 当我们用Handler发送消息时,会把信息放到MessageQueue中,轮询线程会取出这条消息,交给Handler的handleMessage()方法进行处理
     27  */
     28 public class ImageLoader {
     29     private static ImageLoader mInstance; // 实例
     30     private LruCache<String, Bitmap> mLruCache; // 图片缓存的核心类
     31     private ExecutorService mThreadPool; // 线程池
     32     private static final int DEFAULT_THREAD_COUNT = 1; // 线程池的线程数量,默认为1
     33     private Type mType = Type.LIFO; // 任务队列的默认调度方式
     34     private LinkedList<Runnable> taskQueue; // 任务队列
     35     private Thread mPoolThread; // 后台轮询线程
     36     private Handler mPoolThreadHandler; // 与后台轮询线程绑定的Handler
     37     private Handler uiHandler; // 运行在UI线程的handler,用于给ImageView设置图片
     38     /**
     39      * 引入一个值为1的信号量,防止mPoolThreadHander未初始化完成
     40      * Semaphore的作用是限制某一资源的同步访问
     41      * 可以把Semaphore理解成一个可以容纳N个人的房间,如果人数没有达到N就可以进去,如果人满了,就要等待有人出来才可以进去
     42      * 在addTask()方法中需要用到后台轮询线程poolThreadHandler,但存在线程同步问题,
     43      * 即addTask()方法可能在poolThreadHandler初始化之前就被调用了,所以我们需要定义这样一个“信号量”来调控线程同步
     44      */
     45     private volatile Semaphore mPoolThreadHandlerSemaphore = new Semaphore(0); // 控制addTask()方法在mPoolThreadHandler吃实话之后才能调用
     46     private volatile Semaphore mPoolSemaphore; // 引入一个值为1的信号量,由于线程池内部也有一个阻塞线程,防止加入任务的速度过快,使LIFO效果不明显
     47 
     48     /**
     49      * 图片加载的策略(FIFO先进先出、LIFO后进先出)
     50      */
     51     public enum Type {
     52         FIFO, LIFO
     53     }
     54 
     55     /**
     56      * 构造函数
     57      * 由于ImageLoader中需要使用LruCache来缓存图片,需要占据较大的空间,所以整个项目中只需要一个ImageLoader即可(需要使用单例模式)
     58      * 因此我们把构造方法设为private权限,防止用户new出实例
     59      *
     60      * @param threadCount 任务队列中默认线程数
     61      * @param type        图片加载策略(先进先出/后进先出)
     62      */
     63     private ImageLoader(int threadCount, Type type) {
     64         init(threadCount, type);
     65     }
     66 
     67     /**
     68      * 单例获得该实例对象(无参数,按照默认值进行初始化)
     69      */
     70     public static ImageLoader getInstance() {
     71         if (mInstance == null) {
     72             synchronized (ImageLoader.class) {
     73                 if (mInstance == null) {
     74                     mInstance = new ImageLoader(DEFAULT_THREAD_COUNT, Type.LIFO);
     75                 }
     76             }
     77         }
     78         return mInstance;
     79     }
     80 
     81     /**
     82      * 单例获得该实例对象(有参数,用户可以根据实际需要对ImageLoader进行实例化)
     83      */
     84     public static ImageLoader getInstance(int threadCount, Type type) {
     85         if (mInstance == null) {
     86             synchronized (ImageLoader.class) {
     87                 if (mInstance == null) {
     88                     mInstance = new ImageLoader(threadCount, type);
     89                 }
     90             }
     91         }
     92         return mInstance;
     93     }
     94 
     95     /**
     96      * 完成成员变量的初始化操作
     97      */
     98     private void init(int threadCount, Type type) {
     99         /**
    100          * 初始化后台轮询线程(使用Looper、Handler、Message实现)
    101          */
    102         mPoolThread = new Thread() {
    103             @Override
    104             public void run() {
    105                 Looper.prepare();
    106                 mPoolThreadHandler = new Handler() {
    107                     @Override
    108                     public void handleMessage(Message msg) {
    109                         mThreadPool.execute(getTask()); // 线程池取出一个任务去执行
    110                         try {
    111                             mPoolSemaphore.acquire();
    112                         } catch (InterruptedException e) {
    113                         }
    114                     }
    115                 };
    116                 mPoolThreadHandlerSemaphore.release(); // 释放一个信号量
    117                 Looper.loop(); // 开始无限循环
    118             }
    119         };
    120         mPoolThread.start();
    121         /**
    122          * 初始化LruCache
    123          */
    124         int maxMemory = (int) Runtime.getRuntime().maxMemory(); // 获取应用程序最大可用内存
    125         int cacheSize = maxMemory / 8; // 设置缓存的存储空间是总空间的1/8
    126         mLruCache = new LruCache<String, Bitmap>(cacheSize) {
    127             @Override
    128             protected int sizeOf(String key, Bitmap value) {
    129                 return value.getRowBytes() * value.getHeight(); // getRowBytes()获取到图片每行有多少字节,乘以图片的高度就是图片占据的内存
    130             }
    131         };
    132         /**
    133          * 初始化其他
    134          */
    135         mThreadPool = Executors.newFixedThreadPool(threadCount); // 初始化线程池threadPool
    136         mPoolSemaphore = new Semaphore(threadCount); // 初始化线程池(消息队列)信号量
    137         taskQueue = new LinkedList<Runnable>(); // 初始化任务队列taskQueue
    138         mType = type == null ? Type.LIFO : type; // 同步Type
    139     }
    140 
    141     /**
    142      * 根据路径path找到对应的图片并异步加载到主界面中
    143      *
    144      * @param path      图片的路径
    145      * @param imageView 目标的ImageView(图片将要被加载到的ImageView)
    146      */
    147     public void loadImage(final String path, final ImageView imageView) {
    148         imageView.setTag(path); // 将PATH设置为imageView的TAG,方便在主线程中对比、设置图片
    149         // UI线程
    150         if (uiHandler == null) {
    151             uiHandler = new Handler() {
    152                 @Override
    153                 public void handleMessage(Message msg) {
    154                     ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
    155                     ImageView imageView = holder.imageView;
    156                     Bitmap bm = holder.bitmap;
    157                     String path = holder.path;
    158                     // 将path与imageView的tag进行比较,如果相同,则为imageView设置图片
    159                     if (imageView.getTag().toString().equals(path)) {
    160                         imageView.setImageBitmap(bm);
    161                     }
    162                 }
    163             };
    164         }
    165 
    166         Bitmap bm = getBitmapFromLruCache(path); // 根据路径Path在缓存中获取Bitmap
    167         if (bm != null) { // 如果这张图片存在于缓存中,则通知UI线程更新图片
    168             ImgBeanHolder holder = new ImgBeanHolder();
    169             holder.bitmap = bm;
    170             holder.imageView = imageView;
    171             holder.path = path;
    172             Message message = Message.obtain();
    173             message.obj = holder;
    174             uiHandler.sendMessage(message);
    175         } else { // 如果这张图片没有存在于缓存中,则添加一个新的任务到任务队列中
    176             addTask(new Runnable() { // 添加一个任务到任务队列中
    177                 @Override
    178                 public void run() {
    179                     /**
    180                      * 加载图片
    181                      */
    182                     // 压缩图片:1、获取图片需要显示的大小
    183                     ImageSize imageSize = getImageViewSize(imageView);
    184                     // 压缩图片:2、压缩图片
    185                     int reqWidth = imageSize.width;
    186                     int reqHeight = imageSize.height;
    187                     Bitmap bm = decodeSampledBitmapFromResource(path, reqWidth, reqHeight);
    188                     // 压缩图片:3、将图片加入到缓存
    189                     addBitmapToLruCache(path, bm);
    190                     // 将上面操作获取到的数据封装到ImgBeanHolder实体对象中,通知UI线程处理
    191                     ImgBeanHolder holder = new ImgBeanHolder();
    192                     holder.bitmap = getBitmapFromLruCache(path);
    193                     holder.imageView = imageView;
    194                     holder.path = path;
    195                     Message message = Message.obtain();
    196                     message.obj = holder;
    197                     uiHandler.sendMessage(message);
    198                     mPoolSemaphore.release(); // 为线程池释放一个信号量
    199                 }
    200             });
    201         }
    202     }
    203 
    204     /**
    205      * 添加一个任务到任务队列
    206      * synchronized是为了避免多个线程同时到达这个方法中申请信号量而发生死锁
    207      */
    208     private synchronized void addTask(Runnable runnable) {
    209         /**
    210          * 请求资源信号量,避免死锁
    211          * 如果还没有对mPoolThreadHandler进行初始化,则默认的房间里可以容纳0个人,所以如果此时addTask()方法请求资源会被阻塞
    212          * 通过这个信号量控制addTask()方法必须在mPoolThreadHandler初始化之后才调用
    213          */
    214         try {
    215             if (mPoolThreadHandler == null)
    216                 mPoolThreadHandlerSemaphore.acquire(); // 如果当前mPoolThreadHandler还没有初始化,则该线程阻塞(等待),直到mPoolThreadHandler初始化
    217         } catch (InterruptedException e) {
    218             e.printStackTrace();
    219         }
    220         taskQueue.add(runnable); // 添加Runnable任务到任务队列
    221         mPoolThreadHandler.sendEmptyMessage(0x110); // 发送一个通知,通知后台轮询线程,0x110是一个随意的值
    222     }
    223 
    224     /**
    225      * 从任务队列中取出一个任务
    226      * 根据为ImageLoader实例设定的图片加载策略决定是取出最后一个还是取出第一个
    227      */
    228     private synchronized Runnable getTask() {
    229         if (mType == Type.FIFO) {
    230             return taskQueue.removeFirst();
    231         } else if (mType == Type.LIFO) {
    232             return taskQueue.removeLast();
    233         }
    234         return null;
    235     }
    236 
    237     /**
    238      * 根据ImageView获得适当的压缩的目标宽和高
    239      */
    240     private ImageSize getImageViewSize(ImageView imageView) {
    241         /**
    242          * 获取ImageView的LayoutParams
    243          */
    244         final DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics();
    245         final LayoutParams lp = imageView.getLayoutParams();
    246         /**
    247          * 定义ImageView显示的宽度
    248          */
    249         int width = lp.width == LayoutParams.WRAP_CONTENT ? 0 : imageView.getWidth(); // 获取ImageView的实际宽度
    250         if (width <= 0) // ImageView可能是刚new出来就来执行这个方法,所以还没有宽高值,只能通过在layout中声明的宽高值来赋值
    251             width = lp.width; // 获取ImageView在layout中声明的宽度
    252         if (width <= 0) // 在layout中设置的宽高值是WRAP_CONTENT或MATCH_PARENT,则还是取出0,这时我们就需要看ImageView有没有设置max值
    253             width = getImageViewFieldValue(imageView, "mMaxWidth"); // 通过反射获取ImageView的宽度最大值
    254         if (width <= 0) // 可能ImageView没有设置max值,因此我们只能设置ImageView的宽或高为屏幕的宽或高
    255             width = metrics.widthPixels;
    256         /**
    257          * 定义ImageView显示的高度(同宽度)
    258          */
    259         int height = lp.height == LayoutParams.WRAP_CONTENT ? 0 : imageView.getHeight();
    260         if (height <= 0) height = lp.height;
    261         if (height <= 0) height = getImageViewFieldValue(imageView, "mMaxHeight");
    262         if (height <= 0) height = metrics.heightPixels;
    263         /**
    264          * 将ImageView压缩后的宽和高封装到ImageSize实体类中返回
    265          */
    266         ImageSize imageSize = new ImageSize();
    267         imageSize.width = width;
    268         imageSize.height = height;
    269         return imageSize;
    270     }
    271 
    272     /**
    273      * 从LruCache中获取一张图片,如果不存在就返回null
    274      */
    275     private Bitmap getBitmapFromLruCache(String key) {
    276         return mLruCache.get(key);
    277     }
    278 
    279     /**
    280      * 将图片添加到LruCache缓存
    281      *
    282      * @param path   路径,先要判断图片是否已经在缓存中
    283      * @param bitmap 图片
    284      */
    285     private void addBitmapToLruCache(String path, Bitmap bitmap) {
    286         if (getBitmapFromLruCache(path) == null) {
    287             if (bitmap != null)
    288                 mLruCache.put(path, bitmap);
    289         }
    290     }
    291 
    292     /**
    293      * 根据图片需求的宽高和图片实际的宽高计算inSampleSize(图片压缩的比例,用于压缩图片)
    294      *
    295      * @param options   图片实际的宽和高
    296      * @param reqWidth  图片需要的宽度
    297      * @param reqHeight 图片需要的高度
    298      */
    299     private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    300         int width = options.outWidth; // 原图片的宽度
    301         int height = options.outHeight; // 原图片的高度
    302         int inSampleSize = 1; // 缩放的比例
    303         if (width > reqWidth && height > reqHeight) {
    304             int widthRatio = Math.round((float) width / (float) reqWidth); // 计算出实际宽度和目标宽度的比率
    305             int heightRatio = Math.round((float) width / (float) reqWidth);
    306             inSampleSize = Math.max(widthRatio, heightRatio); // 取宽度缩放值和高度缩放值的较大值作为图片缩放比例
    307         }
    308         return inSampleSize;
    309     }
    310 
    311     /**
    312      * 根据图片需要显示的宽和高对图片进行压缩
    313      *
    314      * @param path      图片的路径
    315      * @param reqWidth  图片显示的宽度
    316      * @param reqHeight 图片显示的高度
    317      */
    318     private Bitmap decodeSampledBitmapFromResource(String path, int reqWidth, int reqHeight) {
    319         final BitmapFactory.Options options = new BitmapFactory.Options();
    320         options.inJustDecodeBounds = true; // 如果该值设为true那么将不返回实际的bitmap,也不给其分配内存空间(避免内存溢出),但允许我们查询图片信息(包括图片大小信息)
    321         BitmapFactory.decodeFile(path, options); // 经过这行代码,options就获得了图片实际的宽和高
    322         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 调用上面定义的方法计算inSampleSize值(图片压缩的比例)
    323         options.inJustDecodeBounds = false; // 设置可以取出图片
    324         Bitmap bitmap = BitmapFactory.decodeFile(path, options); // 使用获取到的inSampleSize值再次解析图片,取出Bitmap
    325         return bitmap;
    326     }
    327 
    328     /**
    329      * 存放图片的Bitmap源、ImageView和图片路径的实体类
    330      */
    331     private class ImgBeanHolder {
    332         Bitmap bitmap;
    333         ImageView imageView;
    334         String path;
    335     }
    336 
    337     /**
    338      * 存放ImageView显示的宽高的实体类
    339      */
    340     private class ImageSize {
    341         int width; // ImageView显示的宽度
    342         int height; // ImageView显示的高度
    343     }
    344 
    345     /**
    346      * 通过反射获取某个Object的fieldName属性对应的值(本程序中是通过反射获得ImageView设置的最大宽度和高度)
    347      */
    348     private static int getImageViewFieldValue(Object object, String fieldName) {
    349         int value = 0;
    350         try {
    351             Field field = ImageView.class.getDeclaredField(fieldName);
    352             field.setAccessible(true);
    353             int fieldValue = (Integer) field.get(object);
    354             if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
    355                 value = fieldValue;
    356             }
    357         } catch (Exception e) {
    358             e.printStackTrace();
    359         }
    360         return value;
    361     }
    362 }

    图片文件夹实体类FolderModel.java中的代码:

     1 package com.itgungnir.entity;
     2 
     3 /**
     4  * PopUpWindow对应的Bean类
     5  */
     6 public class FolderModel {
     7     private String dir; // 图片的文件夹的路径
     8     private String firstImgPath; // 第一张图片的路径
     9     private String name; // 文件夹的名称
    10     private int count; // 文件夹中图片的数量
    11 
    12     public String getFirstImgPath() {
    13         return firstImgPath;
    14     }
    15 
    16     public void setFirstImgPath(String firstImgPath) {
    17         this.firstImgPath = firstImgPath;
    18     }
    19 
    20     public String getName() {
    21         return name;
    22     }
    23 
    24     public int getCount() {
    25         return count;
    26     }
    27 
    28     public void setCount(int count) {
    29         this.count = count;
    30     }
    31 
    32     public String getDir() {
    33         return dir;
    34     }
    35 
    36     public void setDir(String dir) {
    37         this.dir = dir;
    38         int lastIndex = this.dir.lastIndexOf("/");
    39         this.name = this.dir.substring(lastIndex);
    40     }
    41 }

    主界面GridView的适配器类ImageAdapter.java中的代码:

     1 package com.itgungnir.tools;
     2 
     3 import android.content.Context;
     4 import android.graphics.Color;
     5 import android.view.LayoutInflater;
     6 import android.view.View;
     7 import android.view.ViewGroup;
     8 import android.widget.BaseAdapter;
     9 import android.widget.ImageButton;
    10 import android.widget.ImageView;
    11 
    12 import com.itgungnir.activity.R;
    13 
    14 import java.util.HashSet;
    15 import java.util.List;
    16 import java.util.Set;
    17 
    18 /**
    19  * 主界面MainActivity中的GridView的适配器
    20  */
    21 public class ImageAdapter extends BaseAdapter {
    22     private static final Set<String> selectedImages = new HashSet<String>(); // 存储选中的图片的路径
    23 
    24     private String dirPath; // GridView中图片的父路径
    25     private List<String> imagePaths; // GridView中显示的图片的路径(s)
    26     private LayoutInflater inflater;
    27 
    28     public ImageAdapter(Context context, List<String> data, String dirPath) {
    29         this.dirPath = dirPath;
    30         this.imagePaths = data;
    31         inflater = LayoutInflater.from(context);
    32     }
    33 
    34     @Override
    35     public int getCount() {
    36         return imagePaths.size();
    37     }
    38 
    39     @Override
    40     public Object getItem(int position) {
    41         return imagePaths.get(position);
    42     }
    43 
    44     @Override
    45     public long getItemId(int position) {
    46         return position;
    47     }
    48 
    49     @Override
    50     public View getView(final int position, View convertView, ViewGroup parent) {
    51         final ViewHolder viewHolder;
    52         if (convertView == null) {
    53             convertView = inflater.inflate(R.layout.sideworks_griditem, parent, false);
    54             viewHolder = new ViewHolder();
    55             viewHolder.imageView = (ImageView) convertView.findViewById(R.id.find_griditem_iv_image);
    56             viewHolder.checkBtn = (ImageButton) convertView.findViewById(R.id.find_griditem_ib_check);
    57             convertView.setTag(viewHolder);
    58         } else {
    59             viewHolder = (ViewHolder) convertView.getTag();
    60         }
    61         // 重置状态
    62         viewHolder.imageView.setImageResource(R.mipmap.img_pictures_notfound);
    63         viewHolder.checkBtn.setImageResource(R.mipmap.img_picture_unselected);
    64         viewHolder.imageView.setColorFilter(null);
    65         // 加载图片
    66         ImageLoader.getInstance(3, ImageLoader.Type.LIFO).loadImage(dirPath + "/" + imagePaths.get(position), viewHolder.imageView);
    67 
    68         /**
    69          * 为GridView的Item设置点击事件
    70          */
    71         viewHolder.imageView.setOnClickListener(new View.OnClickListener() {
    72             @Override
    73             public void onClick(View v) {
    74                 String imagePath = dirPath + "/" + imagePaths.get(position);
    75                 if (selectedImages.contains(imagePath)) { // 如果图片已经被选择,则清楚选择状态
    76                     selectedImages.remove(imagePath);
    77                     viewHolder.imageView.setColorFilter(null);
    78                     viewHolder.checkBtn.setImageResource(R.mipmap.img_picture_unselected);
    79                 } else { // 如果图片没有被选择,则选择图片
    80                     selectedImages.add(imagePath);
    81                     viewHolder.imageView.setColorFilter(Color.parseColor("#77000000")); // 设置图片上面覆盖一层透明的黑色蒙版
    82                     viewHolder.checkBtn.setImageResource(R.mipmap.img_pictures_selected); // 设置多选按钮为选中
    83                 }
    84             }
    85         });
    86 
    87         return convertView;
    88     }
    89 
    90     private class ViewHolder {
    91         ImageView imageView;
    92         ImageButton checkBtn;
    93     }
    94 }

    主界面GridView的子View布局sideworks_griditem.xml文件中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent">
     5 
     6     <ImageView
     7         android:id="@+id/find_griditem_iv_image"
     8         android:layout_width="match_parent"
     9         android:layout_height="100.0dip"
    10         android:scaleType="centerCrop"
    11         android:src="@mipmap/img_pictures_notfound" />
    12 
    13     <ImageButton
    14         android:id="@+id/find_griditem_ib_check"
    15         android:layout_width="wrap_content"
    16         android:layout_height="wrap_content"
    17         android:layout_alignParentRight="true"
    18         android:layout_marginRight="3.0dip"
    19         android:layout_marginTop="3.0dip"
    20         android:background="@null"
    21         android:clickable="false"
    22         android:src="@mipmap/img_picture_unselected" />
    23 
    24 </RelativeLayout>

    弹出窗口DirsPopWindow.java中的代码:

      1 package com.itgungnir.view;
      2 
      3 import android.content.Context;
      4 import android.graphics.drawable.BitmapDrawable;
      5 import android.util.DisplayMetrics;
      6 import android.view.LayoutInflater;
      7 import android.view.MotionEvent;
      8 import android.view.View;
      9 import android.view.ViewGroup;
     10 import android.view.WindowManager;
     11 import android.widget.AdapterView;
     12 import android.widget.ArrayAdapter;
     13 import android.widget.ImageView;
     14 import android.widget.ListView;
     15 import android.widget.PopupWindow;
     16 import android.widget.TextView;
     17 
     18 import com.itgungnir.activity.R;
     19 import com.itgungnir.entity.FolderModel;
     20 import com.itgungnir.tools.ImageLoader;
     21 
     22 import java.util.List;
     23 
     24 public class DirsPopWindow extends PopupWindow {
     25     private int width; // PopUpWindow的宽度
     26     private int height; // PopUpWindow的高度
     27     private View convertView; // 代表PopUpWindow
     28     private ListView dirList; // 显示目录信息的ListView
     29     private List<FolderModel> datas; // ListView中显示的信息的列表
     30 
     31     public OnDirSelectListener listener;
     32 
     33     public interface OnDirSelectListener {
     34         void onDirSelected(FolderModel folder);
     35     }
     36 
     37     public void setOnDirSelectListener(OnDirSelectListener listener) {
     38         this.listener = listener;
     39     }
     40 
     41     public DirsPopWindow(Context context, List<FolderModel> datas) {
     42         calculateWidthAndHeight(context);
     43         convertView = LayoutInflater.from(context).inflate(R.layout.sideworks_popwindow, null);
     44         this.datas = datas;
     45         setContentView(convertView);
     46         setWidth(this.width);
     47         setHeight(this.height);
     48         setFocusable(true);
     49         setTouchable(true);
     50         setOutsideTouchable(true); // 外面可以点击
     51         setBackgroundDrawable(new BitmapDrawable()); // 点击外面的区域会使PopUpWindow消失
     52         // 点击外部的事件(让PopUpWindow消失)
     53         setTouchInterceptor(new View.OnTouchListener() {
     54             @Override
     55             public boolean onTouch(View v, MotionEvent event) {
     56                 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
     57                     dismiss();
     58                     return true;
     59                 }
     60                 return false;
     61             }
     62         });
     63 
     64         initView(context);
     65         initEvent();
     66     }
     67 
     68     private void initView(Context context) {
     69         dirList = (ListView) convertView.findViewById(R.id.find_popwindow_lv_dirs);
     70         dirList.setAdapter(new DirAdapter(context, datas));
     71     }
     72 
     73     private void initEvent() {
     74         dirList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
     75             @Override
     76             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
     77                 if (listener != null) {
     78                     listener.onDirSelected(datas.get(position));
     79                 }
     80             }
     81         });
     82     }
     83 
     84     /**
     85      * 计算PopUpWindow的宽度和高度
     86      */
     87     private void calculateWidthAndHeight(Context context) {
     88         // 获取屏幕的宽和高
     89         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
     90         DisplayMetrics metrics = new DisplayMetrics();
     91         wm.getDefaultDisplay().getMetrics(metrics);
     92         // 为PopUpWindow设置宽和高
     93         this.width = metrics.widthPixels;
     94         this.height = (int) (metrics.heightPixels * 0.7);
     95     }
     96 
     97     /**
     98      * PopUpWindow中ListView的适配器
     99      */
    100     private class DirAdapter extends ArrayAdapter<FolderModel> {
    101         private LayoutInflater inflater;
    102         private List<FolderModel> datas;
    103 
    104         public DirAdapter(Context context, List<FolderModel> objects) {
    105             super(context, 0, objects);
    106             inflater = LayoutInflater.from(context);
    107         }
    108 
    109         @Override
    110         public View getView(int position, View convertView, ViewGroup parent) {
    111             ViewHolder viewHolder = null;
    112             if (convertView == null) {
    113                 viewHolder = new ViewHolder();
    114                 convertView = inflater.inflate(R.layout.sideworks_popitem, parent, false);
    115                 viewHolder.image = (ImageView) convertView.findViewById(R.id.find_popitem_iv_image);
    116                 viewHolder.dirName = (TextView) convertView.findViewById(R.id.find_popitem_tv_dir);
    117                 viewHolder.imgCount = (TextView) convertView.findViewById(R.id.find_popitem_tv_imgcount);
    118                 convertView.setTag(viewHolder);
    119             } else {
    120                 viewHolder = (ViewHolder) convertView.getTag();
    121             }
    122 
    123             FolderModel folder = getItem(position);
    124             // 重置
    125             viewHolder.image.setImageResource(R.mipmap.img_pictures_notfound);
    126             // 回调加载
    127             ImageLoader.getInstance(3, ImageLoader.Type.LIFO).loadImage(folder.getFirstImgPath(), viewHolder.image);
    128             viewHolder.dirName.setText(folder.getName());
    129             viewHolder.imgCount.setText(folder.getCount() + "");
    130 
    131             return convertView;
    132         }
    133 
    134         private class ViewHolder {
    135             ImageView image;
    136             TextView dirName;
    137             TextView imgCount;
    138         }
    139     }
    140 }

    弹出窗口的布局文件sideworks_popwindow.xml中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5     android:orientation="vertical">
     6 
     7     <ListView
     8         android:id="@+id/find_popwindow_lv_dirs"
     9         android:layout_width="match_parent"
    10         android:layout_height="match_parent"
    11         android:background="@color/cl_white"
    12         android:cacheColorHint="@color/cl_transparent"
    13         android:divider="#EEE3D9"
    14         android:dividerHeight="1.0dip" />
    15 
    16 </LinearLayout>

    弹出窗口中ListView的子View的布局sideworks_popitem.xml文件中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="match_parent"
     4     android:layout_height="120.0dip"
     5     android:background="@color/cl_white"
     6     android:padding="10.0dip">
     7 
     8     <ImageView
     9         android:id="@+id/find_popitem_iv_image"
    10         android:layout_width="100.0dip"
    11         android:layout_height="100.0dip"
    12         android:layout_centerVertical="true"
    13         android:background="@mipmap/img_pic_dir_bg"
    14         android:contentDescription="@string/app_name"
    15         android:paddingBottom="17.0dip"
    16         android:paddingLeft="12.0dip"
    17         android:paddingRight="12.0dip"
    18         android:paddingTop="9.0dip"
    19         android:scaleType="fitXY" />
    20 
    21     <LinearLayout
    22         android:layout_width="wrap_content"
    23         android:layout_height="wrap_content"
    24         android:layout_centerVertical="true"
    25         android:layout_marginLeft="20.0dip"
    26         android:layout_toRightOf="@+id/find_popitem_iv_image"
    27         android:orientation="vertical">
    28 
    29         <TextView
    30             android:id="@+id/find_popitem_tv_dir"
    31             android:layout_width="wrap_content"
    32             android:layout_height="wrap_content"
    33             android:textColor="@color/cl_black"
    34             android:textSize="16.0sp" />
    35 
    36         <TextView
    37             android:id="@+id/find_popitem_tv_imgcount"
    38             android:layout_width="wrap_content"
    39             android:layout_height="wrap_content"
    40             android:textColor="@android:color/darker_gray"
    41             android:textSize="14.0sp" />
    42     </LinearLayout>
    43 
    44     <ImageView
    45         android:layout_width="wrap_content"
    46         android:layout_height="wrap_content"
    47         android:layout_alignParentRight="true"
    48         android:layout_centerVertical="true"
    49         android:src="@mipmap/img_dir_choosen" />
    50 
    51 </RelativeLayout>

    弹出窗口弹出的动画slide_up.xml文件中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <set xmlns:android="http://schemas.android.com/apk/res/android">
     3 
     4     <translate
     5         android:duration="200"
     6         android:fromXDelta="0"
     7         android:fromYDelta="100%"
     8         android:toXDelta="0"
     9         android:toYDelta="0" />
    10 
    11 </set>

    弹出窗口隐藏的动画slide_down.xml文件中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <set xmlns:android="http://schemas.android.com/apk/res/android">
     3 
     4     <translate
     5         android:duration="200"
     6         android:fromXDelta="0"
     7         android:fromYDelta="0"
     8         android:toXDelta="0"
     9         android:toYDelta="100%" />
    10 
    11 </set>

    样式文件styles.xml中的代码:

     1 <resources>
     2 
     3     <!-- Base application theme. -->
     4     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
     5         <!-- Customize your theme here. -->
     6         <item name="colorPrimary">@color/colorPrimary</item>
     7         <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
     8         <item name="colorAccent">@color/colorAccent</item>
     9     </style>
    10 
    11     <style name="Style_Main_TextView">
    12         <item name="android:layout_width">wrap_content</item>
    13         <item name="android:layout_height">wrap_content</item>
    14         <item name="android:layout_centerVertical">true</item>
    15         <item name="android:textColor">@android:color/white</item>
    16         <item name="android:textSize">18.0sp</item>
    17         <item name="android:textStyle">bold</item>
    18     </style>
    19 
    20     <style name="Style_PopWindow_Anim">
    21         <item name="android:windowEnterAnimation">@anim/slide_up</item>
    22         <item name="android:windowExitAnimation">@anim/slide_down</item>
    23     </style>
    24 
    25 </resources>

    清单文件AndroidMenifest.xml文件中的代码:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     3     package="com.itgungnir.activity">
     4 
     5     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     6     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     7 
     8     <application
     9         android:allowBackup="true"
    10         android:icon="@mipmap/ic_launcher"
    11         android:label="@string/app_name"
    12         android:supportsRtl="true"
    13         android:theme="@android:style/Theme.NoTitleBar">
    14         <activity android:name=".MainActivity">
    15             <intent-filter>
    16                 <action android:name="android.intent.action.MAIN" />
    17 
    18                 <category android:name="android.intent.category.LAUNCHER" />
    19             </intent-filter>
    20         </activity>
    21     </application>
    22 
    23 </manifest>
  • 相关阅读:
    Xcode 增强开发效率的插件
    svn 常用指令
    初识JFinal
    企业竞争案例分析
    可行性研究5
    可行性研究习题4
    可行性研究课后题
    关于阅读软件工程和计算机科学技术区别的文章来谈谈自己看法
    关于中文编程是解决中国程序员效率的秘密武器的问题思考
    Java ForkJoinPool 用法及原理
  • 原文地址:https://www.cnblogs.com/blog-wzy/p/5403246.html
Copyright © 2020-2023  润新知