仿微信图片选择器:
一、项目整体分析:
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 LruCachemLruCache; 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 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 @Override100 public void run() {101 Looper.prepare();102 mPoolThreadHandler = new Handler() {103 @Override104 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 (cacheMemory) {128 /**129 * 测量每一个Bitmap图片的大小130 */131 @Override132 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 ();141 mType = type;142 143 //初始化信号量144 mSemaphoreThreadPool = new Semaphore(threadCount);145 }146 147 /**148 * 从任务队列中取出一个方法 149 * @return150 */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 path189 * @param imageView190 */191 public void loadImage(final String path, final ImageView imageView ) {192 imageView.setTag(path);//设置Tag主要是为了校验,防止图片的混乱193 if(mUIHandler == null) {194 mUIHandler = new Handler() {195 @Override196 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在缓存中获取bitmap215 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 * 将图片加入缓存LruCache254 * @param path255 * @param bm256 */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 path270 * @param width271 * @param height272 * @return273 */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 *根据需求的宽和高,以及图片实际的宽和高,计算inSampleSize293 * @param options294 * @param width295 * @param height296 * @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 imageView316 * @return317 */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 object355 * @param fieldName356 * @return357 * 由于方法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 runnable379 */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在缓存中获取bitmap401 * @param key402 * @return403 */404 private Bitmap getBitmapFromLruCache(String key) {405 // TODO 自动生成的方法存根406 return mLruCache.get(key);407 }408 409 /**410 * 压缩图片之后的宽和高411 * @author Administrator412 *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,如下:
16 7 13 14 24 32 54 55 5642 52 53
显示布局效果如下:
来到item_gridview.xml,如下:
16 7 8 9 15 16 28 29
布局效果如下:
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 hebao11 * 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 * 扫描得到的文件夹及其图片信息 在 ListmFolderBeans存储. 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 mDirPaths = new HashSet (); 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,mCurrentDir100 */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 SetmSelectedImg = new HashSet (); 25 /** 26 * 文件夹路径 27 */ 28 private String mDirPath; 29 private List mImgPaths; 30 private LayoutInflater mInflater; 31 //分开存储文件目录,和文件名。节省内存 32 public ImageAdapter(Context context, List 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 ListmDatas; 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 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 context115 */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 {129 private LayoutInflater mInflater;130 private List mDatas;131 132 public ListDirAdapter(Context context,133 List objects) {134 super(context, 0, objects);135 mInflater = LayoutInflater.from(context);136 }137 138 @Override139 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 , 切记~~~具体位置,上面有说;
源码下载: