仿微信图片选择器:
一、项目整体分析:
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 , 切记~~~具体位置,上面有说;
源码下载: