仿微信图片选择器:

一、项目整体分析:

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. import java.lang.reflect.Field;
  3. import java.util.LinkedList;
  4. import java.util.concurrent.ExecutorService;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.Semaphore;
  7. import android.annotation.SuppressLint;
  8. import android.graphics.Bitmap;
  9. import android.graphics.BitmapFactory;
  10. import android.graphics.BitmapFactory.Options;
  11. import android.os.Handler;
  12. import android.os.Looper;
  13. import android.os.Message;
  14. import android.util.DisplayMetrics;
  15. import android.util.LruCache;
  16. import android.view.ViewGroup.LayoutParams;
  17. import android.widget.ImageView;
  18. /**
  19. * 图片加载类
  20. * 这个类使用单例模式
  21. * @author hebao
  22. *
  23. */
  24. public class ImageLoader {
  25. private static ImageLoader mInstance;
  26. /**
  27. * 图片缓存的核心对象
  28. * 管理我们所有图片加载的所需的内存
  29. */
  30. private LruCache<String, Bitmap> mLruCache;
  31. /**
  32. * 线程池
  33. * 执行一些我们加载图片的任务
  34. */
  35. private ExecutorService mThreadPool;
  36. /**
  37. * 线程池中默认线程数
  38. */
  39. private static final int DEAFULT_THREAD_COUNT = 1;
  40. /**
  41. * 队列的调度方式
  42. */
  43. private Type mType = Type.LIFO;
  44. /**
  45. * 任务队列
  46. * 任务队列提供给线程池取任务的
  47. */
  48. private LinkedList<Runnable> mTaskQueue;
  49. /**
  50. * 后台轮询线程
  51. */
  52. private Thread mPoolThread;
  53. /**
  54. * 后台轮询线程的handler
  55. */
  56. private Handler mPoolThreadHandler;
  57. /**
  58. * UI线程的handler
  59. * 用于:更新ImageView
  60. */
  61. private Handler mUIHandler;
  62. /**
  63. * mPoolThreadHandler的信号量,防止使用mPoolThreadHandler的时候其本身没有初始化完毕,报空指针异常
  64. */
  65. private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
  66. /**
  67. * 任务线程信号量,保证线程池真正做到LIFO
  68. */
  69. private Semaphore mSemaphoreThreadPool;
  70. /**
  71. *
  72. * 调度方式
  73. *FIFO:先入先出
  74. *LIFO:后入先出
  75. */
  76. public enum Type {
  77. FIFO,LIFO;
  78. }
  79. private ImageLoader(int threadCount, Type type) {
  80. init(threadCount, type);
  81. }
  82. /**
  83. * 初始化操作
  84. * @param threadCount
  85. * @param type
  86. */
  87. private void init(int threadCount, Type type) {
  88. //后台轮询线程初始化
  89. mPoolThread = new Thread() {
  90. @Override
  91. public void run() {
  92. Looper.prepare();
  93. mPoolThreadHandler = new Handler() {
  94. @Override
  95. public void handleMessage(Message msg) {
  96. //线程池取出一个任务进行执行
  97. mThreadPool.execute(getTask());
  98. try {
  99. mSemaphoreThreadPool.acquire();
  100. } catch (InterruptedException e) {
  101. // TODO 自动生成的 catch 块
  102. e.printStackTrace();
  103. }
  104. }
  105. };
  106. //释放一个信号量
  107. mSemaphorePoolThreadHandler.release();
  108. //Looper不断进行轮询
  109. Looper.loop();
  110. };
  111. };
  112. mPoolThread.start();
  113. //获取我们应用的最大可用内存
  114. int maxMemory = (int) Runtime.getRuntime().maxMemory();
  115. int cacheMemory = maxMemory / 8;
  116. //图片缓存初始化
  117. mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
  118. /**
  119. * 测量每一个Bitmap图片的大小
  120. */
  121. @Override
  122. protected int sizeOf(String key, Bitmap value) {
  123. // 每一个Bitmap图片的大小 = 每一行字节数 * 高度
  124. return value.getRowBytes() * value.getHeight();
  125. }
  126. };
  127. //创建线程池
  128. mThreadPool = Executors.newFixedThreadPool(threadCount);
  129. mTaskQueue = new LinkedList<Runnable>();
  130. mType = type;
  131. //初始化信号量
  132. mSemaphoreThreadPool = new Semaphore(threadCount);
  133. }
  134. /**
  135. * 从任务队列中取出一个方法
  136. * @return
  137. */
  138. private Runnable getTask() {
  139. if(mType == Type.FIFO) {
  140. return mTaskQueue.removeFirst();
  141. }else if(mType == Type.LIFO) {
  142. return mTaskQueue.removeLast();
  143. }
  144. return null;
  145. }
  146. public static ImageLoader getInstance() {
  147. if(mInstance == null) {
  148. synchronized (ImageLoader.class) {
  149. if(mInstance == null) {
  150. mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO);
  151. }
  152. }
  153. }
  154. return mInstance;
  155. }
  156. public static ImageLoader getInstance(int threadCount, Type type) {
  157. if(mInstance == null) {
  158. synchronized (ImageLoader.class) {
  159. if(mInstance == null) {
  160. mInstance = new ImageLoader(threadCount, type);
  161. }
  162. }
  163. }
  164. return mInstance;
  165. }
  166. /**
  167. * 根据path为ImageView是设置图片
  168. * @param path
  169. * @param imageView
  170. */
  171. public void loadImage(final String path, final ImageView imageView ) {
  172. imageView.setTag(path);//设置Tag主要是为了校验,防止图片的混乱
  173. if(mUIHandler == null) {
  174. mUIHandler = new Handler() {
  175. @Override
  176. public void handleMessage(Message msg) {
  177. //获取得到图片,为imageview回调设置图片
  178. ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
  179. Bitmap bm = holder.bitmap;
  180. ImageView imageview = holder.imageView;
  181. String path = holder.path;
  182. /**
  183. * 将path和getTag存储路径进行比较
  184. * 如果不比较,就会出现我们滑动到第二张图片,但是显示的还是第一张的图片
  185. * 这里我们绑定imageview和path就是为了防止这种情况
  186. */
  187. if(imageview.getTag().toString().equals(path)) {
  188. imageview.setImageBitmap(bm);
  189. }
  190. };
  191. };
  192. }
  193. //根据path在缓存中获取bitmap
  194. Bitmap bm = getBitmapFromLruCache(path);
  195. if(bm != null) {
  196. refreashBitmap(path, imageView, bm);
  197. } else {//内存中没有图片,加载图片到内存
  198. addTasks(new Runnable() {
  199. public void run() {
  200. /**加载图片
  201. * 图片的压缩
  202. */
  203. //1. 获得图片需要显示的大小
  204. ImageSize imageSize = getImageViewSize(imageView);
  205. //2. 压缩图片
  206. Bitmap bm = decodeSampleBitmapFromPath(path,imageSize.width,imageSize.height);
  207. //3. 把图片加载到缓存 (一定要记得)
  208. addBitmapToLruCache(path,bm);
  209. refreashBitmap(path, imageView, bm);
  210. //每次线程任务加载完图片,之后释放一个信号量,即:信号量-1,此时就会寻找下一个任务(根据FIFO/LIFO不同的策略取出任务)
  211. mSemaphoreThreadPool.release();
  212. }
  213. });
  214. }
  215. }
  216. public void refreashBitmap(final String path,
  217. final ImageView imageView, Bitmap bm) {
  218. Message message = Message.obtain();
  219. ImgBeanHolder holder = new ImgBeanHolder();
  220. holder.bitmap = bm;
  221. holder.path = path;
  222. holder.imageView = imageView;
  223. message.obj = holder;
  224. mUIHandler.sendMessage(message);
  225. }
  226. /**
  227. * 将图片加入缓存LruCache
  228. * @param path
  229. * @param bm
  230. */
  231. private void addBitmapToLruCache(String path, Bitmap bm) {
  232. if(getBitmapFromLruCache(path) == null) {
  233. if(bm != null) {
  234. mLruCache.put(path, bm);
  235. }
  236. }
  237. }
  238. /**
  239. * 根据图片需要显示的宽和高,对图片进行压缩
  240. * @param path
  241. * @param width
  242. * @param height
  243. * @return
  244. */
  245. private Bitmap decodeSampleBitmapFromPath(String path,
  246. int width, int height) {
  247. //获取图片的宽和高,但是不把图片加载到内存中
  248. BitmapFactory.Options options = new BitmapFactory.Options();
  249. options.inJustDecodeBounds =true;//不把图片加载到内存中
  250. BitmapFactory.decodeFile(path, options);
  251. options.inSampleSize = caculateInSampleSize(options,width, height);//计算获取压缩比
  252. //使用获取到的inSampleSize再次解析图片
  253. options.inJustDecodeBounds =false;//加载图片到内存
  254. Bitmap bitmap = BitmapFactory.decodeFile(path, options);
  255. return bitmap;
  256. }
  257. /**
  258. *根据需求的宽和高,以及图片实际的宽和高,计算inSampleSize
  259. * @param options
  260. * @param width
  261. * @param height
  262. * @return inSampleSize 压缩比
  263. */
  264. private int caculateInSampleSize(Options options, int reqWidth, int reqHeight) {
  265. int width = options.outWidth;
  266. int height = options.outHeight;
  267. int inSampleSize = 1;
  268. if(width>reqWidth || height > reqHeight) {
  269. int widthRadio = Math.round(width*1.0f / reqWidth);
  270. int heightRadio = Math.round(height*1.0f / reqHeight);
  271. inSampleSize = Math.max(widthRadio, heightRadio);
  272. }
  273. return inSampleSize;
  274. }
  275. /**
  276. * 根据ImageView获取适当的压缩的宽和高
  277. * @param imageView
  278. * @return
  279. */
  280. protected ImageSize getImageViewSize(ImageView imageView) {
  281. ImageSize imageSize = new ImageSize();
  282. DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
  283. LayoutParams lp = imageView.getLayoutParams();
  284. int width = imageView.getWidth();//获取imageview的实际宽度
  285. if(width<=0) {
  286. width = lp.width;//获取imageview在layout中声明的宽度
  287. }
  288. if(width<=0) {
  289. width = getImageViewFieldValue(imageView, "mMaxWidth");//利用反射,检测获得最大值
  290. }
  291. if(width<=0) {
  292. width = displayMetrics.widthPixels;
  293. }
  294. int height = imageView.getHeight();//获取imageview的实际高度
  295. if(height<=0) {
  296. height = lp.height;//获取imageview在layout中声明的高度
  297. }
  298. if(height<=0) {
  299. height = getImageViewFieldValue(imageView, "mMaxHeight");//利用反射,检测获得最大值
  300. }
  301. if(height<=0) {
  302. height = displayMetrics.heightPixels;
  303. }
  304. imageSize.width = width;
  305. imageSize.height = height;
  306. return imageSize;
  307. };
  308. /**
  309. *
  310. * 通过反射获取imageview的某个属性值
  311. * @param object
  312. * @param fieldName
  313. * @return
  314. * 由于方法getMaxHeight是API16以上的才能使用,这里我们用反射使用这个方法
  315. */
  316. private static int getImageViewFieldValue(Object object, String fieldName) {
  317. int value=0;
  318. try {
  319. Field field = ImageView.class.getDeclaredField(fieldName);
  320. field.setAccessible(true);
  321. int fieldValue = field.getInt(object);
  322. if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
  323. value = fieldValue;
  324. }
  325. } catch (Exception e) {
  326. // TODO 自动生成的 catch 块
  327. e.printStackTrace();
  328. }
  329. return value;
  330. }
  331. /**
  332. * 添加任务到任务队列,交给线程池执行
  333. * @param runnable
  334. */
  335. @SuppressLint("NewApi")
  336. private synchronized void addTasks(Runnable runnable) {//synchronized同步代码,防止多个线程进来出现死锁
  337. mTaskQueue.add(runnable);
  338. //if(mPoolThreadHandler == null) wait();
  339. //确保我们在使用mPoolThreadHandler之前,我们初始化完毕mPoolThreadHandler(不为空),这里引入信号量
  340. try {
  341. if(mPoolThreadHandler == null) {
  342. mSemaphorePoolThreadHandler.acquire();
  343. }
  344. } catch (InterruptedException e) {
  345. // TODO 自动生成的 catch 块
  346. e.printStackTrace();
  347. }
  348. mPoolThreadHandler.sendEmptyMessage(0x110);
  349. }
  350. /**
  351. * 根据path在缓存中获取bitmap
  352. * @param key
  353. * @return
  354. */
  355. private Bitmap getBitmapFromLruCache(String key) {
  356. // TODO 自动生成的方法存根
  357. return mLruCache.get(key);
  358. }
  359. /**
  360. * 压缩图片之后的宽和高
  361. * @author Administrator
  362. *
  363. */
  364. private class ImageSize {
  365. int width;
  366. int height;
  367. }
  368. private class ImgBeanHolder {
  369. Bitmap bitmap;
  370. ImageView imageView;
  371. String path;
  372. }
  373. }

三、代码实践 - 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. android:numColumns="3" 设置显示的列数
  8. android:stretchMode="columnWidth" 缩放与列宽大小同步
  9. android:cacheColorHint="@android:color/transparent" 自定义GridView拖动背景色
  10. android:listSelector="@android:color/transparent" 选中item,item显示透明
  11. -->
  12. <GridView
  13. android:id="@+id/id_gridView"
  14. android:layout_width="match_parent"
  15. android:layout_height="match_parent"
  16. android:cacheColorHint="@android:color/transparent"
  17. android:horizontalSpacing="3dp"
  18. android:listSelector="@android:color/transparent"
  19. android:numColumns="3"
  20. android:stretchMode="columnWidth"
  21. android:verticalSpacing="3dp" />
  22. <RelativeLayout
  23. android:layout_width="match_parent"
  24. android:layout_height="50dp"
  25. android:layout_alignParentBottom="true"
  26. android:background="#ee000000"
  27. android:clipChildren="true"
  28. android:id="@+id/id_bottom_ly"
  29. >
  30. <TextView
  31. android:id="@+id/id_dir_name"
  32. android:layout_width="wrap_content"
  33. android:layout_height="wrap_content"
  34. android:layout_alignParentLeft="true"
  35. android:layout_centerVertical="true"
  36. android:paddingLeft="10dp"
  37. android:text="所有图片"
  38. android:textColor="@android:color/white"
  39. />
  40. <TextView
  41. android:id="@+id/id_dir_count"
  42. android:layout_width="wrap_content"
  43. android:layout_height="wrap_content"
  44. android:layout_alignParentRight="true"
  45. android:layout_centerVertical="true"
  46. android:paddingRight="10dp"
  47. android:text="100张"
  48. android:textColor="@android:color/white"
  49. />
  50. </RelativeLayout>
  51. </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. <!-- android:scaleType="centerCrop" 防止图片变形 -->
  7. <ImageView
  8. android:id="@+id/id_item_image"
  9. android:layout_width="match_parent"
  10. android:layout_height="100dp"
  11. android:scaleType="centerCrop"
  12. android:src="@drawable/pictures_no" />
  13. <ImageButton
  14. android:id="@+id/id_item_select"
  15. android:clickable="false"
  16. android:layout_width="wrap_content"
  17. android:layout_height="wrap_content"
  18. android:layout_alignParentRight="true"
  19. android:layout_alignParentTop="true"
  20. android:layout_marginTop="3dp"
  21. android:layout_marginRight="3dp"
  22. android:background="@null"
  23. android:src="@drawable/picture_unselected"
  24. />
  25. </RelativeLayout>

布局效果如下:

2. 这里我们首先对手机中图片进行扫描,拿到图片数量最多的,直接显示在GridView上;并且扫描结束,得到一个所有包含图片的文件夹信息的集合。为了便于存储手机中所有文件夹信息,我们单独创建一个Bean实体类,命名为"FolderBean",新建包com.himi.imageloader.bean,将这个类放在里面,如下:

  1. package com.himi.imageloader.bean;
  2. /**
  3. * FolderBean :图片的文件夹信息类
  4. *
  5. * 注意:
  6. * 用来存储当前文件夹的路径,当前文件夹包含多少张图片,以及第一张图片路径用于做文件夹的图标;
  7. * 注:文件夹的名称,我们在set文件夹的路径的时候,自动提取,仔细看下setDir这个方法.
  8. *
  9. * @author hebao
  10. *
  11. */
  12. public class FolderBean {
  13. /**
  14. * 图片的文件夹路径
  15. */
  16. private String dir;
  17. /**
  18. * 第一张图片的路径
  19. */
  20. private String firstImgPath;
  21. /**
  22. * 文件夹的名称
  23. */
  24. private String name;
  25. /**
  26. * 图片的数量
  27. */
  28. private int count;
  29. public String getDir() {
  30. return dir;
  31. }
  32. public void setDir(String dir) {
  33. this.dir = dir;
  34. int lastIndexOf = this.dir.lastIndexOf("/");
  35. this.name = this.dir.substring(lastIndexOf);
  36. }
  37. public String getFirstImgPath() {
  38. return firstImgPath;
  39. }
  40. public void setFirstImgPath(String firstImgPath) {
  41. this.firstImgPath = firstImgPath;
  42. }
  43. public String getName() {
  44. return name;
  45. }
  46. public int getCount() {
  47. return count;
  48. }
  49. public void setCount(int count) {
  50. this.count = count;
  51. }
  52. }

 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. private void initView() {
  10. mGridView = (GridView) findViewById(R.id.id_gridView);
  11. mBottomLy = (RelativeLayout) findViewById(R.id.id_bottom_ly);
  12. mDirName = (TextView) findViewById(R.id.id_dir_name);
  13. mDirCount = (TextView) findViewById(R.id.id_dir_count);
  14. }
  15. /**
  16. * 利用ContentProvider扫描手机中的图片,此方法在运行在子线程中 完成图片的扫描,最终获得jpg最多的那个文件夹
  17. */
  18. private void initDatas() {
  19. if (!Environment.getExternalStorageState().equals(
  20. Environment.MEDIA_MOUNTED)) {
  21. Toast.makeText(this, "当前存储卡不可用", Toast.LENGTH_SHORT).show();
  22. return;
  23. }
  24. /**
  25. * 显示进度条
  26. */
  27. mProgressDialog = ProgressDialog.show(this, null, "正在加载……");
  28. /**
  29. * 扫描手机中所有的图片,很明显这是一个耗时的操作,所以我们不能在UI线程中,采用子线程.
  30. * 扫描得到的文件夹及其图片信息 在 List<FolderBean> mFolderBeans存储.
  31. */
  32. new Thread() {
  33. public void run() {
  34. Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
  35. ContentResolver cr = MainActivity.this.getContentResolver();
  36. //只查询jpeg和png的图片
  37. Cursor cursor = cr.query(mImgUri, null,
  38. MediaStore.Images.Media.MIME_TYPE + "? or"
  39. + MediaStore.Images.Media.MIME_TYPE + "?",
  40. new String[] { "image/jpeg", "image/png", },
  41. MediaStore.Images.Media.DATE_MODIFIED);
  42. /**
  43. * 存放已经遍历的文件夹路径,防止重复遍历
  44. */
  45. Set<String> mDirPaths = new HashSet<String>();
  46. /**
  47. * 遍历手机图片
  48. */
  49. while (cursor.moveToNext()) {
  50. // 获取图片的路径
  51. String path = cursor.getString(cursor
  52. .getColumnIndex(MediaStore.Images.Media.DATA));
  53. // 获取该图片的父路径名
  54. File parentFile = new File(path).getParentFile();
  55. if (parentFile == null) {
  56. continue;
  57. }
  58. String dirPath = parentFile.getAbsolutePath();
  59. FolderBean folderBean = null;
  60. // 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~)
  61. if (mDirPaths.contains(dirPath)) {
  62. continue;
  63. } else {
  64. mDirPaths.add(dirPath);
  65. // 初始化imageFloder
  66. folderBean = new FolderBean();
  67. //图片的文件夹路径
  68. folderBean.setDir(dirPath);
  69. //第一张图片的路径
  70. folderBean.setFirstImgPath(path);
  71. }
  72. //有些图片比较诡异~~;无法显示,这里加判断,防止空指针异常
  73. if (parentFile.list() == null) {
  74. continue;
  75. }
  76. int picSize = parentFile.list(new FilenameFilter() {
  77. public boolean accept(File dir, String filename) {
  78. if (filename.endsWith(".jpg")
  79. || filename.endsWith(".jpeg")
  80. || filename.endsWith(".png")) {
  81. return true;
  82. }
  83. return false;
  84. }
  85. }).length;
  86. //图片的数量
  87. folderBean.setCount(picSize);
  88. mFolderBeans.add(folderBean);
  89. /**
  90. * 如果此时扫描到图片文件夹中图片数量最多,则赋值给mMaxCount,mCurrentDir
  91. */
  92. if (picSize > mMaxCount) {
  93. mMaxCount = picSize;
  94. mCurrentDir = parentFile;
  95. }
  96. }
  97. //关闭游标
  98. cursor.close();
  99. // 通知handler扫描图片完成
  100. mHandler.sendEmptyMessage(DATA_LOADED);
  101. };
  102. }.start();
  103. }

initView就不看了,都是些findViewById;

initDatas主要就是扫描图片的代码,我们开启了一个Thread进行扫描,扫描完成以后,我们得到了图片最多文件夹路径(mCurrentDir),手机中图片数量(totalCount);以及所有包含图片文件夹信息(mFolderBeans)

然后在MainActivity,我们通过handler发送消息,在handleMessage里面:

()创建GridView的适配器,为我们的GridView设置适配器,显示图片;

()有了mFolderBeans,就可以创建我们的popupWindow了;

  1. private Handler mHandler = new Handler() {
  2. public void handleMessage(android.os.Message msg) {
  3. if (msg.what == DATA_LOADED) {
  4. mProgressDialog.dismiss();
  5. // 绑定数据到GridView
  6. data2View();
  7. // 初始化PopupWindow
  8. initDirPopupWindow();
  9. }
  10. }
  11. };

可以看到分别干了上述的两件事:

()在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. mImgs = Arrays.asList(mCurrentDir.list());
  10. /**
  11. * 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗;
  12. */
  13. mImgAdapter = new ImageAdapter(this, mImgs,
  14. mCurrentDir.getAbsolutePath());
  15. mGridView.setAdapter(mImgAdapter);
  16. mDirCount.setText(mMaxCount + "");
  17. mDirName.setText(mCurrentDir.getName());
  18. };

()看到上面(1)还用到了一个Adapter(for GridView),我们自定义一个适配器ImageAdapter继承自BaseAdapter,它和MainActivity所处一个包下,如下

  1. package com.himi.imageloader;
  2. import java.util.HashSet;
  3. import java.util.List;
  4. import java.util.Set;
  5. import android.content.Context;
  6. import android.graphics.Color;
  7. import android.view.LayoutInflater;
  8. import android.view.View;
  9. import android.view.View.OnClickListener;
  10. import android.view.ViewGroup;
  11. import android.widget.BaseAdapter;
  12. import android.widget.ImageButton;
  13. import android.widget.ImageView;
  14. import com.himi.imageloader.util.ImageLoader;
  15. import com.himi.imageloader.util.ImageLoader.Type;
  16. public class ImageAdapter extends BaseAdapter {
  17. /**
  18. * 用户选择的图片,存储为图片的完整路径
  19. */
  20. private static Set<String> mSelectedImg = new HashSet<String>();
  21. /**
  22. * 文件夹路径
  23. */
  24. private String mDirPath;
  25. private List<String> mImgPaths;
  26. private LayoutInflater mInflater;
  27. //分开存储文件目录,和文件名。节省内存
  28. public ImageAdapter(Context context, List<String> mDatas, String dirPath) {
  29. this.mDirPath = dirPath;
  30. this.mImgPaths = mDatas;
  31. mInflater = LayoutInflater.from(context);
  32. }
  33. public int getCount() {
  34. return mImgPaths.size();
  35. }
  36. public Object getItem(int position) {
  37. return mImgPaths.get(position);
  38. }
  39. public long getItemId(int position) {
  40. return position;
  41. }
  42. public View getView(final int position, View convertView, ViewGroup parent) {
  43. final ViewHolder viewHolder;
  44. if(convertView == null) {
  45. convertView = mInflater.inflate(R.layout.item_gridview, parent,false);
  46. viewHolder = new ViewHolder();
  47. viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_item_image);
  48. viewHolder.mSelect = (ImageButton) convertView.findViewById(R.id.id_item_select);
  49. convertView.setTag(viewHolder);
  50. } else {
  51. viewHolder = (ViewHolder) convertView.getTag();
  52. }
  53. /**
  54. * 重置状态,如果不重置第一次选中,第二次还会复用之前的,这样就会产生错乱
  55. */
  56. viewHolder.mImg.setImageResource(R.drawable.pictures_no);
  57. viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
  58. viewHolder.mImg.setColorFilter(null);
  59. ImageLoader.getInstance(3, Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position),
  60. viewHolder.mImg);
  61. final String filePath = mDirPath+"/"+mImgPaths.get(position);
  62. // 设置ImageView的点击事件
  63. viewHolder.mImg.setOnClickListener(new OnClickListener() {
  64. // 选择,则将图片变暗,反之则反之
  65. public void onClick(View v) {
  66. //已经被选择
  67. if(mSelectedImg.contains(filePath)) {
  68. mSelectedImg.remove(filePath);
  69. //改变Item状态,没有必要刷新显示
  70. viewHolder.mImg.setColorFilter(null);
  71. viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
  72. }else {//未被选择
  73. mSelectedImg.add(filePath);
  74. //改变Item状态,没有必要刷新显示
  75. viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
  76. viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
  77. }
  78. //notifyDataSetChanged();不能使用,会出现闪屏
  79. }
  80. });
  81. /**
  82. * 已经选择过的图片,显示出选择过的效果
  83. */
  84. if(mSelectedImg.contains(filePath)) {
  85. viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
  86. viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
  87. }
  88. return convertView;
  89. }
  90. private class ViewHolder {
  91. ImageView mImg;
  92. ImageButton mSelect;
  93. }
  94. }

