上次我写了一个关于MVC框架怎么用在项目里,相关基类的封装方法。其实就是为今天的这篇文章做准备。相册,也就是图片选择器,在我们的项目中用的还是比较多的。但就我了解,多数程序员还是通过引用第三方框架实现这个功能。但是如果UI有要求,或者我们自己有什么特殊需要,可能根本无法满足。
所以今天我给大家带来自己做的相册。既给你一个完整的相册,也给你一套做相册的方法。同时我们可以从其中学到很多的知识点。
在开始之前要说明一下,这个相册要依赖之前讲的MVC框架,所以有两种选择,一种是把那个框架先搭建好,然后无感复制粘贴,就可以实现相册效果。或者自己分析,用现有框架也可以实现。
首先在res下建立anim
其中有两个类
slide_down.xml
slide_up.xml
这两个是选择文件夹的动画,在styles.xml中加入
这样动画就准备好了。接着我们准备布局几个图片
这是我准备的三张图片,左边的两张是图片右上角和文件夹选择弹框的最右边是否选中的标识图标。第三张是一个图片未加载出时的占位图片。
清单文件中我们要加入两条权限
接着我们有四个布局
activity_main.xml
这个布局文件是相册主页面的布局,代码如下
如果我们需要修改相册的样式,就在这布局中修改
然后是item_album.xml,这个是每个照片格子的布局
接着是item_popup_main.xml,这个是文件夹选择弹框的布局
最后是我们的文件夹选项弹框主布局popup_main.xml
一顿操作猛如虎,终于把我们的资源都整齐了。现在可以开始安心写java代码了。首先是我们的图片加载类
/**
* 图片加载类
* @author WaterWood
*/
public class ImageLoader {
/**
* 单例对象
*/
private static ImageLoader mInstance;
/**
* 图片缓存的核心对象
*/
private LruCache mLruCache;
/**
* 线程池
*/
private ExecutorService mThreadPool;
/**
* 默认线程个数
*/
private static final int DEAFAULT_THREAD_COUNT = 1;
/**
* 队列方式
*/
private Type mType = Type.LIFO;
/**
* 任务队列
*/
private LinkedList mTaskQueue;
/**
* 后台轮询线程
*/
private Thread mPoolThread;
/**
* 与线程绑定的Handler,对线程中的Queue发送消息
*/
private Handler mPoolThreadHandler;
/**
* UI线程的一个Handler
*/
private Handler mUIHandler;
/**
* addTask使用mPoolThreadHandler的信号量,防止mPoolThreadHandler为空
*/
private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
/**
* 是内部队列执行完,再从TaskQueue中取下一个
*/
private Semaphore mSemaphoreThreadPool;
/**
* 私有构造方法
*/
private ImageLoader(int threadCount,Type type){
init(threadCount,type);
}
/**
* 单例获取
* @return
*/
public static ImageLoader getInstance(){
if (mInstance == null){
synchronized (ImageLoader.class){
if (mInstance == null) {
mInstance = new ImageLoader(DEAFAULT_THREAD_COUNT,Type.LIFO);
}
}
}
return mInstance;
}
/**
* 单例获取
* @return
*/
public static ImageLoader getInstance(int threadCount,Type type){
if (mInstance == null){
synchronized (ImageLoader.class){
if (mInstance == null) {
mInstance = new ImageLoader(threadCount,type);
}
}
}
return mInstance;
}
/**
* 队列类型枚举
*/
public enum Type{
FIFO,LIFO
}
/**
* 一系列的初始化操作
* @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) {
e.printStackTrace();
}
}
};
mSemaphorePoolThreadHandler.release();
Looper.loop();
}
};
mPoolThread.start();
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheMemory = maxMemory/8;
mLruCache = new LruCache(cacheMemory){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight();
}
};
mThreadPool = Executors.newFixedThreadPool(threadCount);
mTaskQueue = new LinkedList();
mType = type;
mSemaphoreThreadPool = new Semaphore(threadCount);
}
/**
* 根据path加载图片
* @param path
* @param imageView
*/
public void loadImage(final String path, final ImageView imageView){
imageView.setTag(path);
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存储路径进行比较
if (imageview.getTag().toString().equals(path)){
imageview.setImageBitmap(bm);
}
}
};
}
Bitmap bm = getBitmapFromLruCache(path);
if (bm!=null){
//内存中有的情况
refreshBitmap(path,imageView,bm);
}else{
//内存中没有的情况
addTask(new Runnable(){
@Override
public void run() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ImageSize imageSize = getImageViewSize(imageView);
Bitmap bm = decodeSampledBitmapFromPath(path,imageSize.width,imageSize.height);
addBitmapToLruCache(path,bm);
refreshBitmap(path,imageView,bm);
mSemaphoreThreadPool.release();
}
}
});
}
}
private Bitmap getBitmapFromLruCache(String key){
return mLruCache.get(key);
}
private class ImgBeanHolder{
Bitmap bitmap;
ImageView imageView;
String path;
}
private synchronized void addTask(Runnable runnable){
mTaskQueue.add(runnable);
try {
if (mPoolThreadHandler==null) {
mSemaphorePoolThreadHandler.acquire();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
mPoolThreadHandler.sendEmptyMessage(0x110);
}
private Runnable getTask(){
if (mType == Type.FIFO){
return mTaskQueue.removeFirst();
}else if (mType == Type.LIFO){
return mTaskQueue.removeLast();
}
return null;
}
/**
* 计算压缩尺寸
* @param imageView
* @return
*/
private ImageSize getImageViewSize(ImageView imageView){
ImageSize imageSize = new ImageSize();
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) imageView.getLayoutParams();
DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
int width = imageView.getWidth();
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();
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;
}
private class ImageSize{
int width;
int height;
}
/**
* 将path转换为bimap带压缩
* @param path
* @param width
* @param height
* @return
*/
protected Bitmap decodeSampledBitmapFromPath(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);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(path,options);
return bitmap;
}
/**
* 计算压缩比
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
private int caculateInSampleSize(BitmapFactory.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;
}
/**
* 将bitmap加入内存缓存中
* @param path
* @param bm
*/
protected void addBitmapToLruCache(String path,Bitmap bm){
if (getBitmapFromLruCache(path) == null){
if (bm!=null){
mLruCache.put(path,bm);
}
}
}
/**
* 刷新图片
* @param path
* @param imageView
* @param bitmap
*/
private void refreshBitmap(String path,ImageView imageView,Bitmap bitmap){
Message message = Message.obtain();
ImgBeanHolder holder = new ImgBeanHolder();
holder.bitmap = bitmap;
holder.path = path;
holder.imageView = imageView;
message.obj = holder;
mUIHandler.sendMessage(message);
}
/**
* 通过反射获取任何对象的任何属性值
* @param object
* @param fieldName
* @return
*/
private static int getImageViewFieldValue(Object object,String fieldName){
try {
int value = 0;
Field field = ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true);
int fieldValue = field.getInt(object);
if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE){
value = fieldValue;
}
return value;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
}
接着是图片文件夹实体
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+1);
}
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;
}
}
图片主界面GridView的Adapter
public class ImageAdapter extends BaseAdapter {
private String mDirPath;
private List mImgPaths;
private LayoutInflater mInflater;
private Context context;
private static Set mSeletedImg = new HashSet();
public ImageAdapter(Context context, List mDatas, String dirPath){
this.context = context;
this.mDirPath = dirPath;
this.mImgPaths = mDatas;
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mImgPaths.size();
}
@Override
public Object getItem(int position) {
return mImgPaths.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null){
convertView = mInflater.inflate(R.layout.item_album,parent,false);
viewHolder = new ViewHolder();
viewHolder.mImg = convertView.findViewById(R.id.id_item_image);
viewHolder.mSelect = convertView.findViewById(R.id.id_item_select);
//这里要根据手机尺寸修改一下图片的大小
int widthAndHeight = (CommonUtils.getScreenWidth(context) - CommonUtils.dp2px(context,9))/4;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) viewHolder.mImg.getLayoutParams();
params.width = widthAndHeight;
params.height = widthAndHeight;
viewHolder.mImg.setLayoutParams(params);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
//重置状态
viewHolder.mImg.setImageResource(R.mipmap.noimage);
viewHolder.mSelect.setImageResource(R.mipmap.ic_unchoose);
viewHolder.mImg.setColorFilter(null);
ImageLoader.getInstance(3, ImageLoader.Type.LIFO).loadImage(mDirPath+"/"+mImgPaths.get(position),viewHolder.mImg);
final String filePath = mDirPath+"/"+mImgPaths.get(position);
final ViewHolder finalViewHolder = viewHolder;
viewHolder.mImg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSeletedImg.contains(filePath)){
//已经被选择
mSeletedImg.remove(filePath);
finalViewHolder.mImg.setColorFilter(null);
finalViewHolder.mSelect.setImageResource(R.mipmap.ic_unchoose);
}else{
//未被选择
mSeletedImg.add(filePath);
finalViewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
finalViewHolder.mSelect.setImageResource(R.mipmap.ic_choose);
}
// notifyDataSetChanged();
}
});
if (mSeletedImg.contains(filePath)){
viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
viewHolder.mSelect.setImageResource(R.mipmap.ic_choose);
}
return convertView;
}
private class ViewHolder{
ImageView mImg;
ImageButton mSelect;
}
}
文件夹弹框的Adapter
public class ListImageDirPopupWindow extends PopupWindow{
private int mWidth;
private int mHeight;
private View mConvertView;
private ListView mListView;
private List mDatas;
private OnDirSelectedListener mListener;
public ListImageDirPopupWindow(Context context,List datas) {
calWidthAndHeight(context);
mConvertView = LayoutInflater.from(context).inflate(R.layout.popup_main,null);
mDatas = datas;
setContentView(mConvertView);
setWidth(mWidth);
setHeight(mHeight);
setFocusable(true);
setTouchable(true);
setOutsideTouchable(true);
setBackgroundDrawable(new BitmapDrawable());
setTouchInterceptor(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE){
dismiss();
return true;
}
return false;
}
});
initView(context);
initEvent();
}
private void initView(Context context) {
mListView = mConvertView.findViewById(R.id.id_list_dir);
mListView.setAdapter(new ListDirAdapter(context,mDatas));
}
private void initEvent(){
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
if (mListener!=null){
mListener.onSelected(mDatas.get(position));
}
}
});
}
private void calWidthAndHeight(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mWidth = outMetrics.widthPixels;
mHeight = (int) (outMetrics.heightPixels*0.7);
}
private class ListDirAdapter extends ArrayAdapter{
private LayoutInflater mInflater;
private List mDatas;
public ListDirAdapter(Context context,List objects) {
super(context,0,objects);
mInflater = LayoutInflater.from(context);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null){
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_popup_main,parent,false);
holder.mImg = convertView.findViewById(R.id.id_id_dir_item_image);
holder.mDirName = convertView.findViewById(R.id.id_dir_item_name);
holder.mDirCount = convertView.findViewById(R.id.id_dir_item_count);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
FolderBean bean = getItem(position);
//重置
holder.mImg.setImageResource(R.mipmap.noimage);
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;
}
}
public interface OnDirSelectedListener{
void onSelected(FolderBean folderBean);
}
public void setmListener(OnDirSelectedListener mListener) {
this.mListener = mListener;
}
}
主界面Activity
public class MainActivity extends WaterPermissionActivity implements MainCallback {
/**
* 控件四个
*/
private GridView mGridView;
private RelativeLayout mBottomly;
private TextView mDirName;
private TextView mDirCount;
/**
* 图片数据集
*/
private List mImgs;
/**
* 对应当前文件夹的File对象
*/
private File mCurrentDir;
/**
* 当前文件夹中的文件个数
*/
private int mMaxCount;
/**
* 文件夹列表
*/
private List mFolderBeans = new ArrayList<>();
/**
* 进度条弹框
*/
private ProgressDialog mProgressDialog;
/**
* 弹框
*/
private ListImageDirPopupWindow mDirPopupWindow;
/**
* 该参数负责子线程查询图片后通知主线程更新UI
*/
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
if (msg.what == 0x110){
mProgressDialog.dismiss();//消失对话框
data2View();
initDirPopupWindow();
}
}
};
/**
* 图片适配器
*/
private ImageAdapter mImgAdapter;
@Override
protected MainModel getModelImp() {
return new MainModel(this,this);
}
@Override
protected int getContentLayoutId() {
return R.layout.activity_main;
}
@Override
protected void initWidget() {
mGridView = findViewById(R.id.mGridView);
mBottomly = findViewById(R.id.mBottomly);
mDirName = findViewById(R.id.mDirName);
mDirCount = findViewById(R.id.mDirCount);
}
@Override
protected void initData() {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
Toast.makeText(this,"当前存储卡不可用!",Toast.LENGTH_SHORT).show();
return;
}
requestPermission(READ_EXTERNAL_STORAGE);
mBottomly.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDirPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim);
mDirPopupWindow.showAsDropDown(mBottomly,0,0);
lightOff();
}
});
}
@Override
protected void doSDRead() {
requestPermission(WRITE_EXTERNAL_STORAGE);
}
@Override
protected void doSDWrite() {
mProgressDialog = ProgressDialog.show(this,null,"正在加载...");
new Thread(){
@Override
public void run() {
Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver cr = MainActivity.this.getContentResolver();
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 mDirPaths = new HashSet();
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;
if (mDirPaths.contains(dirPath)){
continue;
}else{
//当前文件夹之前没扫描到
mDirPaths.add(dirPath);
folderBean = new FolderBean();
folderBean.setDir(dirPath);
folderBean.setFirstImgPath(path);
}
if (parentFile.list() == null){
continue;
}
int picSize = parentFile.list(new FilenameFilter() {
@Override
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);
if (picSize > mMaxCount){
mMaxCount = picSize;
mCurrentDir = parentFile;
}
}
cursor.close();
mHandler.sendEmptyMessage(0x110);
}
}.start();
}
protected 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());
}
private void initDirPopupWindow(){
mDirPopupWindow = new ListImageDirPopupWindow(this,mFolderBeans);
mDirPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
lightOn();
}
});
mDirPopupWindow.setmListener(new ListImageDirPopupWindow.OnDirSelectedListener() {
@Override
public void onSelected(FolderBean folderBean) {
mCurrentDir = new File(folderBean.getDir());
mImgs = Arrays.asList(mCurrentDir.list(new FilenameFilter() {
@Override
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();
}
});
}
private void lightOn(){
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1.0f;
getWindow().setAttributes(lp);
}
private void lightOff(){
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 0.3f;
getWindow().setAttributes(lp);
}
}
Callback和Model这里先省略了。
这样我们就可以无感制作出一个完整的相册,是不是很方便。时间有限,大家可以自己分析其中的知识点。能用到的很多,之后我会一一拆出来分别写出博客以供使用。