仿抖音图片/视频混合选择器
功能介绍
搜索所有视频和图片并混合显示,视频在右下角显示时长,点击时右上角显示当前为第几个选中的图片/视频,取消时补位。
这个选择器是基于RecyclerView来实现的
下面介绍关键类
QueryProcessor
QueryProcessor是用于异步查询所有图片/视频的类
public class QueryProcessor {
//弱引用,防止内存泄漏
private WeakReference<Context> context;
private ContentResolver resolver;
private Cursor cursor = null;
public QueryProcessor(Context context) {
this.context = new WeakReference<>(context);
resolver = context.getContentResolver();
}
/**
* 查询所有图片和视频
* @param callback 查询完毕后的回调接口
*/
public void queryAll(final QueryCallback callback) {
if (context == null) {
return;
}
//开辟一个新线程执行耗时查询
new Thread(new Runnable() {
@Override
public void run() {
ArrayList<MultiModel> multiModels = new ArrayList<>();
queryImages(multiModels, callback);
queryVideos(multiModels, callback);
callback.querySuccess(multiModels);
cursor.close();
}
}).start();
}
/**
* 查询所有图片
* @param multiModels 存放查询到的图片和视频的集合
* @param callback 查询完毕后的回调接口
*/
private void queryImages(ArrayList<MultiModel> multiModels, final QueryCallback callback) {
cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null
, null, null);
if (cursor == null) {
callback.queryFailed();
return;
}
while (cursor.moveToNext()) {
MultiModel multiModel = new MultiModel();
multiModel.setPath(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)));
multiModel.setDate(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)));
multiModel.setType(0);
multiModels.add(multiModel);
}
}
/**
* 查询所有视频
* @param multiModels 存放查询到的图片和视频的集合
* @param callback 查询完毕后的回调接口
*/
private void queryVideos(ArrayList<MultiModel> multiModels, final QueryCallback callback) {
cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null
, null, null);
if (cursor == null) {
callback.queryFailed();
return;
}
while (cursor.moveToNext()) {
MultiModel multiModel = new MultiModel();
multiModel.setPath(cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATA)));
multiModel.setDate(cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATE_ADDED)));
multiModel.setDuration(cursor.getLong(cursor.getColumnIndex(MediaStore.Video.Media.DURATION)));
multiModel.setType(1);
multiModels.add(multiModel);
}
}
/**
* 查询照片和视频结束后的回调接口
*/
public interface QueryCallback {
void querySuccess(ArrayList<MultiModel> multiModels);
void queryFailed();
}
}
GalleryAdapter
在Adapter中比较麻烦的一个是子View的点击事件,另一个就是点击已经选择过的处于中间位置的图片/视频时,其后面的图片/视频右上角的编号要依次补位,而这里又涉及到局部刷新,局部刷新的关键点就是notifyItemChanged(postion, payload)
方法以及其回调onBindViewHolder(GalleryViewHolder holder, int position, List<Object> payloads)
public class GalleryAdapter extends RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder> {
private Context context;
//数据源,所有的图片/视频
private List<MultiModel> data;
//已选中的图片/视频
private List<MultiModel> selectedModels;
//记录已选中的图片在数据源中的索引位置
private List<Integer> selectedPosList;
//素材导入界面最上层显示的文字
private TextView tvSelectedModelsNum;
public GalleryAdapter(Context context, List<MultiModel> data, TextView tvSelectedModelsNum) {
this.context = context;
this.data = data;
this.tvSelectedModelsNum = tvSelectedModelsNum;
this.selectedModels = new ArrayList<>();
this.selectedPosList = new ArrayList<>();
}
@NonNull
@Override
public GalleryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.gallery_item, null);
return new GalleryViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final GalleryViewHolder holder, int position) {
MultiModel model = data.get(position);
//使用Fresco加载图片
FrescoHelper.INSTANCE.bindImage(holder.importPreview, "file://" + model.getPath()
, new ResizeOptions((int)UIUtils.dip2Px(context, 88.5f)
, (int)UIUtils.dip2Px(context, 88.5f))
, Priority.HIGH, null);
//判断是图片还是视频,若是视频需要在右下角显示时长
if (model.getType() == 1) {
holder.importVideoDuration.setVisibility(View.VISIBLE);
holder.importVideoDuration.setText(timeParse(model.getDuration()));
} else {
holder.importVideoDuration.setVisibility(View.GONE);
}
//当子项出现(或重新出现)到屏幕内时,需要判断其是否已经被选择,若已经被选择则需要将其置为对应位置;若不是被选择的图片/视频,需要取消其右上角的数字显示(在这里有坑,不判断的话在新刷出来的子项里会有之前选择过的图片的数字在新的图片的右上角,原因尚未明确)
if (selectedModels.contains(model)) {
int index = selectedModels.indexOf(model);
holder.iconContainer.setBackgroundResource(R.drawable.bg_import_icon_container);
holder.importSelectNumber.setText(context.getString(R.string.import_selected_icon_num
, index + 1));
holder.importSelectNumber.setVisibility(View.VISIBLE);
} else {
holder.importSelectNumber.setVisibility(View.GONE);
holder.importSelectNumber.setText("");
holder.iconContainer.setBackgroundResource(R.drawable.ic_import_unselected);
}
}
/**
* 三个参数的onBind方法,实际上onBind方法首先都是回调三参的onBind,若payloads里面没有数据,则再回调两个参数的onBind。notifyItemChanged方法也是如此,首先回调的是三参的onBind。
**/
@Override
public void onBindViewHolder(@NonNull GalleryViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
return;
}
Bundle payload = (Bundle) payloads.get(0);
holder.importSelectNumber.setText(context.getString(R.string.import_selected_num_change
, payload.getInt("change")));
}
@Override
public int getItemCount() {
return data.size();
}
/**
* 将以毫秒为单位的时长转化为分钟形式(如02:30为二分三十秒)
* @param duration 视频时长
* @return xx:xx
*/
private String timeParse(long duration) {
String time = "" ;
long minute = duration / 60000 ;
long seconds = duration % 60000 ;
long second = Math.round((float)seconds/1000) ;
if( minute < 10 ){
time += "0" ;
}
time += minute+":" ;
if( second < 10 ){
time += "0" ;
}
time += second ;
return time ;
}
/**
* 素材导入界面RecyclerView的ViewHolder
*/
class GalleryViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
//RecyclerView子项最外层布局
private SimpleDraweeView importPreview;
//RecyclerView子项右上角图标的container
private FrameLayout iconContainer;
//RecyclerView子项右上角点击的数字
private TextView importSelectNumber;
//RecyclerView子项右下角显示的视频的时长
private TextView importVideoDuration;
public GalleryViewHolder(View itemView) {
super(itemView);
importPreview = itemView.findViewById(R.id.iv_import_preview);
iconContainer = itemView.findViewById(R.id.import_icon_container);
importSelectNumber = itemView.findViewById(R.id.iv_import_select_number);
importVideoDuration = itemView.findViewById(R.id.tv_import_video_duration);
initListener();
}
private void initListener() {
iconContainer.setOnClickListener(this);
}
/**
* item右上角点击事件的逻辑
* @param view
*/
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.import_icon_container: {
MultiModel model = data.get(getAdapterPosition());
if (importSelectNumber.getVisibility() == View.GONE) {
selectedModels.add(model);
selectedPosList.add(getAdapterPosition());
iconContainer.setBackgroundResource(R.drawable.bg_import_icon_container);
importSelectNumber.setText(context.getString(R.string.import_selected_icon_num
, selectedModels.size()));
tvSelectedModelsNum.setText(context.getString(R.string.import_selected_count
, selectedModels.size()));
importSelectNumber.setVisibility(View.VISIBLE);
} else if (importSelectNumber.getVisibility() == View.VISIBLE){
int index = selectedModels.indexOf(model);
selectedModels.remove(model);
selectedPosList.remove(index);
iconContainer.setBackgroundResource(R.drawable.ic_import_unselected);
importSelectNumber.setText("");
tvSelectedModelsNum.setText(context.getString(R.string.import_selected_count
, selectedModels.size()));
importSelectNumber.setVisibility(View.GONE);
for (int i = index; i < selectedPosList.size(); i++) {
Bundle payload = new Bundle();
payload.putInt("change", i + 1);
//局部刷新,仅刷新被要被改动的子项
notifyItemChanged(selectedPosList.get(i), payload);
}
}
break;
}
}
}
}
}
RecyclerView的子项布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="2dp">
<FrameLayout
android:layout_width="88.5dp"
android:layout_height="88.5dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/iv_import_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<FrameLayout
android:id="@+id/import_icon_container"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
android:background="@drawable/ic_import_unselected">
<TextView
android:id="@+id/iv_import_select_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#ffffff"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/tv_import_video_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#ededed"
android:gravity="end"
android:layout_gravity="end|bottom"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
在Activity中的用法
//查询图片和视频并显示在RecyclerView上
new QueryProcessor(this).queryAll(new QueryProcessor.QueryCallback() {
@Override
public void querySuccess(ArrayList<MultiModel> multiModels) {
ImportActivity.this.galleryDataSource.addAll(multiModels);
//将所有视频和图片按照创建时间进行一次排序,由最新的到最老的
Collections.sort(ImportActivity.this.galleryDataSource);
mAdapter = new GalleryAdapter(ImportActivity.this, ImportActivity.this.galleryDataSource,
tvSelectedModelsNum);
runOnUiThread(new Runnable() {
@Override
public void run() {
mGallery.setAdapter(mAdapter);
}
});
}
@Override
public void queryFailed() {
Toast.makeText(ImportActivity.this, "视频或图片查询失败", Toast.LENGTH_SHORT).show();
}
});
效果基本和知乎开源的Matisse一致,就不放效果图了。