本文出自博客Scott_S CSDN博客,如需转载请标明出处,尊重原创谢谢
博客地址: https://blog.csdn.net/Scott_S/article/details/84186144
github地址: https://github.com/sunquan9301/MediaEditor
图片选择
上一篇已经介绍完查询系统图片的核心函数,如果不清楚,可参看上篇文章,本篇将完成图片选择器的开发。先上效果图:
新建ChooseMediaActivity,并在AndroidManifest.xml中进行注册,在布局文件activity_choose_media.xml布局文件中,添加如下代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<com.sunquan.mediaeditor.view.TitleBar
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:s_title="choose media" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
RecycleView,在build.gradle已经添加了相关dependency,Titlebar是自己定义的拥有特定属性的公共控件。 这里附带说明如何进行自定义view。
——————————————分割线——————————————
自定义view
先构思该view需要提供哪些功能,可以拿笔清晰明了的列下来,然后从所列出的功能里圈出互斥的功能,即可变的功能,针对这些可变的功能,定义对应的字段来控制该view的行为。
具体到该案例
需要的功能:
- 左边返回back键,常态,不需要字段控制
- 中间显示的标题,颜色,大小常态,内容可变,需要一个属性控制显示的内容。
- 右面确定按钮,颜色,大小,样式常态,内容可变,需要属性控制显示的内容。
定义属性字段:
<declare-styleable name="TitleBar">
<attr name="s_title" format="string|reference" />
<attr name="s_right" format="string|reference" />
</declare-styleable>
s_title 控制显示标题的内容
s_right 控制确定按钮的标题内容
format标志字段的输入格式,这里可以直接输入字符串,也可以输入r.string.里面的引用。
因此定义一个TitleBar类,在布局文件中定义左边back按钮,中间TextView标题,和右边TextView按钮,具体步骤如下:
- inflate布局文件
- 获取初始化属性字段
- 绑定view的交互事件
- 提供接口暴露功能给外部调用
具体代码如下:
//初始化
public TitleBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
inflate(context, R.layout.view_titlebar, this);
if (attrs != null) {
getAttrs(context, attrs);
}
}
//获取属性
private void getAttrs(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TitleBar);
titleStr = ta.getString(R.styleable.TitleBar_s_title);
right = ta.getString(R.styleable.TitleBar_s_right);
initView();
bindAction();
}
//定义接口
private OnTitleBarClickListener listener;
public abstract static class OnTitleBarClickListener {
public void clickBack(){
}
public void clickConfirm(){
}
}
//绑定事件
private void bindAction() {
back.setOnClickListener(v -> {
if (listener != null) {
listener.clickBack();
}
});
confirm.setOnClickListener(v -> {
if (listener != null) {
listener.clickConfirm();
}
});
}
//在别的布局文件中使用TitleBar
<com.sunquan.mediaeditor.view.TitleBar
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:s_title="choose media" />
——————————————分割线——————————————
说完了自定义View相关内容,返回主线,我们设置RecycleView,并为其绑定Adapter。
//com.sunquan.mediaeditor.component.MediaChooseComponent.java
adapter = new MediaChooseAdapter(this);
recyclerView.setLayoutManager(new GridLayoutManager(this
, ChooseMediaActivity.GRID_COLUMS));
recyclerView.addItemDecoration(new MediaChooseItemDecoration(
ChooseMediaActivity.GRID_COLUMS, ViewUtil.dp2px(this, 1), false));
recyclerView.setAdapter(adapter);
((SimpleItemAnimator)recyclerView.getItemAnimator())
.setSupportsChangeAnimations(false);
adapter.loadData("");
- 首先新建一个MediaChooseAdapter
- 设置LayoutManager为GridLayoutManager,并标明具体几列ChooseMediaActivity.GRID_COLUMS
- 设置ItemDecoration标明每列之间的间隔dp2px方法如下:
//com.sunquan.mediaeditor.utils.ViewUtil
public static int dp2px(Context context, float dp) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
- 绑定adapter,禁止recycleview的默认动画
- loadData
loadData具体所做的事情是访问MediaChooseComponent.java并获取其查询的Medias,接下来转到MediaChooseComponent.java中的loadImageMedias方法。在次之前,定义了如下2个属性,一个线程和一个list。
private Thread imageQueryThread;
private List<MediaItem> imageMediasCache = new ArrayList<>();
在loadImageMedias方法中,主要做如下几件事情
- 判断imageQueryThread是否还在运行,如果还在运行,则中断它
if (imageQueryThread != null && imageQueryThread.isAlive()) {
imageQueryThread.interrupt();
imageQueryThread = null;
}
- 遍历缓存,并调用listener.notifyMedia()方法通知外部
for (MediaItem mediaInfo : imageMediasCache) {
if (listener != null) {
listener.notifyMedia(mediaInfo);
}
}
- 发起queryImageList Task
imageQueryThread = new Thread(() -> {
queryImageList(dir);
});
imageQueryThread.start();
- 具体queryImageList()方法可查看上一篇文章,这里稍作修改,针对查询出来的每一个MediaInfo,我们首先加入到cache list中,同时调用list.notifyMedia通知订阅方。
接下来转到MediaChooseAdapter.java类中,我们需要实现如下三个方法
//com.sunquan.mediaeditor.photo.choose.MediaChooseAdapter.java
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {}
@Override
public int getItemCount() {}
实现之前,构造函数中添加如下代码
//com.sunquan.mediaeditor.photo.choose.MediaChooseAdapter.java
public MediaChooseAdapter(Context context) {
this.context = context;
this.mItemSize = (ViewUtil.getScreenWidth(context)(ChooseMediaActivity.GRID_COLUMS - 1)
* ViewUtil.dp2px(context, 1)) / ChooseMediaActivity.GRID_COLUMS;
ME.media(context).setOnMediaChangeListener(mediaInfo -> {
if (!isExist(mediaInfo)) {
((Activity) context).runOnUiThread(() -> {
items.add(mediaInfo);
notifyItemInserted(items.size() - 1);
});
}
});
}
//com.sunquan.mediaeditor.utils.ViewUtil.java
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display d = wm.getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
return realDisplayMetrics.widthPixels;
}
首先定义了一个字段mItemSize用来标志图片显示的大小,其次订阅MediaChooseComponent.java中的MediaChange接口。针对每个MediaInfo,首先判断是否已经存在在集合中,不存在则添加进去并调用notifyItemInserted()刷新界面。由于查询Media在异步线程运行,需要调用runOnUiThread方法使之在UI线程更新界面。
//com.sunquan.mediaeditor.photo.choose.MediaChooseAdapter.java
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MediaVH(parent);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof MediaVH) {
((MediaVH) holder).bind(items.get(position), position, (MediaVH) holder);
}
}
@Override
public int getItemCount() {
return items.size();
}
接下来,在onCreateViewHolder方法里,new了一个内部类,用于初始化view显示每一个图片,在onBindViewHolder中进行数据绑定,getItemCount方法返回size就。内部类就不细说了,主要贴一下用Fresco load image的方法
//com.sunquan.mediaeditor.utils.FrescoUtil.java
public static void loadImage(SimpleDraweeView simpleDraweeView, String url) {
if (TextUtils.isEmpty(url)) {
return;
}
Uri uri = Uri.fromFile(new File(url));
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(uri)
.build();
PipelineDraweeController controller = (PipelineDraweeController) Fresco.newDraweeControllerBuilder()
.setOldController(simpleDraweeView.getController())
.setImageRequest(request)
.setAutoPlayAnimations(true)
.build();
simpleDraweeView.setController(controller);
}
最后我们在adapter中loadData方法中发起查询Media的Task就可以看到效果了。
//com.sunquan.mediaeditor.photo.choose.MediaChooseAdapter.java
public void loadData(String dir) {
items.clear();
notifyDataSetChanged();
ME.media(context).loadImageMedias(dir);
}
根据目录显示图片
接下来我们为其添加按目录显示图片功能,先上图:
在MediaChooseComponent.java类中添加如下代码:
//com.sunquan.mediaeditor.component.MediaChooseComponent.java
private Set<String> imageDirs = new HashSet<>();
private HashMap<String, List<MediaItem>> dirMapImage = new HashMap<>();
- imageDirs用于存储所有都目录
- dirMapImage 用于存储对应目录下的图片
接着对queryImageList函数进行修改,添加dir参数,在查询到每一个MediaInfo做如下处理:
//com.sunquan.mediaeditor.component.MediaChooseComponent.java
String filePath = imageCursor.getString(col_data_image);
if (TextUtils.isEmpty(filePath)) {
continue;
}
final String mediaDir = getMediaDir(filePath);
//dir cache
imageDirs.add(mediaDir);
//通知dir更新
if (dirListener != null) {
dirListener.notifyMediaDir(mediaDir);
}
//传入到dir和mediaInfo的dir不匹配则跳过
if (!TextUtils.isEmpty(dir) && !isMatch(getMediaDir(filePath), dir)) {
continue;
}
MediaItem imageInfo = ...
//加入dirMap
if (dirMapImage.get(imageInfo.dir) == null) {
List<MediaItem> medias = new ArrayList<>();
medias.add(imageInfo);
dirMapImage.put(imageInfo.dir, medias);
} else {
final List<MediaItem> mediaInfos = dirMapImage.get(imageInfo.dir);
if (!isExistInImageCache(imageInfo, mediaInfos)) {
mediaInfos.add(imageInfo);
}
}
//如果匹配dir则通知media
if (listener != null && isMatch(imageInfo.dir, dir)) {
listener.notifyMedia(imageInfo);
}
新建ChooseMediaDirActivity.java 在AndroidManifest.xml文件中注册,除了ChooseMediaActivity.java的代码外,在其布局文件中额外加入一个TextView用于显示目录title
<TextView
android:id="@+id/dir"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="20sp"
tools:text="全部照片" />
在ChooseMediaDirActivity.java添加PopupWindow用于显示目录选项
//com.sunquan.mediaeditor.photo.choose.ChooseMediaWithDirActivity.java
private MediaChooseAdapter adapter;
private MediaDirAdapter dirAdapter;
private PopupWindow popupWindow;
private boolean isShowDir;
RecyclerView rvDir = (RecyclerView) View.inflate(this, R.layout.recycleview, null);
rvDir.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
dirAdapter = new MediaDirAdapter(this);
rvDir.setAdapter(dirAdapter);
dirAdapter.setData(ME.media(this).getDirs());
popupWindow = new PopupWindow(rvDir,
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
popupWindow.setBackgroundDrawable(new ColorDrawable(
this.getResources().getColor(android.R.color.white)));
popupWindow.setOutsideTouchable(true);
new了一个RecycleView绑定在PopupWindow上,通过对应的dirAdapter提供目录数据,目录数据直接调用MediaChooseComponent.java类的getDirs()方法获得。在点击目录title的时候弹出目录列表,点击其中一个目录列表调用adapter.loadData(dir) load数据。如此即可实现根据目录显示图片列表。
总结
此篇实现了图片选择器,并扩展了根据目录进行浏览图片的功能,于此同时详细说明了自定义view的步骤。下一篇将进行图片浏览功能的实现。
更多精彩技术分享请扫码关注小码时间(XiaoMaTime)