Android之仿微信图片选择器
先上效果图。第一张图显示的是“相机”文件夹中的所有图片;通过点击多张图片可以到第二张图所示的效果(被选择的图片会变暗,同时选择按钮变亮);点击最下面的那一栏可以到第三张图所示的效果(显示手机中所有包含图片文件的文件夹)。
一、目标
1、尽可能避免内存溢出 A、根据图片的显示大小去压缩图片 B、使用缓存对图片进行管理(LruCache) 2、保证用户操作UI尽可能的流畅 在getView()方法中尽量不做耗时操作(异步显示 + 回调显示) 3、用户预期显示的图片必须尽可能快的显示(图片加载策略的选择: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>