转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39943731,本文出自:【张鸿洋的博客】

1、概述

关于手机图片载入器,在当今像素随随便便破千万的时代。一张图片占领的内存都相当可观,作为高大尚程序员的我们。有必要掌握图片的压缩,缓存等处理,以到达纵使你有万张照片。纵使你的像素再高,我们也能正确的显示全部的图片。当然了,单纯显示图片没撒意思,我们决定高仿一下微信的图片选择器,在此,感谢微信!本篇博客将基于以下两篇博客:

Android 高速开发系列 打造万能的ListView GridView 适配器  将使用我们打造的CommonAdapter作为我们样例中GridView以及ListView的适配器

Android Handler 异步消息处理机制的妙用 创建强大的图片载入类 将使用我们自己写的ImageLoader作为我们的图片载入的核心类

假设你没看过也没关系,等看完本篇博客,能够结合以上两篇再进行充分理解一下。

好了。首先贴一下效果图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbG1qNjIzNTY1Nzkx/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbG1qNjIzNTY1Nzkx/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

动态图实在是录不出来,大家自己打开微信点击发表图片,或者聊天窗体发送图片,大致和微信的效果一样~

简单描写叙述一下:

1、默认显示图片最多的目录图片,以及底部显示图片总数量。如上图1;

2、点击底部,弹出popupWindow,popupWindow包括全部含有图片的目录,以及显示每一个目录中图片数量。如上图2。注:此时Activity变暗

3、选择不论什么目录,进入该目录图片显示,能够点击选择图片,当然了,点击已选择的图片则会取消选择。如上图3。注:选中图片变暗

当然了,最重要的效果一定流畅,不能动不动OOM~~

本人測试手机小米2s,图片6802张,未出现OOM异常,效果也是非常流畅,堪比图库~

只是存在bug在所难免。大家能够留言说下自己发现的bug;文末会提供源代码下载。

好了,以下就能够代码的征程了~

2、图片的列表页

首先对手机中图片进行扫描,拿到图片数量最多的,直接显示在GridView上。而且扫描结束。得到一个全部包括图片的目录信息的List;

对于目录信息,我们单独创建了一个Bean:

package com.zhy.bean;

public class ImageFloder
{
/**
* 图片的目录路径
*/
private String dir; /**
* 第一张图片的路径
*/
private String firstImagePath; /**
* 目录的名称
*/
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 getFirstImagePath()
{
return firstImagePath;
} public void setFirstImagePath(String firstImagePath)
{
this.firstImagePath = firstImagePath;
} public String getName()
{
return name;
}
public int getCount()
{
return count;
} public void setCount(int count)
{
this.count = count;
} }

用来存储当前目录的路径。当前目录包括多少张图片,以及第一张图片路径用于做目录的图标;注:目录的名称,我们在set目录的路径的时候。自己主动提取,细致看下setDir这种方法。

接下来就是扫描手机图片的代码了:

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); DisplayMetrics outMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
mScreenHeight = outMetrics.heightPixels; initView();
getImages();
initEvent(); } /**
* 利用ContentProvider扫描手机中的图片,此方法在执行在子线程中 完毕图片的扫描。终于获得jpg最多的那个目录
*/
private void getImages()
{
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED))
{
Toast.makeText(this, "暂无外部存储", Toast.LENGTH_SHORT).show();
return;
}
// 显示运行进度条
mProgressDialog = ProgressDialog.show(this, null, "正在载入..."); new Thread(new Runnable()
{
@Override
public void run()
{ String firstImage = null; Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = MainActivity.this
.getContentResolver(); // 仅仅查询jpeg和png的图片
Cursor mCursor = mContentResolver.query(mImageUri, null,
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=? ",
new String[] { "image/jpeg", "image/png" },
MediaStore.Images.Media.DATE_MODIFIED); Log.e("TAG", mCursor.getCount() + "");
while (mCursor.moveToNext())
{
// 获取图片的路径
String path = mCursor.getString(mCursor
.getColumnIndex(MediaStore.Images.Media.DATA)); Log.e("TAG", path);
// 拿到第一张图片的路径
if (firstImage == null)
firstImage = path;
// 获取该图片的父路径名
File parentFile = new File(path).getParentFile();
if (parentFile == null)
continue;
String dirPath = parentFile.getAbsolutePath();
ImageFloder imageFloder = null;
// 利用一个HashSet防止多次扫描同一个目录(不加这个推断,图片多起来还是相当恐怖的~~)
if (mDirPaths.contains(dirPath))
{
continue;
} else
{
mDirPaths.add(dirPath);
// 初始化imageFloder
imageFloder = new ImageFloder();
imageFloder.setDir(dirPath);
imageFloder.setFirstImagePath(path);
} int picSize = parentFile.list(new FilenameFilter()
{
@Override
public boolean accept(File dir, String filename)
{
if (filename.endsWith(".jpg")
|| filename.endsWith(".png")
|| filename.endsWith(".jpeg"))
return true;
return false;
}
}).length;
totalCount += picSize; imageFloder.setCount(picSize);
mImageFloders.add(imageFloder); if (picSize > mPicsSize)
{
mPicsSize = picSize;
mImgDir = parentFile;
}
}
mCursor.close(); // 扫描完毕。辅助的HashSet也就能够释放内存了
mDirPaths = null; // 通知Handler扫描图片完毕
mHandler.sendEmptyMessage(0x110); }
}).start(); }

ps:执行出现空指针的话,在81行的位置加入推断,if(parentFile.list()==null)continue , 切记~~~有些图片比較诡异~~;

initView就不看了。都是些findViewById;

getImages主要就是扫描图片的代码,我们开启了一个Thread进行扫描,扫描完毕以后。我们得到了图片最多目录路径(mImgDir),手机中图片数量(totalCount);以及全部包括图片目录信息(mImageFloders)

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

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

2、有了mImageFloders。就能够创建我们的popupWindow了

看一眼我们的Handler

private Handler mHandler = new Handler()
{
public void handleMessage(android.os.Message msg)
{
mProgressDialog.dismiss();
//为View绑定数据
data2View();
//初始化展示目录的popupWindw
initListDirPopupWindw();
}
};

能够看到分别干了上述的两件事:

/**
* 为View绑定数据
*/
private void data2View()
{
if (mImgDir == null)
{
Toast.makeText(getApplicationContext(), "擦,一张图片没扫描到",
Toast.LENGTH_SHORT).show();
return;
} mImgs = Arrays.asList(mImgDir.list());
/**
* 能够看到目录的路径和图片的路径分开保存,极大的降低了内存的消耗;
*/
mAdapter = new MyAdapter(getApplicationContext(), mImgs,
R.layout.grid_item, mImgDir.getAbsolutePath());
mGirdView.setAdapter(mAdapter);
mImageCount.setText(totalCount + "张");
};

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

看到这里还用到了一个Adapter。我们GridView的:

package com.zhy.imageloader;

import java.util.LinkedList;
import java.util.List; import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView; import com.zhy.utils.CommonAdapter; public class MyAdapter extends CommonAdapter<String>
{ /**
* 用户选择的图片。存储为图片的完整路径
*/
public static List<String> mSelectedImage = new LinkedList<String>(); /**
* 目录路径
*/
private String mDirPath; public MyAdapter(Context context, List<String> mDatas, int itemLayoutId,
String dirPath)
{
super(context, mDatas, itemLayoutId);
this.mDirPath = dirPath;
} @Override
public void convert(final com.zhy.utils.ViewHolder helper, final String item)
{
// 设置no_pic
helper.setImageResource(R.id.id_item_image, R.drawable.pictures_no);
// 设置no_selected
helper.setImageResource(R.id.id_item_select,
R.drawable.picture_unselected);
// 设置图片
helper.setImageByUrl(R.id.id_item_image, mDirPath + "/" + item); final ImageView mImageView = helper.getView(R.id.id_item_image);
final ImageView mSelect = helper.getView(R.id.id_item_select); mImageView.setColorFilter(null);
// 设置ImageView的点击事件
mImageView.setOnClickListener(new OnClickListener()
{
// 选择,则将图片变暗。反之则反之
@Override
public void onClick(View v)
{ // 已经选择过该图片
if (mSelectedImage.contains(mDirPath + "/" + item))
{
mSelectedImage.remove(mDirPath + "/" + item);
mSelect.setImageResource(R.drawable.picture_unselected);
mImageView.setColorFilter(null);
} else
// 未选择该图片
{
mSelectedImage.add(mDirPath + "/" + item);
mSelect.setImageResource(R.drawable.pictures_selected);
mImageView.setColorFilter(Color.parseColor("#77000000"));
} }
}); /**
* 已经选择过的图片。显示出选择过的效果
*/
if (mSelectedImage.contains(mDirPath + "/" + item))
{
mSelect.setImageResource(R.drawable.pictures_selected);
mImageView.setColorFilter(Color.parseColor("#77000000"));
} }
}

能够看到我们GridView的Adapter继承了我们的CommonAdapter。假设不知道CommonAdapter为何物,能够去看看万能适配器那篇博文;

我们如今仅仅须要实现convert方法:

在convert中,我们设置图片。设置事件等,对于图片的变暗。我们使用的是ImageView的setColorFilter ;依据Url载入图片的操作封装在helper.setImageByUrl(view,url)中,内部使用的是我们自定义的ImageLoader,包括错乱处理都已经封装了。图片策略我们使用的是LIFO后进先出;不清楚的能够看文章一開始说明的那两篇博文,对于CommonAdapter以及ImageLoader都有从无到有的具体打造过程。

到此我们的第一个Activity的全部的任务就完毕了~~~

3、展现目录的PopupWindow

如今我们要实现。点击底部的布局弹出我们的目录选择框。而且我们弹出框后面的Activity要变暗;

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

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

我们创建了一个popupWindow使用的超类:

package com.zhy.utils;

import java.util.List;

import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.PopupWindow; public abstract class BasePopupWindowForListView<T> extends PopupWindow
{
/**
* 布局文件的最外层View
*/
protected View mContentView;
protected Context context;
/**
* ListView的数据集
*/
protected List<T> mDatas; public BasePopupWindowForListView(View contentView, int width, int height,
boolean focusable)
{
this(contentView, width, height, focusable, null);
} public BasePopupWindowForListView(View contentView, int width, int height,
boolean focusable, List<T> mDatas)
{
this(contentView, width, height, focusable, mDatas, new Object[0]); } public BasePopupWindowForListView(View contentView, int width, int height,
boolean focusable, List<T> mDatas, Object... params)
{
super(contentView, width, height, focusable);
this.mContentView = contentView;
context = contentView.getContext();
if (mDatas != null)
this.mDatas = mDatas; if (params != null && params.length > 0)
{
beforeInitWeNeedSomeParams(params);
} setBackgroundDrawable(new BitmapDrawable());
setTouchable(true);
setOutsideTouchable(true);
setTouchInterceptor(new OnTouchListener()
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
if (event.getAction() == MotionEvent.ACTION_OUTSIDE)
{
dismiss();
return true;
}
return false;
}
});
initViews();
initEvents();
init();
} protected abstract void beforeInitWeNeedSomeParams(Object... params); public abstract void initViews(); public abstract void initEvents(); public abstract void init(); public View findViewById(int id)
{
return mContentView.findViewById(id);
} protected static int dpToPx(Context context, int dp)
{
return (int) (context.getResources().getDisplayMetrics().density * dp + 0.5f);
} }

也就是封装了一下popupWindow经常使用的一些设置。然后使用了类似模版方法模式,约束子类,必须实现initView,initEvent,init等方法

package com.zhy.imageloader;

import java.util.List;

import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import com.zhy.bean.ImageFloder;
import com.zhy.utils.BasePopupWindowForListView;
import com.zhy.utils.CommonAdapter;
import com.zhy.utils.ViewHolder; public class ListImageDirPopupWindow extends BasePopupWindowForListView<ImageFloder>
{
private ListView mListDir; public ListImageDirPopupWindow(int width, int height,
List<ImageFloder> datas, View convertView)
{
super(convertView, width, height, true, datas);
} @Override
public void initViews()
{
mListDir = (ListView) findViewById(R.id.id_list_dir);
mListDir.setAdapter(new CommonAdapter<ImageFloder>(context, mDatas,
R.layout.list_dir_item)
{
@Override
public void convert(ViewHolder helper, ImageFloder item)
{
helper.setText(R.id.id_dir_item_name, item.getName());
helper.setImageByUrl(R.id.id_dir_item_image,
item.getFirstImagePath());
helper.setText(R.id.id_dir_item_count, item.getCount() + "张");
}
});
} public interface OnImageDirSelected
{
void selected(ImageFloder floder);
} private OnImageDirSelected mImageDirSelected; public void setOnImageDirSelected(OnImageDirSelected mImageDirSelected)
{
this.mImageDirSelected = mImageDirSelected;
} @Override
public void initEvents()
{
mListDir.setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{ if (mImageDirSelected != null)
{
mImageDirSelected.selected(mDatas.get(position));
}
}
});
} @Override
public void init()
{
// TODO Auto-generated method stub } @Override
protected void beforeInitWeNeedSomeParams(Object... params)
{
// TODO Auto-generated method stub
} }

好了,如今就是我们正在的popupWindow咯。布局目录主要是个ListView。所以在initView里面,我们得设置它的适配器;当然了。这里的适配器依旧用我们的CommonAdapter,几行代码搞定~~

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

关于交互,我们从Activity的角度去看弹出框,Activity想知道什么,仅仅想知道选择了别的目录来告诉我。所以我们创建一个接口OnImageDirSelected,对Activity设置回调。

这里还能够这么写:就是把popupWindow的ListView发布出去。然后在Activity里面使用popupWindow.getListView(),setOnItemClickListener,这么做,个人认为不好,耦合度太高。客户简单改下需求“这个目录展示,给我们换了,换成GridView”。呵呵,此时,你须要到处去改动Activity里面的代码。由于你Activity里面居然还有个popupWindow.getListView。

好了,扯多了。初始化事件的代码:

@Override
public void initEvents()
{
mListDir.setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{ if (mImageDirSelected != null)
{
mImageDirSelected.selected(mDatas.get(position));
}
}
});
}

假设有人设置了回调。我们就调用;

到此,整个popupWindow就出炉了,接下来就看啥时候让它展示了。

4、选择不同的目录

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

在handleMessage里面调用initListDirPopupWindw

/**
* 初始化展示目录的popupWindw
*/
private void initListDirPopupWindw()
{
mListImageDirPopupWindow = new ListImageDirPopupWindow(
LayoutParams.MATCH_PARENT, (int) (mScreenHeight * 0.7),
mImageFloders, LayoutInflater.from(getApplicationContext())
.inflate(R.layout.list_dir, null)); mListImageDirPopupWindow.setOnDismissListener(new OnDismissListener()
{ @Override
public void onDismiss()
{
// 设置背景颜色变暗
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1.0f;
getWindow().setAttributes(lp);
}
});
// 设置选择目录的回调
mListImageDirPopupWindow.setOnImageDirSelected(this);
}

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

private void initEvent()
{
/**
* 为底部的布局设置点击事件。弹出popupWindow
*/
mBottomLy.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
mListImageDirPopupWindow
.setAnimationStyle(R.style.anim_popup_dir);
mListImageDirPopupWindow.showAsDropDown(mBottomLy, 0, 0); // 设置背景颜色变暗
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = .3f;
getWindow().setAttributes(lp);
}
});
}

能够看到。我们为底部布局设置点击事件。设置popupWindow的弹出与消失的动画;已经让Activity背景变暗变亮,通过改变Window alpha实现的。变亮在弹出框消息的监听里面~~

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

popupWindow弹出了,用户此时能够选择不同的目录,那么如今该看选择后的回调的代码了:

我们的Activity实现了该接口,直接看实现的方法:

	@Override
public void selected(ImageFloder floder)
{ mImgDir = new File(floder.getDir());
mImgs = Arrays.asList(mImgDir.list(new FilenameFilter()
{
@Override
public boolean accept(File dir, String filename)
{
if (filename.endsWith(".jpg") || filename.endsWith(".png")
|| filename.endsWith(".jpeg"))
return true;
return false;
}
}));
/**
* 能够看到目录的路径和图片的路径分开保存,极大的降低了内存的消耗。
*/
mAdapter = new MyAdapter(getApplicationContext(), mImgs,
R.layout.grid_item, mImgDir.getAbsolutePath());
mGirdView.setAdapter(mAdapter);
// mAdapter.notifyDataSetChanged();
mImageCount.setText(floder.getCount() + "张");
mChooseDir.setText(floder.getName());
mListImageDirPopupWindow.dismiss(); }

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

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

在此希望大家能够通过该案例,能够去其糟粕,取其精华,学习当中值得借鉴的代码风格,不要真的当作一个样例去学习~~

源代码点击下载

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

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

---------------------------------------------------------------------------------------------------------

我建了一个QQ群。方便大家交流。群号:55032675

----------------------------------------------------------------------------------------------------------

博主部分视频已经上线。假设你不喜欢枯燥的文本,请猛戳(初录。期待您的支持):

1、高仿微信5.2.1主界面及消息提醒

2、高仿QQ5.0側滑

最新文章

  1. 域名解析与多域名绑定多个Tomcat项目
  2. sublime text3 --前端工程师必备神器
  3. c#部分---结构体;
  4. 页面设计--Tree目录树
  5. Python脚本控制的WebDriver 常用操作 &lt;十&gt; 层级定位
  6. mongo 安装
  7. http请求方法与响应状态码
  8. SVN服务器从Windows迁移到Linux
  9. bzoj1306
  10. 开源项目AndroidUtil-采用Fragment实现TabHost
  11. django-extensions
  12. android网络交互之DNS优化知识整理
  13. 【HTTP 2】 序言
  14. 通过浏览器navigator判断浏览器版本或者手机类型&amp;&amp;判断微信访问
  15. linux下安装python3
  16. istio实现自动sidecar自动注入(k8s1.13.3+istio1.1.1)
  17. 配置ssl
  18. JAVA核心问题(一)反射之引言 构造函数
  19. 17秋 软件工程 团队第五次作业 Alpha Scrum11
  20. android stdio Error Could not find com.android.tools common 25.2.2

热门文章

  1. mysql 连接远程服务器
  2. webapi net 直接更改协议头
  3. 转载自——Json.net动态序列化以及对时间格式的处理
  4. SpringMVC+Shiro权限管理(转载)
  5. 实现一个Java五子棋
  6. PHP利用lua实现Redis Sorted set的zPop操作
  7. 多线程一共就俩问题:1.线程安全(访问共享数据) 2.线程通信(wait(),notify())
  8. Jsp2.0自定义标签(第三天)——EL表达式的使用
  9. Android图片突出
  10. Oracle Form's Trigger Tutorial With Sample FMB