IM即时通讯项目讲解(二)--自定义实现图片选择GalleryView
标签(空格分隔): 开源项目
###该系列技术课程来源慕课IM实战
#####通过该课程可以学习到以下知识点
- 1、了解和开发后台项目(这个是需要长期积累的,有了这个可以说入门没问题)
- 2、学习到IM相关知识点,创建群、添加群、单聊、群聊
- 3、可以学习到数据库的相关操作(建表、表之间的关联等知识)
- 4、学习到MVP模式,更加深入了解MVP模式的架构和实现
- 5、学习到关于IM相关的优化,比如如何快速刷新界面
- 6、学习到如何进行推送等相关操作(服务器端推送,单推、群推)
- 7。。。当然还有好多的,大家不妨去了解一下,学习到知识才是最重要的
###效果图来一发
是不是感觉界面还是挺简洁的呢,那下面就看下如何实现的吧,实现还真不难,反而很简单的。###前言
项目总结一:
###进入主题 #####先大致分析一下,主要有以下几点需要我们考虑。
- 1、如何拿到手机本地图片
- 2、使用什么控件进行该展示
- 3、怎么控制照片选择状态
- 4、要考虑到复用情况,毕竟手机照片可能会有好多张
- 5、显示出来的是方形(但是加载的图片是形状不规则的)
#####上述问题解决方案
- 问题一通过LoaderManager和相关类实现
- 问题二、四
- 可以看到展示的是四列(可以自己定制),而且每一个图片的展示都市相同的,那么我们可以考虑GridView或者RecyclerView,但是从使用好感上我还是选择了RecyclerView
- 对于复用的问题,recyclerview也是做了很好的处理,内部强制使用Holder。(这里就不做详细探讨)
- 问题三
- 其实这个我们可以放个方式来问,那就是我们的一个item里面都有什么布局。
- 首先一个ImageView,然后又CheckBox,还有就是点击的阴影效果
- 问题五--->这个就需要自己定义一个显示方形的控件了,其实就是 重新onMeasure,然后在测量的时候,传入一个依据宽度的值(长和宽都是宽度就行了)
#####好了首先开始写之前的问题,我们都有了相应的解决方案,对于开发中出现的问题我们在遇到的时候当场解决吧。进入实战
###封装实战之前先来看下我们的item布局并附有相关解释 #####首先是方形控件SquareLayout
//我们选择继承自FrameLayout 重写onMeasure @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //高宽给父类 传递的测量值都是宽度 那么就可以形成基于宽度的正方形控件 if (mBaseDirection == 1) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); } else if (mBaseDirection == 2) { super.onMeasure(heightMeasureSpec, heightMeasureSpec); } else { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightSize == 0) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); return; } if (widthSize == 0) { super.onMeasure(heightMeasureSpec, heightMeasureSpec); return; } if (widthSize > heightSize) super.onMeasure(heightMeasureSpec, heightMeasureSpec); else super.onMeasure(widthMeasureSpec, widthMeasureSpec); } } 复制代码
#####布局控件
复制代码 >
cb_gallery.xml
复制代码
相比没什么好解释的和之前描述的问题解答一样,这里就不多做解释。直接看下面的封装吧
###封装RecyclerView 既然已经选择了RecyclerView来进行我们的本地相片的列表展示,是时候来封装一波RecyclerView,也就是在RecyclerView的基础上自定义view了。 #####开始自定义view的第一步,继承RecyclerView 名字就是GalleryView
// 代表直接在java代码中引用如setContentView(View) public GalleryView(Context context) { super(context); init(); }// 关联中的xml文件中当控件使用 public GalleryView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); }// 在xml引用,又要自己定义一些属性 public GalleryView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); }复制代码
#####第二步我们先来加载本地图片吧 首先在加载图片之前(为啥来个首先。。。难道还有好多么,哈哈,不是很多,但是你要定义一个bean吧,定义我们需要取哪些数据,哪些字段使我们需要的吧)
/** * 图片Image jvabean */ private static class Image { int id; //数据的id String path; //图片的路径 boolean isSelect; //图片是否选择 long date; //图片创建的日期 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Image image = (Image) o; return path != null ? path.equals(image.path) : image.path == null; } @Override public int hashCode() { return path != null ? path.hashCode() : 0; } }复制代码
加载本地并整合到集合中
/** * 用于实际数据加载的Loader */ private class LoaderCallback implements LoaderManager.LoaderCallbacks{ //读取图片文件的参数 private final String[] IMAGE_PROJECTION = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATE_ADDED}; @Override public Loader onCreateLoader(int id, Bundle args) { if (id == LOADER_ID) { return new CursorLoader(getContext(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[2] + " DESC"); } return null; } @Override public void onLoadFinished(Loader loader, final Cursor data) { //当Loader加载完成的时候回调方法 List images = new ArrayList<>(); if (data != null) { int count = data.getCount(); if (count > 0) { data.moveToFirst(); do { //getColumnIndexOrThrow(String columnName) //从零开始返回指定列名称,如果不存在将抛出IllegalArgumentException 异常 int id = data.getInt(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0])); //获取到图片本地地址 String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1])); //获取到照片的时间 long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2])); File file = new File(path); if (!file.exists() || file.length() < MIN_IMAGE_LEN) continue; //构建javabean Image image = new Image(); image.id = id; image.path = path; image.date = dateTime; //添加到集合中 images.add(image); } while (data.moveToNext()); } } //加载完本地找之后进行更新资源 updateSource(images); } @Override public void onLoaderReset(Loader loader) { //当Loader销毁或者重置 updateSource(null); } }复制代码
因为LoaderManager需要配合Activity或者Fragment,所以我们需要对外提供一个方法来传入这两个的实例
/** * 初始化方法 * * @param manager LoaderManager Loader管理器 * @param listener 选择改变监听 * @return 任何一个LOADER_ID 可以用于销毁Loader */ public int setup(LoaderManager manager, SelectedChangeListener listener) { mListener = listener; // 一个标识加载器的唯一ID 一个可选的参数以支持加载器的构建 一个LoaderManager.LoaderCallbacks的实现 manager.initLoader(LOADER_ID, null, callback); return LOADER_ID; }复制代码
相关变量
private static final int LOADER_ID = 0x0100; private static final long MIN_IMAGE_LEN = 10 * 1024; //最大的照片的大小 10MB private static final long MAX_IMAGE_COUNT = 9; //最大选择的照片的数量复制代码
关于这个方法我们在后面会有介绍updateSource(images)
#####图片已经加载到images里面了,该是我们的展示了 无非是写adapter和holder,然后inflater布局,绑定控件,然后设置数据
private class GalleryAdapter extends RecyclerAdapter { @Override protected ViewHolder onCreateViewHolder(View root, int viewType) { return new GalleryView.ViewHolder(root); } @Override protected int getItemViewType(int position, Image image) { return R.layout.cell_gallery; } } private class ViewHolder extends RecyclerAdapter.ViewHolder { //图片 private ImageView mPic; //引用 private View mShade; //checkbox private CheckBox mSelected; public ViewHolder(View itemView) { super(itemView); mPic = (ImageView) itemView.findViewById(R.id.im_image); mShade = itemView.findViewById(R.id.view_shade); mSelected = (CheckBox) itemView.findViewById(R.id.cb_select); } @Override protected void onBind(Image image) { //加载图片 Glide.with(getContext()) .load(image.path) .diskCacheStrategy(DiskCacheStrategy.NONE) .centerCrop() .placeholder(R.color.grey_200) .into(mPic); //设置选择阴影 mShade.setVisibility(image.isSelect ? VISIBLE : INVISIBLE); //是否选择 mSelected.setChecked(image.isSelect); //是否显示 未选择的图片checkbox不显示 mSelected.setVisibility(image.isSelect ? VISIBLE : INVISIBLE); }}复制代码
大家肯定会说继承的RecyclerAdapter(还有一个泛型Image是什么鬼),这个不要急,是封装的一个RecyclerView的adapter,这个在文章的结尾会给个地址的(如果篇幅过长,不要打我哈)
//四列图片 setLayoutManager(new GridLayoutManager(getContext(), 4)); setAdapter(mAdapter); //设置adapter复制代码
这个时候运行一下就是可以出现了哈,但是相关的点击逻辑我们还没有实现哦,现在抓紧时间来实现吧。想法比较简单,逻辑也不复杂哈。
首先是更新数据,也就是loader加载拿到的图片集合后更新数据
/** * 更新选择的数据 * * @param images 相册中的图片集合 */ private void updateSource(List images) { mAdapter.replace(images); }复制代码
接下来就是item的点击实现,然后实现选择和未选择的逻辑
mAdapter.setAdapterItemClickListener(new RecyclerAdapter.AdapterItemClickListener() { @Override public void onItemClick(RecyclerAdapter.ViewHolder holder, Image image) { if (onItemSelectClick(image)) { //noinspection unchecked holder.updateData(image); } } @Override public void onLongItemClick(RecyclerAdapter.ViewHolder holder, Image data) { } });复制代码
/** * item点击事件逻辑处理 * * @param image 图片Item * @return true 选择 false 未选择 */ private boolean onItemSelectClick(Image image) { boolean notifyRefresh; //判断是否已经选择过了 if (mSelectedImages.contains(image)) { //如果选择过了就移除这个image mSelectedImages.remove(image); //选择标志置为false image.isSelect = false; notifyRefresh = true; //需要刷新的标志置为true } else { //判断选择的总共大小是否超出了自定义的可选择大小 if (mSelectedImages.size() >= MAX_IMAGE_COUNT) { //Cell点击操作 如果说我们的点击是允许的 那么更新对应的Cell状态 //然后去更新界面 如果不允许点击(已经达到我们最大的选择数量) 那么就不需要刷新数据 Application.showToast(String.format( getResources().getText(R.string.label_gallery_select_max_size).toString(), MAX_IMAGE_COUNT)); //不需要刷新 notifyRefresh = false; } else { //如果不在已选择集合中 那么就添加到集合中 mSelectedImages.add(image); image.isSelect = true; //选择标志置为true notifyRefresh = true; //需要通知刷新 } } //如果是需要刷新的 添加 或者删除都需要进行刷新 if (notifyRefresh) //通知刷新 notifySelectChanged(); return notifyRefresh; }复制代码
通知刷新一下
/** * 通知选择改变的时候刷新 */ private void notifySelectChanged() { SelectedChangeListener listener = mListener; if (listener != null) listener.onSelectedCountChanged(mSelectedImages.size()); }复制代码
因为我们有一个最大选择个数,这里定义一个接口,返回我们的选择个数
/** * 图片选择监听器 */ public interface SelectedChangeListener { /** * 选择的个数监听器 * * @param count 图片个数 */ void onSelectedCountChanged(int count); }复制代码
因为我们最终还需要和界面进行交互,因此我们需要定义一个方法来让外部通过这个方法获取图片地址(简单的就是向外提供选择的图片集合的本地地址)
/** * 获取到选择过的图片的路径 * * @return 图片路径集合 */ public String[] getSelectedPath() { String[] paths = new String[mSelectedImages.size()]; int index = 0; for (Image mSelectedImage : mSelectedImages) { paths[index++] = mSelectedImage.path; } return paths; }复制代码
好了这里已经实现了,基本上也就是获取本地图片--->封装成我们需要的javabean--->使用recyclerview进行加载--->点击item--->改变item的状态(是否选中,显示checkbox)--->给外部暴露一个获取图片集合路径的方法。好了思路清晰,方法明了。实现也是比较简单。今天就到这了哈。关于安卓实现获取本机的所有图片的方法和解释,这里在参考阅读中给了地址。就不详细描述了。
###想说 鉴于本篇文章已经很长了,这里就不贴全部的代码和封装的recyclerview的代码了,我这里直接提供git地址,这是从一个完整的项目中提取出来的相关总结,大家也可以下载看下,有问题可以讨论。
###参考阅读