图片策略我们使用的是LIFO后进先出。

到此我们的第一个Activity的所有的任务就完成了~~~

四、展现文件夹的PopupWindow

在我们要实现,点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;

不急着贴代码,我们先考虑下PopupWindow怎么用最好,我们的PopupWindow需要设置布局文件,需要初始化View,需要初始化事件,还需要和Activity交互~~

那么肯定的,我们使用独立的类,这个类和Activity很相似,在里面initView(),initEvent()之类的。

1.  自定义PopupWindow,命名为"ListImageDirPopupWindow ",如下:

  1. package com.himi.imageloader;
  2. import java.util.List;
  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.View.OnTouchListener;
  10. import android.view.ViewGroup;
  11. import android.view.WindowManager;
  12. import android.widget.AdapterView;
  13. import android.widget.AdapterView.OnItemClickListener;
  14. import android.widget.ArrayAdapter;
  15. import android.widget.ImageView;
  16. import android.widget.ListView;
  17. import android.widget.PopupWindow;
  18. import android.widget.TextView;
  19. import com.himi.imageloader.bean.FolderBean;
  20. import com.himi.imageloader.util.ImageLoader;
  21. /**
  22. * 自定义的PopupWindow
  23. * 作用:展现文件夹信息
  24. * @author hebao
  25. *
  26. */
  27. public class ListImageDirPopupWindow extends PopupWindow {
  28. private int mWidth;
  29. private int mHeight;
  30. private View mConvertView;
  31. private ListView mListView;
  32. private List<FolderBean> mDatas;
  33. /**
  34. * 文件夹选中的监听器(接口)
  35. * @author hebao
  36. *
  37. */
  38. public interface OnDirSelectedListener {
  39. void onSelected(FolderBean folderBean);
  40. }
  41. public OnDirSelectedListener mListener;
  42. public void setOnDirSelectedListener (OnDirSelectedListener mListener) {
  43. this.mListener = mListener;
  44. }
  45. public ListImageDirPopupWindow(Context context, List<FolderBean> datas) {
  46. calWidthAndHeight(context);
  47. mConvertView = LayoutInflater.from(context).inflate(R.layout.popup_main, null);
  48. setContentView(mConvertView);
  49. setWidth(mWidth);
  50. setHeight(mHeight);
  51. //设置可触摸
  52. setFocusable(true);
  53. setTouchable(true);
  54. setOutsideTouchable(true);
  55. setBackgroundDrawable(new BitmapDrawable());
  56. setTouchInterceptor(new OnTouchListener() {
  57. public boolean onTouch(View v, MotionEvent event) {
  58. if(event.getAction() == MotionEvent.ACTION_OUTSIDE){
  59. dismiss();
  60. return true;
  61. }
  62. return false;
  63. }
  64. });
  65. initViews(context);
  66. initEvent();
  67. }
  68. private void initViews(Context context) {
  69. mListView = (ListView) mConvertView.findViewById(R.id.id_list_dir);
  70. mListView.setAdapter(new ListDirAdapter(context, mDatas));
  71. }
  72. /**
  73. * 设置监听事件
  74. */
  75. private void initEvent() {
  76. mListView.setOnItemClickListener(new OnItemClickListener() {
  77. public void onItemClick(AdapterView<?> parent, View view,
  78. int position, long id) {
  79. if(mListener != null) {
  80. mListener.onSelected(mDatas.get(position));
  81. }
  82. }
  83. });
  84. }
  85. /**
  86. * 计算popupWindow的宽度和高度
  87. * @param context
  88. */
  89. private void calWidthAndHeight(Context context) {
  90. WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  91. //Andorid.util 包下的DisplayMetrics 类提供了一种关于显示的通用信息,如显示大小,分辨率和字体。
  92. DisplayMetrics outMetrics = new DisplayMetrics();
  93. wm.getDefaultDisplay().getMetrics(outMetrics);
  94. mWidth = outMetrics.widthPixels;
  95. mHeight = (int) (outMetrics.heightPixels * 0.7);
  96. }
  97. private class ListDirAdapter extends ArrayAdapter<FolderBean> {
  98. private LayoutInflater mInflater;
  99. private List<FolderBean> mDatas;
  100. public ListDirAdapter(Context context,
  101. List<FolderBean> objects) {
  102. super(context, 0, objects);
  103. mInflater = LayoutInflater.from(context);
  104. }
  105. @Override
  106. public View getView(int position, View convertView, ViewGroup parent) {
  107. ViewHolder holder = null;
  108. if(convertView == null) {
  109. holder = new ViewHolder();
  110. convertView = mInflater.inflate(R.layout.item_popup_main, parent, false);
  111. holder.mImg = (ImageView) convertView.findViewById(R.id.id_id_dir_item_image);
  112. holder.mDirName = (TextView) convertView.findViewById(R.id.id_dir_item_name);
  113. holder.mDirCount = (TextView) convertView.findViewById(R.id.id_dir_item_count);
  114. convertView.setTag(holder);
  115. } else {
  116. holder =(ViewHolder) convertView.getTag();
  117. }
  118. FolderBean bean =getItem(position);
  119. //重置
  120. holder.mImg.setImageResource(R.drawable.pictures_no);
  121. //回调加载图片
  122. ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg);
  123. holder.mDirCount.setText(bean.getCount()+"");
  124. holder.mDirName.setText(bean.getName());
  125. return convertView;
  126. }
  127. private class ViewHolder {
  128. ImageView mImg;
  129. TextView mDirName;
  130. TextView mDirCount;
  131. }
  132. }
  133. }

好了,现在就是我们正在的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. mDirPopupWindow.setOnDismissListener(new OnDismissListener() {
  7. public void onDismiss() {
  8. lightOn();
  9. }
  10. });
  11. /**
  12. * 设置选择文件夹的回调
  13. */
  14. mDirPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() {
  15. public void onSelected(FolderBean folderBean) {
  16. mCurrentDir = new File(folderBean.getDir());
  17. mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() {
  18. public boolean accept(File dir, String filename) {
  19. if (filename.endsWith(".jpg")
  20. || filename.endsWith(".jpeg")
  21. || filename.endsWith(".png")) {
  22. return true;
  23. }
  24. return false;
  25. }
  26. }));
  27. mImgAdapter = new ImageAdapter(MainActivity.this, mImgs,
  28. mCurrentDir.getAbsolutePath());
  29. mGridView.setAdapter(mImgAdapter);
  30. mDirCount.setText(mImgs.size() + "");
  31. mDirName.setText(folderBean.getName());
  32. mDirPopupWindow.dismiss();
  33. }
  34. });
  35. }
  36. /**
  37. * 内容区域变亮
  38. */
  39. protected void lightOn() {
  40. WindowManager.LayoutParams lp = getWindow().getAttributes();
  41. lp.alpha = 1.0f;
  42. getWindow().setAttributes(lp);
  43. }
  44. /**
  45. * 内容区域变暗
  46. */
  47. protected void lightOff() {
  48. WindowManager.LayoutParams lp = getWindow().getAttributes();
  49. lp.alpha = .3f;
  50. getWindow().setAttributes(lp);
  51. }

我们初始化我们的popupWindow,设置了关闭对话框的回调,已经设置了选择不同文件夹的回调;
这里仅仅是初始化,下面看我们合适将其弹出的,其实整个Activity也就一个事件,点击弹出该对话框,所以看Activity的initEvent方法:

  1. /**
  2. * 添加点击事件
  3. */
  4. private void initEvent() {
  5. mBottomLy.setOnClickListener(new OnClickListener() {
  6. public void onClick(View v) {
  7. // 设置PopupWindow动画
  8. mDirPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim);
  9. // 设置PopupWindow的出现
  10. mDirPopupWindow.showAsDropDown(mBottomLy, 0, 0);
  11. lightOff();
  12. }
  13. });
  14. }

动画的文件就不贴了,大家自己看源码;

我们改变了GridView的适配器,以及底部的控件上的文件夹名称,文件数量等等;

好了,到此结束;整篇由于篇幅原因没有贴任何布局文件,大家自己通过源码查看;

五、总结:

1. Imageloader:

