仿微信图片选择器:
一、项目整体分析:
1. Android加载图片的3个目标:
(1)尽可能的去避免内存溢出。
a. 根据图片的显示大小去压缩图片
b. 使用缓存对我们图片进行管理(LruCache)
(2)用户操作UI控件必须充分的流畅。
a. getView里面尽可能不去做耗时的操作(异步加载 + 回调显示)
(3)用户预期显示的图片尽可能的快(图片的加载策略的选择,一般选择是LIFO)。
a. LIFO
2. 定义一个Imageloader完成上面1中的3个目标:
Imageloader
getView()
{
url -> Bitmap
url -> LruCache 查找
->找到返回
->找不到 url -> Task -> TaskQueue且发送一个通知去提醒后台轮询线程。
}
•Task ->run() {根据url加载图片:
1. 获得图片显示的大小
2. 使用Options对图片进行压缩
3. 加载图片且放入LruCache
}
•后台轮询线程
TaskQueue ->Task ->将Task交给线程池去执行(执行run方法)
一般情况下:(我们没有采用,效率低)
new Thread() {
run() {
while(true) {}
}
}.start();
这里这种场景,采用Handler + looper + Message:
3. 项目最终的效果:
(1)默认显示图片最多的文件夹图片,以及底部显示图片总数量。如下图:
(2)点击底部,弹出popupWindow,popupWindow包含所有含有图片的文件夹,以及显示每个文件夹中图片数量。如下图:
(注:此时Activity变暗)
(3)选择任何文件夹,进入该文件夹图片显示,可以点击选择图片,当然了,点击已选择的图片则会取消选择。如下图:
(注:选中图片变暗)
二、代码实践 - 图片缓存、获取、展示
1. 打开Eclipse,新建一个Android工程,命名为"Imageloader",如下:
2. 新建一个包"com.himi.imageloader.util",编写一个图片加载工具类,如下:
ImageLoader.java,如下:
1 package com.himi.imageloader.util; 2 3 import java.lang.reflect.Field; 4 import java.util.LinkedList; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Semaphore; 8 9 import android.annotation.SuppressLint; 10 import android.graphics.Bitmap; 11 import android.graphics.BitmapFactory; 12 import android.graphics.BitmapFactory.Options; 13 import android.os.Handler; 14 import android.os.Looper; 15 import android.os.Message; 16 import android.util.DisplayMetrics; 17 import android.util.LruCache; 18 import android.view.ViewGroup.LayoutParams; 19 import android.widget.ImageView; 20 21 /** 22 * 图片加载类 23 * 这个类使用单例模式 24 * @author hebao 25 * 26 */ 27 public class ImageLoader { 28 private static ImageLoader mInstance; 29 /** 30 * 图片缓存的核心对象 31 * 管理我们所有图片加载的所需的内存 32 */ 33 private LruCache<String, Bitmap> mLruCache; 34 /** 35 * 线程池 36 * 执行一些我们加载图片的任务 37 */ 38 private ExecutorService mThreadPool; 39 /** 40 * 线程池中默认线程数 41 */ 42 private static final int DEAFULT_THREAD_COUNT = 1; 43 44 /** 45 * 队列的调度方式 46 */ 47 private Type mType = Type.LIFO; 48 /** 49 * 任务队列 50 * 任务队列提供给线程池取任务的 51 */ 52 private LinkedList<Runnable> mTaskQueue; 53 /** 54 * 后台轮询线程 55 */ 56 private Thread mPoolThread; 57 /** 58 * 后台轮询线程的handler 59 */ 60 private Handler mPoolThreadHandler; 61 /** 62 * UI线程的handler 63 * 用于:更新ImageView 64 */ 65 private Handler mUIHandler; 66 /** 67 * mPoolThreadHandler的信号量,防止使用mPoolThreadHandler的时候其本身没有初始化完毕,报空指针异常 68 */ 69 private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0); 70 /** 71 * 任务线程信号量,保证线程池真正做到LIFO 72 */ 73 private Semaphore mSemaphoreThreadPool; 74 75 /** 76 * 77 * 调度方式 78 *FIFO:先入先出 79 *LIFO:后入先出 80 */ 81 82 public enum Type { 83 FIFO,LIFO; 84 } 85 86 87 private ImageLoader(int threadCount, Type type) { 88 init(threadCount, type); 89 } 90 91 /** 92 * 初始化操作 93 * @param threadCount 94 * @param type 95 */ 96 private void init(int threadCount, Type type) { 97 //后台轮询线程初始化 98 mPoolThread = new Thread() { 99 @Override 100 public void run() { 101 Looper.prepare(); 102 mPoolThreadHandler = new Handler() { 103 @Override 104 public void handleMessage(Message msg) { 105 //线程池取出一个任务进行执行 106 mThreadPool.execute(getTask()); 107 try { 108 mSemaphoreThreadPool.acquire(); 109 } catch (InterruptedException e) { 110 // TODO 自动生成的 catch 块 111 e.printStackTrace(); 112 } 113 } 114 }; 115 //释放一个信号量 116 mSemaphorePoolThreadHandler.release(); 117 //Looper不断进行轮询 118 Looper.loop(); 119 }; 120 }; 121 mPoolThread.start(); 122 123 //获取我们应用的最大可用内存 124 int maxMemory = (int) Runtime.getRuntime().maxMemory(); 125 int cacheMemory = maxMemory / 8; 126 //图片缓存初始化 127 mLruCache = new LruCache<String, Bitmap>(cacheMemory) { 128 /** 129 * 测量每一个Bitmap图片的大小 130 */ 131 @Override 132 protected int sizeOf(String key, Bitmap value) { 133 // 每一个Bitmap图片的大小 = 每一行字节数 * 高度 134 return value.getRowBytes() * value.getHeight(); 135 } 136 }; 137 138 //创建线程池 139 mThreadPool = Executors.newFixedThreadPool(threadCount); 140 mTaskQueue = new LinkedList<Runnable>(); 141 mType = type; 142 143 //初始化信号量 144 mSemaphoreThreadPool = new Semaphore(threadCount); 145 } 146 147 /** 148 * 从任务队列中取出一个方法 149 * @return 150 */ 151 private Runnable getTask() { 152 if(mType == Type.FIFO) { 153 return mTaskQueue.removeFirst(); 154 }else if(mType == Type.LIFO) { 155 return mTaskQueue.removeLast(); 156 } 157 return null; 158 } 159 160 161 public static ImageLoader getInstance() { 162 if(mInstance == null) { 163 synchronized (ImageLoader.class) { 164 if(mInstance == null) { 165 mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO); 166 } 167 } 168 169 } 170 return mInstance; 171 } 172 173 public static ImageLoader getInstance(int threadCount, Type type) { 174 if(mInstance == null) { 175 synchronized (ImageLoader.class) { 176 if(mInstance == null) { 177 mInstance = new ImageLoader(threadCount, type); 178 } 179 } 180 181 } 182 return mInstance; 183 } 184 185 186 /** 187 * 根据path为ImageView是设置图片 188 * @param path 189 * @param imageView 190 */ 191 public void loadImage(final String path, final ImageView imageView ) { 192 imageView.setTag(path);//设置Tag主要是为了校验,防止图片的混乱 193 if(mUIHandler == null) { 194 mUIHandler = new Handler() { 195 @Override 196 public void handleMessage(Message msg) { 197 //获取得到图片,为imageview回调设置图片 198 ImgBeanHolder holder = (ImgBeanHolder) msg.obj; 199 Bitmap bm = holder.bitmap; 200 ImageView imageview = holder.imageView; 201 String path = holder.path; 202 /** 203 * 将path和getTag存储路径进行比较 204 * 如果不比较,就会出现我们滑动到第二张图片,但是显示的还是第一张的图片 205 * 这里我们绑定imageview和path就是为了防止这种情况 206 */ 207 if(imageview.getTag().toString().equals(path)) { 208 imageview.setImageBitmap(bm); 209 } 210 211 }; 212 }; 213 } 214 //根据path在缓存中获取bitmap 215 Bitmap bm = getBitmapFromLruCache(path); 216 if(bm != null) { 217 refreashBitmap(path, imageView, bm); 218 } else {//内存中没有图片,加载图片到内存 219 addTasks(new Runnable() { 220 public void run() { 221 /**加载图片 222 * 图片的压缩 223 */ 224 //1. 获得图片需要显示的大小 225 ImageSize imageSize = getImageViewSize(imageView); 226 //2. 压缩图片 227 Bitmap bm = decodeSampleBitmapFromPath(path,imageSize.width,imageSize.height); 228 //3. 把图片加载到缓存 (一定要记得) 229 addBitmapToLruCache(path,bm); 230 refreashBitmap(path, imageView, bm); 231 //每次线程任务加载完图片,之后释放一个信号量,即:信号量-1,此时就会寻找下一个任务(根据FIFO/LIFO不同的策略取出任务) 232 mSemaphoreThreadPool.release(); 233 } 234 235 }); 236 } 237 } 238 239 240 public void refreashBitmap(final String path, 241 final ImageView imageView, Bitmap bm) { 242 Message message = Message.obtain(); 243 ImgBeanHolder holder = new ImgBeanHolder(); 244 holder.bitmap = bm; 245 holder.path = path; 246 holder.imageView = imageView; 247 248 message.obj = holder; 249 mUIHandler.sendMessage(message); 250 } 251 252 /** 253 * 将图片加入缓存LruCache 254 * @param path 255 * @param bm 256 */ 257 private void addBitmapToLruCache(String path, Bitmap bm) { 258 if(getBitmapFromLruCache(path) == null) { 259 if(bm != null) { 260 mLruCache.put(path, bm); 261 } 262 } 263 264 } 265 266 267 /** 268 * 根据图片需要显示的宽和高,对图片进行压缩 269 * @param path 270 * @param width 271 * @param height 272 * @return 273 */ 274 private Bitmap decodeSampleBitmapFromPath(String path, 275 int width, int height) { 276 //获取图片的宽和高,但是不把图片加载到内存中 277 BitmapFactory.Options options = new BitmapFactory.Options(); 278 options.inJustDecodeBounds =true;//不把图片加载到内存中 279 BitmapFactory.decodeFile(path, options); 280 281 options.inSampleSize = caculateInSampleSize(options,width, height);//计算获取压缩比 282 //使用获取到的inSampleSize再次解析图片 283 options.inJustDecodeBounds =false;//加载图片到内存 284 Bitmap bitmap = BitmapFactory.decodeFile(path, options); 285 286 287 return bitmap; 288 } 289 290 291 /** 292 *根据需求的宽和高,以及图片实际的宽和高,计算inSampleSize 293 * @param options 294 * @param width 295 * @param height 296 * @return inSampleSize 压缩比 297 */ 298 private int caculateInSampleSize(Options options, int reqWidth, int reqHeight) { 299 int width = options.outWidth; 300 int height = options.outHeight; 301 302 int inSampleSize = 1; 303 if(width>reqWidth || height > reqHeight) { 304 int widthRadio = Math.round(width*1.0f / reqWidth); 305 int heightRadio = Math.round(height*1.0f / reqHeight); 306 307 inSampleSize = Math.max(widthRadio, heightRadio); 308 } 309 310 return inSampleSize; 311 } 312 313 /** 314 * 根据ImageView获取适当的压缩的宽和高 315 * @param imageView 316 * @return 317 */ 318 protected ImageSize getImageViewSize(ImageView imageView) { 319 ImageSize imageSize = new ImageSize(); 320 DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics(); 321 LayoutParams lp = imageView.getLayoutParams(); 322 323 int width = imageView.getWidth();//获取imageview的实际宽度 324 if(width<=0) { 325 width = lp.width;//获取imageview在layout中声明的宽度 326 } 327 if(width<=0) { 328 width = getImageViewFieldValue(imageView, "mMaxWidth");//利用反射,检测获得最大值 329 } 330 if(width<=0) { 331 width = displayMetrics.widthPixels; 332 } 333 334 335 int height = imageView.getHeight();//获取imageview的实际高度 336 if(height<=0) { 337 height = lp.height;//获取imageview在layout中声明的高度 338 } 339 if(height<=0) { 340 height = getImageViewFieldValue(imageView, "mMaxHeight");//利用反射,检测获得最大值 341 } 342 if(height<=0) { 343 height = displayMetrics.heightPixels; 344 } 345 346 imageSize.width = width; 347 imageSize.height = height; 348 return imageSize; 349 }; 350 351 /** 352 * 353 * 通过反射获取imageview的某个属性值 354 * @param object 355 * @param fieldName 356 * @return 357 * 由于方法getMaxHeight是API16以上的才能使用,这里我们用反射使用这个方法 358 */ 359 private static int getImageViewFieldValue(Object object, String fieldName) { 360 int value=0; 361 try { 362 Field field = ImageView.class.getDeclaredField(fieldName); 363 field.setAccessible(true); 364 365 int fieldValue = field.getInt(object); 366 if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) { 367 value = fieldValue; 368 } 369 } catch (Exception e) { 370 // TODO 自动生成的 catch 块 371 e.printStackTrace(); 372 } 373 return value; 374 } 375 376 /** 377 * 添加任务到任务队列,交给线程池执行 378 * @param runnable 379 */ 380 @SuppressLint("NewApi") 381 private synchronized void addTasks(Runnable runnable) {//synchronized同步代码,防止多个线程进来出现死锁 382 mTaskQueue.add(runnable); 383 //if(mPoolThreadHandler == null) wait(); 384 //确保我们在使用mPoolThreadHandler之前,我们初始化完毕mPoolThreadHandler(不为空),这里引入信号量 385 try { 386 if(mPoolThreadHandler == null) { 387 mSemaphorePoolThreadHandler.acquire(); 388 } 389 } catch (InterruptedException e) { 390 // TODO 自动生成的 catch 块 391 e.printStackTrace(); 392 } 393 mPoolThreadHandler.sendEmptyMessage(0x110); 394 395 396 } 397 398 399 /** 400 * 根据path在缓存中获取bitmap 401 * @param key 402 * @return 403 */ 404 private Bitmap getBitmapFromLruCache(String key) { 405 // TODO 自动生成的方法存根 406 return mLruCache.get(key); 407 } 408 409 /** 410 * 压缩图片之后的宽和高 411 * @author Administrator 412 * 413 */ 414 private class ImageSize { 415 int width; 416 int height; 417 } 418 419 private class ImgBeanHolder { 420 Bitmap bitmap; 421 ImageView imageView; 422 String path; 423 } 424 425 }
三、代码实践 - UI、UI适配器
1. 布局文件设计,首先我们从美工那边获得布局设计需要的图片,如下:
来到activity_main.xml,如下:
1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 tools:context="com.himi.imageloader.MainActivity" > 6 7 <!-- 8 android:numColumns="3" 设置显示的列数 9 android:stretchMode="columnWidth" 缩放与列宽大小同步 10 android:cacheColorHint="@android:color/transparent" 自定义GridView拖动背景色 11 android:listSelector="@android:color/transparent" 选中item,item显示透明 12 --> 13 14 <GridView 15 android:id="@+id/id_gridView" 16 android:layout_width="match_parent" 17 android:layout_height="match_parent" 18 android:cacheColorHint="@android:color/transparent" 19 android:horizontalSpacing="3dp" 20 android:listSelector="@android:color/transparent" 21 android:numColumns="3" 22 android:stretchMode="columnWidth" 23 android:verticalSpacing="3dp" /> 24 <RelativeLayout 25 android:layout_width="match_parent" 26 android:layout_height="50dp" 27 android:layout_alignParentBottom="true" 28 android:background="#ee000000" 29 android:clipChildren="true" 30 android:id="@+id/id_bottom_ly" 31 > 32 <TextView 33 android:id="@+id/id_dir_name" 34 android:layout_width="wrap_content" 35 android:layout_height="wrap_content" 36 android:layout_alignParentLeft="true" 37 android:layout_centerVertical="true" 38 android:paddingLeft="10dp" 39 android:text="所有图片" 40 android:textColor="@android:color/white" 41 /> 42 <TextView 43 android:id="@+id/id_dir_count" 44 android:layout_width="wrap_content" 45 android:layout_height="wrap_content" 46 android:layout_alignParentRight="true" 47 android:layout_centerVertical="true" 48 android:paddingRight="10dp" 49 android:text="100张" 50 android:textColor="@android:color/white" 51 /> 52 53 </RelativeLayout> 54 55 56 </RelativeLayout>
显示布局效果如下:
来到item_gridview.xml,如下:
1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 tools:context="com.himi.imageloader.MainActivity" > 6 7 <!-- android:scaleType="centerCrop" 防止图片变形 --> 8 9 <ImageView 10 android:id="@+id/id_item_image" 11 android:layout_width="match_parent" 12 android:layout_height="100dp" 13 android:scaleType="centerCrop" 14 android:src="@drawable/pictures_no" /> 15 16 <ImageButton 17 android:id="@+id/id_item_select" 18 android:clickable="false" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 android:layout_alignParentRight="true" 22 android:layout_alignParentTop="true" 23 android:layout_marginTop="3dp" 24 android:layout_marginRight="3dp" 25 android:background="@null" 26 android:src="@drawable/picture_unselected" 27 /> 28 29 </RelativeLayout>
布局效果如下:
2. 这里我们首先对手机中图片进行扫描,拿到图片数量最多的,直接显示在GridView上;并且扫描结束,得到一个所有包含图片的文件夹信息的集合。为了便于存储手机中所有文件夹信息,我们单独创建一个Bean实体类,命名为"FolderBean",新建包com.himi.imageloader.bean,将这个类放在里面,如下:
1 package com.himi.imageloader.bean; 2 3 /** 4 * FolderBean :图片的文件夹信息类 5 * 6 * 注意: 7 * 用来存储当前文件夹的路径,当前文件夹包含多少张图片,以及第一张图片路径用于做文件夹的图标; 8 * 注:文件夹的名称,我们在set文件夹的路径的时候,自动提取,仔细看下setDir这个方法. 9 * 10 * @author hebao 11 * 12 */ 13 14 public class FolderBean { 15 /** 16 * 图片的文件夹路径 17 */ 18 private String dir; 19 20 /** 21 * 第一张图片的路径 22 */ 23 private String firstImgPath; 24 25 /** 26 * 文件夹的名称 27 */ 28 private String name; 29 30 /** 31 * 图片的数量 32 */ 33 private int count; 34 35 public String getDir() { 36 return dir; 37 } 38 39 public void setDir(String dir) { 40 this.dir = dir; 41 int lastIndexOf = this.dir.lastIndexOf("/"); 42 this.name = this.dir.substring(lastIndexOf); 43 } 44 45 public String getFirstImgPath() { 46 return firstImgPath; 47 } 48 49 public void setFirstImgPath(String firstImgPath) { 50 this.firstImgPath = firstImgPath; 51 } 52 53 public String getName() { 54 return name; 55 } 56 57 public int getCount() { 58 return count; 59 } 60 61 public void setCount(int count) { 62 this.count = count; 63 } 64 65 }
3. 接下来自然要说到扫描手机图片的代码,在MainActivity中,如下:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 initView(); 6 initDatas(); 7 initEvent(); 8 } 9 10 private void initView() { 11 mGridView = (GridView) findViewById(R.id.id_gridView); 12 mBottomLy = (RelativeLayout) findViewById(R.id.id_bottom_ly); 13 mDirName = (TextView) findViewById(R.id.id_dir_name); 14 mDirCount = (TextView) findViewById(R.id.id_dir_count); 15 16 } 17 18 /** 19 * 利用ContentProvider扫描手机中的图片,此方法在运行在子线程中 完成图片的扫描,最终获得jpg最多的那个文件夹 20 */ 21 private void initDatas() { 22 23 if (!Environment.getExternalStorageState().equals( 24 Environment.MEDIA_MOUNTED)) { 25 Toast.makeText(this, "当前存储卡不可用", Toast.LENGTH_SHORT).show(); 26 return; 27 } 28 /** 29 * 显示进度条 30 */ 31 mProgressDialog = ProgressDialog.show(this, null, "正在加载……"); 32 /** 33 * 扫描手机中所有的图片,很明显这是一个耗时的操作,所以我们不能在UI线程中,采用子线程. 34 * 扫描得到的文件夹及其图片信息 在 List<FolderBean> mFolderBeans存储. 35 */ 36 new Thread() { 37 public void run() { 38 Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 39 ContentResolver cr = MainActivity.this.getContentResolver(); 40 //只查询jpeg和png的图片 41 Cursor cursor = cr.query(mImgUri, null, 42 MediaStore.Images.Media.MIME_TYPE + "? or" 43 + MediaStore.Images.Media.MIME_TYPE + "?", 44 new String[] { "image/jpeg", "image/png", }, 45 MediaStore.Images.Media.DATE_MODIFIED); 46 47 /** 48 * 存放已经遍历的文件夹路径,防止重复遍历 49 */ 50 Set<String> mDirPaths = new HashSet<String>(); 51 /** 52 * 遍历手机图片 53 */ 54 while (cursor.moveToNext()) { 55 // 获取图片的路径 56 String path = cursor.getString(cursor 57 .getColumnIndex(MediaStore.Images.Media.DATA)); 58 // 获取该图片的父路径名 59 File parentFile = new File(path).getParentFile(); 60 if (parentFile == null) { 61 continue; 62 } 63 String dirPath = parentFile.getAbsolutePath(); 64 65 FolderBean folderBean = null; 66 // 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~) 67 if (mDirPaths.contains(dirPath)) { 68 continue; 69 } else { 70 mDirPaths.add(dirPath); 71 // 初始化imageFloder 72 folderBean = new FolderBean(); 73 74 //图片的文件夹路径 75 folderBean.setDir(dirPath); 76 //第一张图片的路径 77 folderBean.setFirstImgPath(path); 78 } 79 //有些图片比较诡异~~;无法显示,这里加判断,防止空指针异常 80 if (parentFile.list() == null) { 81 continue; 82 } 83 84 int picSize = parentFile.list(new FilenameFilter() { 85 86 public boolean accept(File dir, String filename) { 87 if (filename.endsWith(".jpg") 88 || filename.endsWith(".jpeg") 89 || filename.endsWith(".png")) { 90 return true; 91 } 92 return false; 93 } 94 }).length; 95 //图片的数量 96 folderBean.setCount(picSize); 97 mFolderBeans.add(folderBean); 98 /** 99 * 如果此时扫描到图片文件夹中图片数量最多,则赋值给mMaxCount,mCurrentDir 100 */ 101 if (picSize > mMaxCount) { 102 mMaxCount = picSize; 103 mCurrentDir = parentFile; 104 } 105 106 } 107 //关闭游标 108 cursor.close(); 109 // 通知handler扫描图片完成 110 mHandler.sendEmptyMessage(DATA_LOADED); 111 112 }; 113 }.start(); 114 115 }
initView就不看了,都是些findViewById;
initDatas主要就是扫描图片的代码,我们开启了一个Thread进行扫描,扫描完成以后,我们得到了图片最多文件夹路径(mCurrentDir),手机中图片数量(totalCount);以及所有包含图片文件夹信息(mFolderBeans)
然后在MainActivity,我们通过handler发送消息,在handleMessage里面:
(1)创建GridView的适配器,为我们的GridView设置适配器,显示图片;
(2)有了mFolderBeans,就可以创建我们的popupWindow了;
1 private Handler mHandler = new Handler() { 2 3 public void handleMessage(android.os.Message msg) { 4 if (msg.what == DATA_LOADED) { 5 mProgressDialog.dismiss(); 6 // 绑定数据到GridView 7 data2View(); 8 // 初始化PopupWindow 9 initDirPopupWindow(); 10 } 11 } 12 };
可以看到分别干了上述的两件事:
(1)在MainActivity中,data2View如下:
data2View就是我们当前Activity上所有的View设置数据了。
1 /** 2 * 为View绑定数据 3 */ 4 private void data2View() { 5 if (mCurrentDir == null) { 6 Toast.makeText(this, "未扫描到任何图片", Toast.LENGTH_SHORT).show(); 7 return; 8 } 9 10 mImgs = Arrays.asList(mCurrentDir.list()); 11 12 /** 13 * 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗; 14 */ 15 mImgAdapter = new ImageAdapter(this, mImgs, 16 mCurrentDir.getAbsolutePath()); 17 mGridView.setAdapter(mImgAdapter); 18 19 mDirCount.setText(mMaxCount + ""); 20 mDirName.setText(mCurrentDir.getName()); 21 22 };
(2)看到上面(1)还用到了一个Adapter(for GridView),我们自定义一个适配器ImageAdapter继承自BaseAdapter,它和MainActivity所处一个包下,如下:
1 package com.himi.imageloader; 2 3 import java.util.HashSet; 4 import java.util.List; 5 import java.util.Set; 6 7 import android.content.Context; 8 import android.graphics.Color; 9 import android.view.LayoutInflater; 10 import android.view.View; 11 import android.view.View.OnClickListener; 12 import android.view.ViewGroup; 13 import android.widget.BaseAdapter; 14 import android.widget.ImageButton; 15 import android.widget.ImageView; 16 17 import com.himi.imageloader.util.ImageLoader; 18 import com.himi.imageloader.util.ImageLoader.Type; 19 20 public class ImageAdapter extends BaseAdapter { 21 /** 22 * 用户选择的图片,存储为图片的完整路径 23 */ 24 private static Set<String> mSelectedImg = new HashSet<String>(); 25 /** 26 * 文件夹路径 27 */ 28 private String mDirPath; 29 private List<String> mImgPaths; 30 private LayoutInflater mInflater; 31 //分开存储文件目录,和文件名。节省内存 32 public ImageAdapter(Context context, List<String> mDatas, String dirPath) { 33 this.mDirPath = dirPath; 34 this.mImgPaths = mDatas; 35 mInflater = LayoutInflater.from(context); 36 } 37 38 public int getCount() { 39 return mImgPaths.size(); 40 } 41 42 public Object getItem(int position) { 43 return mImgPaths.get(position); 44 } 45 46 public long getItemId(int position) { 47 return position; 48 } 49 50 public View getView(final int position, View convertView, ViewGroup parent) { 51 final ViewHolder viewHolder; 52 if(convertView == null) { 53 convertView = mInflater.inflate(R.layout.item_gridview, parent,false); 54 55 viewHolder = new ViewHolder(); 56 viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_item_image); 57 viewHolder.mSelect = (ImageButton) convertView.findViewById(R.id.id_item_select); 58 convertView.setTag(viewHolder); 59 } else { 60 viewHolder = (ViewHolder) convertView.getTag(); 61 } 62 63 /** 64 * 重置状态,如果不重置第一次选中,第二次还会复用之前的,这样就会产生错乱 65 */ 66 viewHolder.mImg.setImageResource(R.drawable.pictures_no); 67 viewHolder.mSelect.setImageResource(R.drawable.picture_unselected); 68 viewHolder.mImg.setColorFilter(null); 69 70 ImageLoader.getInstance(3, Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position), 71 viewHolder.mImg); 72 final String filePath = mDirPath+"/"+mImgPaths.get(position); 73 74 // 设置ImageView的点击事件 75 viewHolder.mImg.setOnClickListener(new OnClickListener() { 76 // 选择,则将图片变暗,反之则反之 77 public void onClick(View v) { 78 //已经被选择 79 if(mSelectedImg.contains(filePath)) { 80 mSelectedImg.remove(filePath); 81 //改变Item状态,没有必要刷新显示 82 viewHolder.mImg.setColorFilter(null); 83 viewHolder.mSelect.setImageResource(R.drawable.picture_unselected); 84 }else {//未被选择 85 mSelectedImg.add(filePath); 86 //改变Item状态,没有必要刷新显示 87 viewHolder.mImg.setColorFilter(Color.parseColor("#77000000")); 88 viewHolder.mSelect.setImageResource(R.drawable.pictures_selected); 89 } 90 //notifyDataSetChanged();不能使用,会出现闪屏 91 92 } 93 }); 94 95 /** 96 * 已经选择过的图片,显示出选择过的效果 97 */ 98 if(mSelectedImg.contains(filePath)) { 99 viewHolder.mImg.setColorFilter(Color.parseColor("#77000000")); 100 viewHolder.mSelect.setImageResource(R.drawable.pictures_selected); 101 } 102 103 return convertView; 104 } 105 106 private class ViewHolder { 107 ImageView mImg; 108 ImageButton mSelect; 109 } 110 111 }
图片策略我们使用的是LIFO后进先出。
到此我们的第一个Activity的所有的任务就完成了~~~
四、展现文件夹的PopupWindow
在我们要实现,点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;
不急着贴代码,我们先考虑下PopupWindow怎么用最好,我们的PopupWindow需要设置布局文件,需要初始化View,需要初始化事件,还需要和Activity交互~~
那么肯定的,我们使用独立的类,这个类和Activity很相似,在里面initView(),initEvent()之类的。
1. 自定义PopupWindow,命名为"ListImageDirPopupWindow ",如下:
1 package com.himi.imageloader; 2 3 import java.util.List; 4 5 import android.content.Context; 6 import android.graphics.drawable.BitmapDrawable; 7 import android.util.DisplayMetrics; 8 import android.view.LayoutInflater; 9 import android.view.MotionEvent; 10 import android.view.View; 11 import android.view.View.OnTouchListener; 12 import android.view.ViewGroup; 13 import android.view.WindowManager; 14 import android.widget.AdapterView; 15 import android.widget.AdapterView.OnItemClickListener; 16 import android.widget.ArrayAdapter; 17 import android.widget.ImageView; 18 import android.widget.ListView; 19 import android.widget.PopupWindow; 20 import android.widget.TextView; 21 22 import com.himi.imageloader.bean.FolderBean; 23 import com.himi.imageloader.util.ImageLoader; 24 25 /** 26 * 自定义的PopupWindow 27 * 作用:展现文件夹信息 28 * @author hebao 29 * 30 */ 31 public class ListImageDirPopupWindow extends PopupWindow { 32 private int mWidth; 33 private int mHeight; 34 private View mConvertView; 35 private ListView mListView; 36 37 38 private List<FolderBean> mDatas; 39 40 41 /** 42 * 文件夹选中的监听器(接口) 43 * @author hebao 44 * 45 */ 46 public interface OnDirSelectedListener { 47 void onSelected(FolderBean folderBean); 48 } 49 public OnDirSelectedListener mListener; 50 public void setOnDirSelectedListener (OnDirSelectedListener mListener) { 51 this.mListener = mListener; 52 } 53 54 55 56 public ListImageDirPopupWindow(Context context, List<FolderBean> datas) { 57 calWidthAndHeight(context); 58 59 mConvertView = LayoutInflater.from(context).inflate(R.layout.popup_main, null); 60 setContentView(mConvertView); 61 62 setWidth(mWidth); 63 setHeight(mHeight); 64 65 //设置可触摸 66 setFocusable(true); 67 setTouchable(true); 68 setOutsideTouchable(true); 69 setBackgroundDrawable(new BitmapDrawable()); 70 71 setTouchInterceptor(new OnTouchListener() { 72 73 public boolean onTouch(View v, MotionEvent event) { 74 if(event.getAction() == MotionEvent.ACTION_OUTSIDE){ 75 dismiss(); 76 return true; 77 } 78 return false; 79 } 80 }); 81 82 initViews(context); 83 initEvent(); 84 85 } 86 87 private void initViews(Context context) { 88 mListView = (ListView) mConvertView.findViewById(R.id.id_list_dir); 89 mListView.setAdapter(new ListDirAdapter(context, mDatas)); 90 } 91 92 /** 93 * 设置监听事件 94 */ 95 private void initEvent() { 96 mListView.setOnItemClickListener(new OnItemClickListener() { 97 98 public void onItemClick(AdapterView<?> parent, View view, 99 int position, long id) { 100 if(mListener != null) { 101 mListener.onSelected(mDatas.get(position)); 102 } 103 104 } 105 106 }); 107 108 } 109 110 111 112 /** 113 * 计算popupWindow的宽度和高度 114 * @param context 115 */ 116 private void calWidthAndHeight(Context context) { 117 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 118 //Andorid.util 包下的DisplayMetrics 类提供了一种关于显示的通用信息,如显示大小,分辨率和字体。 119 DisplayMetrics outMetrics = new DisplayMetrics(); 120 wm.getDefaultDisplay().getMetrics(outMetrics); 121 122 123 mWidth = outMetrics.widthPixels; 124 mHeight = (int) (outMetrics.heightPixels * 0.7); 125 } 126 127 128 private class ListDirAdapter extends ArrayAdapter<FolderBean> { 129 private LayoutInflater mInflater; 130 private List<FolderBean> mDatas; 131 132 public ListDirAdapter(Context context, 133 List<FolderBean> objects) { 134 super(context, 0, objects); 135 mInflater = LayoutInflater.from(context); 136 } 137 138 @Override 139 public View getView(int position, View convertView, ViewGroup parent) { 140 ViewHolder holder = null; 141 if(convertView == null) { 142 holder = new ViewHolder(); 143 convertView = mInflater.inflate(R.layout.item_popup_main, parent, false); 144 145 holder.mImg = (ImageView) convertView.findViewById(R.id.id_id_dir_item_image); 146 holder.mDirName = (TextView) convertView.findViewById(R.id.id_dir_item_name); 147 holder.mDirCount = (TextView) convertView.findViewById(R.id.id_dir_item_count); 148 149 convertView.setTag(holder); 150 } else { 151 holder =(ViewHolder) convertView.getTag(); 152 } 153 FolderBean bean =getItem(position); 154 //重置 155 holder.mImg.setImageResource(R.drawable.pictures_no); 156 157 //回调加载图片 158 ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg); 159 holder.mDirCount.setText(bean.getCount()+""); 160 holder.mDirName.setText(bean.getName()); 161 return convertView; 162 } 163 164 private class ViewHolder { 165 ImageView mImg; 166 TextView mDirName; 167 TextView mDirCount; 168 } 169 } 170 171 }
好了,现在就是我们正在的popupWindow咯,布局文件夹主要是个ListView,所以在initViews里面,我们得设置它的适配器;当然了,这里的适配器依然用我们的ListDirAdapter。
然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片;
关于交互,我们从Activity的角度去看弹出框,Activity想知道什么,只想知道选择了别的文件夹来告诉我,所以我们创建一个接口OnDirSelectedListener ,对Activity设置回调;initEvent初始化事件,如果有人设置了回调,我们就调用。
2. 接下来到MainActivity,完成MainActivity和PopupWindow的交互,如下:
上面说道,当扫描图片完成,拿到包含图片的文件夹信息列表;这个列表就是我们popupWindow所需的数据,所以我们的popupWindow的初始化在handleMessage(上面贴了handler的代码)里面:
在handleMessage里面调用 initDirPopupWindow
1 /** 2 * 初始化展示文件夹的popupWindw 3 */ 4 private void initDirPopupWindow() { 5 mDirPopupWindow = new ListImageDirPopupWindow(this, mFolderBeans); 6 7 mDirPopupWindow.setOnDismissListener(new OnDismissListener() { 8 9 public void onDismiss() { 10 lightOn(); 11 12 } 13 }); 14 15 /** 16 * 设置选择文件夹的回调 17 */ 18 mDirPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() { 19 20 public void onSelected(FolderBean folderBean) { 21 mCurrentDir = new File(folderBean.getDir()); 22 mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() { 23 24 public boolean accept(File dir, String filename) { 25 if (filename.endsWith(".jpg") 26 || filename.endsWith(".jpeg") 27 || filename.endsWith(".png")) { 28 return true; 29 } 30 return false; 31 } 32 })); 33 34 mImgAdapter = new ImageAdapter(MainActivity.this, mImgs, 35 mCurrentDir.getAbsolutePath()); 36 mGridView.setAdapter(mImgAdapter); 37 38 mDirCount.setText(mImgs.size() + ""); 39 mDirName.setText(folderBean.getName()); 40 41 mDirPopupWindow.dismiss(); 42 } 43 }); 44 45 } 46 47 /** 48 * 内容区域变亮 49 */ 50 51 protected void lightOn() { 52 WindowManager.LayoutParams lp = getWindow().getAttributes(); 53 lp.alpha = 1.0f; 54 getWindow().setAttributes(lp); 55 } 56 57 /** 58 * 内容区域变暗 59 */ 60 protected void lightOff() { 61 WindowManager.LayoutParams lp = getWindow().getAttributes(); 62 lp.alpha = .3f; 63 getWindow().setAttributes(lp); 64 65 }
我们初始化我们的popupWindow,设置了关闭对话框的回调,已经设置了选择不同文件夹的回调;
这里仅仅是初始化,下面看我们合适将其弹出的,其实整个Activity也就一个事件,点击弹出该对话框,所以看Activity的initEvent方法:
1 /** 2 * 添加点击事件 3 */ 4 private void initEvent() { 5 mBottomLy.setOnClickListener(new OnClickListener() { 6 7 public void onClick(View v) { 8 // 设置PopupWindow动画 9 mDirPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim); 10 11 // 设置PopupWindow的出现 12 mDirPopupWindow.showAsDropDown(mBottomLy, 0, 0); 13 lightOff(); 14 15 } 16 }); 17 18 }
动画的文件就不贴了,大家自己看源码;
我们改变了GridView的适配器,以及底部的控件上的文件夹名称,文件数量等等;
好了,到此结束;整篇由于篇幅原因没有贴任何布局文件,大家自己通过源码查看;
五、总结:
1. Imageloader:
(1)Handler + Loop + Message(new Thread().start():这种方式效率低)
(2) 图片的压缩
获取图片应当显示的尺寸---> 使用options进行压缩
(3) 图片显示避免错乱
setTag(url);
2. PopupWindow:
单独自定义一个PopupWindow继承自系统的PopupWindow。
然后处理自己的子View事件,把一些关键的回调接口和方法进行返回,让MainActivity进行设置
3. 注意:
ps:请真机测试,反正我的模拟器扫描不到图片~
ps:运行出现空指针的话,在getImages中添加判断,if(parentFile.list()==null)continue , 切记~~~具体位置,上面有说;
源码下载: