带后台的IM即时通讯App 全程MVP手把手打造
是不是感觉界面还是挺简洁的呢,那下面就看下如何实现的吧,实现还真不难,反而很简单的。
项目总结一:实现类似qq微信表情面板无缝切换(简书地址)
实现类似qq微信表情面板无缝切换(CSDN地址)
//我们选择继承自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);
}
}
<com.mingchu.common.widget.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="1dp"
android:background="@color/white"
android:orientation="vertical"
app:comAccordTo="horizontal">
<ImageView
android:id="@+id/im_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/app_name" />
>
<View
android:id="@+id/view_shade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_alpha_112"
android:visibility="gone" />
<CheckBox
android:id="@+id/cb_select"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="end"
android:layout_margin="@dimen/len_2"
android:button="@drawable/sel_cb_circle"
android:clickable="false"
android:drawablePadding="0dp"
android:enabled="false"
android:padding="0dp"
app:buttonTint="@color/cb_gallery" />
com.mingchu.common.widget.SquareLayout>
cb_gallery.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_checked="true" />
<item android:color="@color/white_alpha_192" />
selector>
相比没什么好解释的和之前描述的问题解答一样,这里就不多做解释。直接看下面的封装吧
既然已经选择了RecyclerView来进行我们的本地相片的列表展示,是时候来封装一波RecyclerView,也就是在RecyclerView的基础上自定义view了。
名字就是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<Cursor> {
//读取图片文件的参数
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)
无非是写adapter和holder,然后inflater布局,绑定控件,然后设置数据
private class GalleryAdapter extends RecyclerAdapter<Image> {
@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<Image> {
//图片
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地址,这是从一个完整的项目中提取出来的相关总结,大家也可以下载看下,有问题可以讨论。
相册选择自定义View—>GalleryView源码
封装的RecyclerViewAdapter—>RecyclerAdapter
Adapter接口—>AdapterCallBack
Android中Cursor类的概念和用法实例:利用cursor来进行排序
Android实现获取本机中所有图片
真实项目运用-RecyclerView封装
封装那些事-RecyclerView封装实践