(1)Handler + Loop + Message(new Thread().start():这种方式效率低

(2) 图片的压缩

    获取图片应当显示的尺寸---> 使用options进行压缩

(3) 图片显示避免错乱

           setTag(url);

2. PopupWindow:

单独自定义一个PopupWindow继承自系统的PopupWindow。

然后处理自己的子View事件,把一些关键的回调接口和方法进行返回,让MainActivity进行设置

3. 注意:

ps:请真机测试,反正我的模拟器扫描不到图片~

ps:运行出现空指针的话,在getImages中添加判断,if(parentFile.list()==null)continue , 切记~~~具体位置,上面有说; 

源码下载:

 https://github.com/PocketBoy/hebao

Android 高级UI设计笔记06:仿微信图片选择器(转载)的更多相关文章

  1. Android 高级UI设计笔记12:ImageSwitcher图片切换器

    1. ImageSwitcher ImageSwitcher是Android中控制图片展示效果的一个控件,如:幻灯片效果...,颇有感觉啊.做相册一绝 2. 重要方法 setImageURI(Uri  ...

  2. Android 高级UI设计笔记07:RecyclerView 的详解

    1. 使用RecyclerView       在 Android 应用程序中列表是一个非常重要的控件,适用场合非常多,如新闻列表.应用列表.消息列表等等,但是从Android 一出生到现在并没有非常 ...

  3. Android 高级UI设计笔记08:Android开发者常用的7款Android UI组件(转载)

    Android开发是目前最热门的移动开发技术之一,随着开发者的不断努力和Android社区的进步,Android开发技术已经日趋成熟,当然,在Android开源社区中也涌现了很多不错的开源UI项目,它 ...

  4. Android 高级UI设计笔记21:Android SegmentView(分段选择控件)

    1. 分段控制(SegmentView) 首先我们先看看什么是SegmentView的效果,如下: 分段控制这个View控件是ios7的分段控制,和QQ消息页面顶部的效果一样,android没有这个控 ...

  5. Android 高级UI设计笔记19:PopupWindow使用详解

    1. PopupWindow使用 PopupWindow这个类用来实现一个弹出框,可以使用任意布局的View作为其内容,这个弹出框是悬浮在当前activity之上的. 2. PopupWindow使用 ...

  6. Android 高级UI设计笔记18:实现圆角图片

    1. 下面我们经常在APP中看到的圆角图片,如下: 再比如:微信聊天会话列表的头像是圆角的. 2. 下面分析一个Github的经典: (1)Github库地址: https://github.com/ ...

  7. Android 高级UI设计笔记17:Android在非UI线程中显示Toast

    1. 子线程的Toast怎么显示不出来? 因为Toast在创建的时候会依赖于一个Handler,并且一个Handler是需要有一个Looper才能够创建,而普通的线程是不会自动去创建一个Looper对 ...

  8. Android 高级UI设计笔记14:Gallery(画廊控件)之 3D图片浏览

    1. 利用Gallery组件实现 3D图片浏览器的功能,如下: 2. 下面是详细的实现过程如下: (1)这里我是测试性代码,我的图片是自己添加到res/drawable/目录下的,如下: 但是开发中不 ...

  9. Android 高级UI设计笔记09:Android如何实现无限滚动列表

    ListView和GridView已经成为原生的Android应用实现中两个最流行的设计模式.目前,这些模式被大量的开发者使用,主要是因为他们是简单而直接的实现,同时他们提供了一个良好,整洁的用户体验 ...

随机推荐

  1. 多校7 HDU5816 Hearthstone 状压DP+全排列

    多校7 HDU5816 Hearthstone 状压DP+全排列 题意:boss的PH为p,n张A牌,m张B牌.抽取一张牌,能胜利的概率是多少? 如果抽到的是A牌,当剩余牌的数目不少于2张,再从剩余牌 ...

  2. Java访问USB设备

    最近在用Java访问RDing设备,使用的是Java HID API.使用过程中发现一个问题,由于是嵌入式小白,不知道如何向USB设备发送report.于是想到可以看看自带的软件如何访问USB的.找到 ...

  3. Android JNI之JAVA与C++对象建立对称关联(JNI优化设计,确保JNI调用的稳定性)

    转载请声明:原文转自:http://www.cnblogs.com/xiezie/p/5930503.html Android JNI之JAVA与C++对象建立对称关联 1.JAVA对象持有C++对象 ...

  4. HDU-4727 The Number Off of FFF 水题

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4727 水题.. //STATUS:C++_AC_187MS_288KB #include <fu ...

  5. 32位和64位dll判断

    如何判断一个dll文件是32位还是64位? 1. 开发中经常会使用到VC的一个工具 Dependency Walker用depends.exe打开dll,文件名前有64标示的即为64位. 但是这个方式 ...

  6. av_interleaved_write_frame 网络不好的情况下返回较慢

    用libvlc做直播推流引擎在网络较差的情况下,需要关闭直播,并且重新开播.这个过程中,推流引擎重启,需要的是快速响应.实际上测试结果发现,经常会发生引擎关闭接口卡住.后来跟踪代码,定位到s_rtmp ...

  7. HDU 5752 Sqrt Bo (数论)

    Sqrt Bo 题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=5752 Description Let's define the function f ...

  8. CodeForces 455A Boredom (DP)

    Boredom 题目链接: http://acm.hust.edu.cn/vjudge/contest/121334#problem/G Description Alex doesn't like b ...

  9. nyoj 127 星际之门(一)

    星际之门(一) 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 公元3000年,子虚帝国统领着N个星系,原先它们是靠近光束飞船来进行旅行的,近来,X博士发明了星际之门 ...

  10. hibernate[版本四]知识总结

    1.hibernate是orm对象关系映射,是对jdbc的封装 2.hibernate版helloworld 2.1导入jar <dependencies> <dependency& ...