仿微信图片选择器:

一、项目整体分析:

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,如下:

 package com.himi.imageloader.util;

 import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView; /**
* 图片加载类
* 这个类使用单例模式
* @author hebao
*
*/
public class ImageLoader {
private static ImageLoader mInstance;
/**
* 图片缓存的核心对象
* 管理我们所有图片加载的所需的内存
*/
private LruCache<String, Bitmap> mLruCache;
/**
* 线程池
* 执行一些我们加载图片的任务
*/
private ExecutorService mThreadPool;
/**
* 线程池中默认线程数
*/
private static final int DEAFULT_THREAD_COUNT = 1; /**
* 队列的调度方式
*/
private Type mType = Type.LIFO;
/**
* 任务队列
* 任务队列提供给线程池取任务的
*/
private LinkedList<Runnable> mTaskQueue;
/**
* 后台轮询线程
*/
private Thread mPoolThread;
/**
* 后台轮询线程的handler
*/
private Handler mPoolThreadHandler;
/**
* UI线程的handler
* 用于:更新ImageView
*/
private Handler mUIHandler;
/**
* mPoolThreadHandler的信号量,防止使用mPoolThreadHandler的时候其本身没有初始化完毕,报空指针异常
*/
private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
/**
* 任务线程信号量,保证线程池真正做到LIFO
*/
private Semaphore mSemaphoreThreadPool; /**
*
* 调度方式
*FIFO:先入先出
*LIFO:后入先出
*/ public enum Type {
FIFO,LIFO;
} private ImageLoader(int threadCount, Type type) {
init(threadCount, type);
} /**
* 初始化操作
* @param threadCount
* @param type
*/
private void init(int threadCount, Type type) {
//后台轮询线程初始化
mPoolThread = new Thread() {
@Override
public void run() {
Looper.prepare();
mPoolThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//线程池取出一个任务进行执行
mThreadPool.execute(getTask());
try {
mSemaphoreThreadPool.acquire();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
};
//释放一个信号量
mSemaphorePoolThreadHandler.release();
//Looper不断进行轮询
Looper.loop();
};
};
mPoolThread.start(); //获取我们应用的最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheMemory = maxMemory / 8;
//图片缓存初始化
mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
/**
* 测量每一个Bitmap图片的大小
*/
@Override
protected int sizeOf(String key, Bitmap value) {
// 每一个Bitmap图片的大小 = 每一行字节数 * 高度
return value.getRowBytes() * value.getHeight();
}
}; //创建线程池
mThreadPool = Executors.newFixedThreadPool(threadCount);
mTaskQueue = new LinkedList<Runnable>();
mType = type; //初始化信号量
mSemaphoreThreadPool = new Semaphore(threadCount);
} /**
* 从任务队列中取出一个方法
* @return
*/
private Runnable getTask() {
if(mType == Type.FIFO) {
return mTaskQueue.removeFirst();
}else if(mType == Type.LIFO) {
return mTaskQueue.removeLast();
}
return null;
} public static ImageLoader getInstance() {
if(mInstance == null) {
synchronized (ImageLoader.class) {
if(mInstance == null) {
mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO);
}
} }
return mInstance;
} public static ImageLoader getInstance(int threadCount, Type type) {
if(mInstance == null) {
synchronized (ImageLoader.class) {
if(mInstance == null) {
mInstance = new ImageLoader(threadCount, type);
}
} }
return mInstance;
} /**
* 根据path为ImageView是设置图片
* @param path
* @param imageView
*/
public void loadImage(final String path, final ImageView imageView ) {
imageView.setTag(path);//设置Tag主要是为了校验,防止图片的混乱
if(mUIHandler == null) {
mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//获取得到图片,为imageview回调设置图片
ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
Bitmap bm = holder.bitmap;
ImageView imageview = holder.imageView;
String path = holder.path;
/**
* 将path和getTag存储路径进行比较
* 如果不比较,就会出现我们滑动到第二张图片,但是显示的还是第一张的图片
* 这里我们绑定imageview和path就是为了防止这种情况
*/
if(imageview.getTag().toString().equals(path)) {
imageview.setImageBitmap(bm);
} };
};
}
//根据path在缓存中获取bitmap
Bitmap bm = getBitmapFromLruCache(path);
if(bm != null) {
refreashBitmap(path, imageView, bm);
} else {//内存中没有图片,加载图片到内存
addTasks(new Runnable() {
public void run() {
/**加载图片
* 图片的压缩
*/
//1. 获得图片需要显示的大小
ImageSize imageSize = getImageViewSize(imageView);
//2. 压缩图片
Bitmap bm = decodeSampleBitmapFromPath(path,imageSize.width,imageSize.height);
//3. 把图片加载到缓存 (一定要记得)
addBitmapToLruCache(path,bm);
refreashBitmap(path, imageView, bm);
//每次线程任务加载完图片,之后释放一个信号量,即:信号量-1,此时就会寻找下一个任务(根据FIFO/LIFO不同的策略取出任务)
mSemaphoreThreadPool.release();
} });
}
} public void refreashBitmap(final String path,
final ImageView imageView, Bitmap bm) {
Message message = Message.obtain();
ImgBeanHolder holder = new ImgBeanHolder();
holder.bitmap = bm;
holder.path = path;
holder.imageView = imageView; message.obj = holder;
mUIHandler.sendMessage(message);
} /**
* 将图片加入缓存LruCache
* @param path
* @param bm
*/
private void addBitmapToLruCache(String path, Bitmap bm) {
if(getBitmapFromLruCache(path) == null) {
if(bm != null) {
mLruCache.put(path, bm);
}
} } /**
* 根据图片需要显示的宽和高,对图片进行压缩
* @param path
* @param width
* @param height
* @return
*/
private Bitmap decodeSampleBitmapFromPath(String path,
int width, int height) {
//获取图片的宽和高,但是不把图片加载到内存中
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds =true;//不把图片加载到内存中
BitmapFactory.decodeFile(path, options); options.inSampleSize = caculateInSampleSize(options,width, height);//计算获取压缩比
//使用获取到的inSampleSize再次解析图片
options.inJustDecodeBounds =false;//加载图片到内存
Bitmap bitmap = BitmapFactory.decodeFile(path, options); return bitmap;
} /**
*根据需求的宽和高,以及图片实际的宽和高,计算inSampleSize
* @param options
* @param width
* @param height
* @return inSampleSize 压缩比
*/
private int caculateInSampleSize(Options options, int reqWidth, int reqHeight) {
int width = options.outWidth;
int height = options.outHeight; int inSampleSize = 1;
if(width>reqWidth || height > reqHeight) {
int widthRadio = Math.round(width*1.0f / reqWidth);
int heightRadio = Math.round(height*1.0f / reqHeight); inSampleSize = Math.max(widthRadio, heightRadio);
} return inSampleSize;
} /**
* 根据ImageView获取适当的压缩的宽和高
* @param imageView
* @return
*/
protected ImageSize getImageViewSize(ImageView imageView) {
ImageSize imageSize = new ImageSize();
DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
LayoutParams lp = imageView.getLayoutParams(); int width = imageView.getWidth();//获取imageview的实际宽度
if(width<=0) {
width = lp.width;//获取imageview在layout中声明的宽度
}
if(width<=0) {
width = getImageViewFieldValue(imageView, "mMaxWidth");//利用反射,检测获得最大值
}
if(width<=0) {
width = displayMetrics.widthPixels;
} int height = imageView.getHeight();//获取imageview的实际高度
if(height<=0) {
height = lp.height;//获取imageview在layout中声明的高度
}
if(height<=0) {
height = getImageViewFieldValue(imageView, "mMaxHeight");//利用反射,检测获得最大值
}
if(height<=0) {
height = displayMetrics.heightPixels;
} imageSize.width = width;
imageSize.height = height;
return imageSize;
}; /**
*
* 通过反射获取imageview的某个属性值
* @param object
* @param fieldName
* @return
* 由于方法getMaxHeight是API16以上的才能使用,这里我们用反射使用这个方法
*/
private static int getImageViewFieldValue(Object object, String fieldName) {
int value=0;
try {
Field field = ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true); int fieldValue = field.getInt(object);
if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
value = fieldValue;
}
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
return value;
} /**
* 添加任务到任务队列,交给线程池执行
* @param runnable
*/
@SuppressLint("NewApi")
private synchronized void addTasks(Runnable runnable) {//synchronized同步代码,防止多个线程进来出现死锁
mTaskQueue.add(runnable);
//if(mPoolThreadHandler == null) wait();
//确保我们在使用mPoolThreadHandler之前,我们初始化完毕mPoolThreadHandler(不为空),这里引入信号量
try {
if(mPoolThreadHandler == null) {
mSemaphorePoolThreadHandler.acquire();
}
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
mPoolThreadHandler.sendEmptyMessage(0x110); } /**
* 根据path在缓存中获取bitmap
* @param key
* @return
*/
private Bitmap getBitmapFromLruCache(String key) {
// TODO 自动生成的方法存根
return mLruCache.get(key);
} /**
* 压缩图片之后的宽和高
* @author Administrator
*
*/
private class ImageSize {
int width;
int height;
} private class ImgBeanHolder {
Bitmap bitmap;
ImageView imageView;
String path;
} }

三、代码实践 - UI、UI适配器

1. 布局文件设计,首先我们从美工那边获得布局设计需要的图片,如下:

来到activity_main.xml,如下:

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.himi.imageloader.MainActivity" > <!--
android:numColumns="3" 设置显示的列数
android:stretchMode="columnWidth" 缩放与列宽大小同步
android:cacheColorHint="@android:color/transparent" 自定义GridView拖动背景色
android:listSelector="@android:color/transparent" 选中item,item显示透明
--> <GridView
android:id="@+id/id_gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="@android:color/transparent"
android:horizontalSpacing="3dp"
android:listSelector="@android:color/transparent"
android:numColumns="3"
android:stretchMode="columnWidth"
android:verticalSpacing="3dp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:background="#ee000000"
android:clipChildren="true"
android:id="@+id/id_bottom_ly"
>
<TextView
android:id="@+id/id_dir_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:paddingLeft="10dp"
android:text="所有图片"
android:textColor="@android:color/white"
/>
<TextView
android:id="@+id/id_dir_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:paddingRight="10dp"
android:text="100张"
android:textColor="@android:color/white"
/> </RelativeLayout> </RelativeLayout>

显示布局效果如下:

来到item_gridview.xml,如下:

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.himi.imageloader.MainActivity" > <!-- android:scaleType="centerCrop" 防止图片变形 --> <ImageView
android:id="@+id/id_item_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="centerCrop"
android:src="@drawable/pictures_no" /> <ImageButton
android:id="@+id/id_item_select"
android:clickable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginTop="3dp"
android:layout_marginRight="3dp"
android:background="@null"
android:src="@drawable/picture_unselected"
/> </RelativeLayout>

布局效果如下:

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

 package com.himi.imageloader.bean;

 /**
* FolderBean :图片的文件夹信息类
*
* 注意:
* 用来存储当前文件夹的路径,当前文件夹包含多少张图片,以及第一张图片路径用于做文件夹的图标;
* 注:文件夹的名称,我们在set文件夹的路径的时候,自动提取,仔细看下setDir这个方法.
*
* @author hebao
*
*/ public class FolderBean {
/**
* 图片的文件夹路径
*/
private String dir; /**
* 第一张图片的路径
*/
private String firstImgPath; /**
* 文件夹的名称
*/
private String name; /**
* 图片的数量
*/
private int count; public String getDir() {
return dir;
} public void setDir(String dir) {
this.dir = dir;
int lastIndexOf = this.dir.lastIndexOf("/");
this.name = this.dir.substring(lastIndexOf);
} public String getFirstImgPath() {
return firstImgPath;
} public void setFirstImgPath(String firstImgPath) {
this.firstImgPath = firstImgPath;
} public String getName() {
return name;
} public int getCount() {
return count;
} public void setCount(int count) {
this.count = count;
} }

 3. 接下来自然要说到扫描手机图片的代码,在MainActivity中,如下:

     @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initDatas();
initEvent();
} private void initView() {
mGridView = (GridView) findViewById(R.id.id_gridView);
mBottomLy = (RelativeLayout) findViewById(R.id.id_bottom_ly);
mDirName = (TextView) findViewById(R.id.id_dir_name);
mDirCount = (TextView) findViewById(R.id.id_dir_count); } /**
* 利用ContentProvider扫描手机中的图片,此方法在运行在子线程中 完成图片的扫描,最终获得jpg最多的那个文件夹
*/
private void initDatas() { if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
Toast.makeText(this, "当前存储卡不可用", Toast.LENGTH_SHORT).show();
return;
}
/**
* 显示进度条
*/
mProgressDialog = ProgressDialog.show(this, null, "正在加载……");
/**
* 扫描手机中所有的图片,很明显这是一个耗时的操作,所以我们不能在UI线程中,采用子线程.
* 扫描得到的文件夹及其图片信息 在 List<FolderBean> mFolderBeans存储.
*/
new Thread() {
public void run() {
Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver cr = MainActivity.this.getContentResolver();
//只查询jpeg和png的图片
Cursor cursor = cr.query(mImgUri, null,
MediaStore.Images.Media.MIME_TYPE + "? or"
+ MediaStore.Images.Media.MIME_TYPE + "?",
new String[] { "image/jpeg", "image/png", },
MediaStore.Images.Media.DATE_MODIFIED); /**
* 存放已经遍历的文件夹路径,防止重复遍历
*/
Set<String> mDirPaths = new HashSet<String>();
/**
* 遍历手机图片
*/
while (cursor.moveToNext()) {
// 获取图片的路径
String path = cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media.DATA));
// 获取该图片的父路径名
File parentFile = new File(path).getParentFile();
if (parentFile == null) {
continue;
}
String dirPath = parentFile.getAbsolutePath(); FolderBean folderBean = null;
// 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~)
if (mDirPaths.contains(dirPath)) {
continue;
} else {
mDirPaths.add(dirPath);
// 初始化imageFloder
folderBean = new FolderBean(); //图片的文件夹路径
folderBean.setDir(dirPath);
//第一张图片的路径
folderBean.setFirstImgPath(path);
}
//有些图片比较诡异~~;无法显示,这里加判断,防止空指针异常
if (parentFile.list() == null) {
continue;
} int picSize = parentFile.list(new FilenameFilter() { public boolean accept(File dir, String filename) {
if (filename.endsWith(".jpg")
|| filename.endsWith(".jpeg")
|| filename.endsWith(".png")) {
return true;
}
return false;
}
}).length;
//图片的数量
folderBean.setCount(picSize);
mFolderBeans.add(folderBean);
/**
* 如果此时扫描到图片文件夹中图片数量最多,则赋值给mMaxCount,mCurrentDir
*/
if (picSize > mMaxCount) {
mMaxCount = picSize;
mCurrentDir = parentFile;
} }
//关闭游标
cursor.close();
// 通知handler扫描图片完成
mHandler.sendEmptyMessage(DATA_LOADED); };
}.start(); }

initView就不看了,都是些findViewById;

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

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

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

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

 private Handler mHandler = new Handler() {

         public void handleMessage(android.os.Message msg) {
if (msg.what == DATA_LOADED) {
mProgressDialog.dismiss();
// 绑定数据到GridView
data2View();
// 初始化PopupWindow
initDirPopupWindow();
}
}
};

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

()在MainActivity中,data2View如下:

data2View就是我们当前Activity上所有的View设置数据了。

 /**
* 为View绑定数据
*/
private void data2View() {
if (mCurrentDir == null) {
Toast.makeText(this, "未扫描到任何图片", Toast.LENGTH_SHORT).show();
return;
} mImgs = Arrays.asList(mCurrentDir.list()); /**
* 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗;
*/
mImgAdapter = new ImageAdapter(this, mImgs,
mCurrentDir.getAbsolutePath());
mGridView.setAdapter(mImgAdapter); mDirCount.setText(mMaxCount + "");
mDirName.setText(mCurrentDir.getName()); };

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

 package com.himi.imageloader;

 import java.util.HashSet;
import java.util.List;
import java.util.Set; import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ImageView; import com.himi.imageloader.util.ImageLoader;
import com.himi.imageloader.util.ImageLoader.Type; public class ImageAdapter extends BaseAdapter {
/**
* 用户选择的图片,存储为图片的完整路径
*/
private static Set<String> mSelectedImg = new HashSet<String>();
/**
* 文件夹路径
*/
private String mDirPath;
private List<String> mImgPaths;
private LayoutInflater mInflater;
//分开存储文件目录,和文件名。节省内存
public ImageAdapter(Context context, List<String> mDatas, String dirPath) {
this.mDirPath = dirPath;
this.mImgPaths = mDatas;
mInflater = LayoutInflater.from(context);
} public int getCount() {
return mImgPaths.size();
} public Object getItem(int position) {
return mImgPaths.get(position);
} public long getItemId(int position) {
return position;
} public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if(convertView == null) {
convertView = mInflater.inflate(R.layout.item_gridview, parent,false); viewHolder = new ViewHolder();
viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_item_image);
viewHolder.mSelect = (ImageButton) convertView.findViewById(R.id.id_item_select);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
} /**
* 重置状态,如果不重置第一次选中,第二次还会复用之前的,这样就会产生错乱
*/
viewHolder.mImg.setImageResource(R.drawable.pictures_no);
viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
viewHolder.mImg.setColorFilter(null); ImageLoader.getInstance(3, Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position),
viewHolder.mImg);
final String filePath = mDirPath+"/"+mImgPaths.get(position); // 设置ImageView的点击事件
viewHolder.mImg.setOnClickListener(new OnClickListener() {
// 选择,则将图片变暗,反之则反之
public void onClick(View v) {
//已经被选择
if(mSelectedImg.contains(filePath)) {
mSelectedImg.remove(filePath);
//改变Item状态,没有必要刷新显示
viewHolder.mImg.setColorFilter(null);
viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
}else {//未被选择
mSelectedImg.add(filePath);
//改变Item状态,没有必要刷新显示
viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
}
//notifyDataSetChanged();不能使用,会出现闪屏 }
}); /**
* 已经选择过的图片,显示出选择过的效果
*/
if(mSelectedImg.contains(filePath)) {
viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
} return convertView;
} private class ViewHolder {
ImageView mImg;
ImageButton mSelect;
} }

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

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

四、展现文件夹的PopupWindow

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

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

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

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

 package com.himi.imageloader;

 import java.util.List;

 import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView; import com.himi.imageloader.bean.FolderBean;
import com.himi.imageloader.util.ImageLoader; /**
* 自定义的PopupWindow
* 作用:展现文件夹信息
* @author hebao
*
*/
public class ListImageDirPopupWindow extends PopupWindow {
private int mWidth;
private int mHeight;
private View mConvertView;
private ListView mListView; private List<FolderBean> mDatas; /**
* 文件夹选中的监听器(接口)
* @author hebao
*
*/
public interface OnDirSelectedListener {
void onSelected(FolderBean folderBean);
}
public OnDirSelectedListener mListener;
public void setOnDirSelectedListener (OnDirSelectedListener mListener) {
this.mListener = mListener;
} public ListImageDirPopupWindow(Context context, List<FolderBean> datas) {
calWidthAndHeight(context); mConvertView = LayoutInflater.from(context).inflate(R.layout.popup_main, null);
setContentView(mConvertView); setWidth(mWidth);
setHeight(mHeight); //设置可触摸
setFocusable(true);
setTouchable(true);
setOutsideTouchable(true);
setBackgroundDrawable(new BitmapDrawable()); setTouchInterceptor(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_OUTSIDE){
dismiss();
return true;
}
return false;
}
}); initViews(context);
initEvent(); } private void initViews(Context context) {
mListView = (ListView) mConvertView.findViewById(R.id.id_list_dir);
mListView.setAdapter(new ListDirAdapter(context, mDatas));
} /**
* 设置监听事件
*/
private void initEvent() {
mListView.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
if(mListener != null) {
mListener.onSelected(mDatas.get(position));
} } }); } /**
* 计算popupWindow的宽度和高度
* @param context
*/
private void calWidthAndHeight(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//Andorid.util 包下的DisplayMetrics 类提供了一种关于显示的通用信息,如显示大小,分辨率和字体。
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics); mWidth = outMetrics.widthPixels;
mHeight = (int) (outMetrics.heightPixels * 0.7);
} private class ListDirAdapter extends ArrayAdapter<FolderBean> {
private LayoutInflater mInflater;
private List<FolderBean> mDatas; public ListDirAdapter(Context context,
List<FolderBean> objects) {
super(context, 0, objects);
mInflater = LayoutInflater.from(context);
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if(convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_popup_main, parent, false); holder.mImg = (ImageView) convertView.findViewById(R.id.id_id_dir_item_image);
holder.mDirName = (TextView) convertView.findViewById(R.id.id_dir_item_name);
holder.mDirCount = (TextView) convertView.findViewById(R.id.id_dir_item_count); convertView.setTag(holder);
} else {
holder =(ViewHolder) convertView.getTag();
}
FolderBean bean =getItem(position);
//重置
holder.mImg.setImageResource(R.drawable.pictures_no); //回调加载图片
ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg);
holder.mDirCount.setText(bean.getCount()+"");
holder.mDirName.setText(bean.getName());
return convertView;
} private class ViewHolder {
ImageView mImg;
TextView mDirName;
TextView mDirCount;
}
} }

好了,现在就是我们正在的popupWindow咯,布局文件夹主要是个ListView,所以在initViews里面,我们得设置它的适配器;当然了,这里的适配器依然用我们的ListDirAdapter。

 然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片;

关于交互,我们从Activity的角度去看弹出框,Activity想知道什么,只想知道选择了别的文件夹来告诉我,所以我们创建一个接口OnDirSelectedListener ,对Activity设置回调;initEvent初始化事件,如果有人设置了回调,我们就调用。

2.  接下来到MainActivity,完成MainActivity和PopupWindow的交互,如下:

上面说道,当扫描图片完成,拿到包含图片的文件夹信息列表;这个列表就是我们popupWindow所需的数据,所以我们的popupWindow的初始化在handleMessage(上面贴了handler的代码)里面:

在handleMessage里面调用 initDirPopupWindow

 /**
* 初始化展示文件夹的popupWindw
*/
private void initDirPopupWindow() {
mDirPopupWindow = new ListImageDirPopupWindow(this, mFolderBeans); mDirPopupWindow.setOnDismissListener(new OnDismissListener() { public void onDismiss() {
lightOn(); }
}); /**
* 设置选择文件夹的回调
*/
mDirPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() { public void onSelected(FolderBean folderBean) {
mCurrentDir = new File(folderBean.getDir());
mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() { public boolean accept(File dir, String filename) {
if (filename.endsWith(".jpg")
|| filename.endsWith(".jpeg")
|| filename.endsWith(".png")) {
return true;
}
return false;
}
})); mImgAdapter = new ImageAdapter(MainActivity.this, mImgs,
mCurrentDir.getAbsolutePath());
mGridView.setAdapter(mImgAdapter); mDirCount.setText(mImgs.size() + "");
mDirName.setText(folderBean.getName()); mDirPopupWindow.dismiss();
}
}); } /**
* 内容区域变亮
*/ protected void lightOn() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1.0f;
getWindow().setAttributes(lp);
} /**
* 内容区域变暗
*/
protected void lightOff() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = .3f;
getWindow().setAttributes(lp); }

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

 /**
* 添加点击事件
*/
private void initEvent() {
mBottomLy.setOnClickListener(new OnClickListener() { public void onClick(View v) {
// 设置PopupWindow动画
mDirPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim); // 设置PopupWindow的出现
mDirPopupWindow.showAsDropDown(mBottomLy, 0, 0);
lightOff(); }
}); }

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

我们改变了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. 转-CMMI在中国之混乱-CMMI比ISO9000会更惨

    CMMI在中国之混乱-CMMI比ISO9000会更惨 自己接触CMM/CMMI已经有8年时间了,现在静心回顾一下,觉得CMMI在中国的命运会比ISO9000还悲惨. 一组现象或许让你我对此结论有更深入 ...

  2. L0、L1与L2范数、核范数(转)

    L0.L1与L2范数.核范数 今天我们聊聊机器学习中出现的非常频繁的问题:过拟合与规则化.我们先简单的来理解下常用的L0.L1.L2和核范数规则化.最后聊下规则化项参数的选择问题.这里因为篇幅比较庞大 ...

  3. 快速建立Linux c/c++编译环境

    sudo apt-get install build-essential 省时又省心~

  4. Spring EL Lists, Maps example

    In this article, we show you how to use Spring EL to get value from Map and List. Actually, the way ...

  5. linux svn使用

    SVN是一种版本管理系统,前身是CVS,是开源软件的基石.即使在沟通充分的情况下,多人维护同一份源代码的一定也会出现混乱的情况,版本管理系统就是为了解决这些问题. SVN中的一些概念 : a. rep ...

  6. MFC中常用的内容

    在程序中更改静态文本内容. GetDlgItem(IDC_STATIC)->SetWindowText("欢迎"); 不用UpdateData(false); 如果提示con ...

  7. python函数介绍

    1.向函数传递信息 def greet_user(username): print('Hello,' + username.title() + '!') greet_user('jesse') 2.位 ...

  8. Python requests模块

    import requests 下面就可以使用神奇的requests模块了! 1.向网页发送数据 >>> payload = {'key1': 'value1', 'key2': [ ...

  9. Hyper-V故障转移群集

    Hyper-V故障转移群集配置完成后,在故障转移群集管理器中新建虚机:角色-虚拟机-新建虚拟机 如果直接在Hyper-V管理器中新建虚机,则不是高可用,需要在故障转移群集管理器中将其添加进来使其成为高 ...

  10. html 前台通用表单

    <!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...