RecyclerView具备强大的复用机制,高效的替代了最初的ListView和GridView等组件,很大程度上实现了代码解耦,并且提供了多种默认LayoutMananger用来处理多种布局,本篇介绍使用RecyclerView来实现图片和视频列表的展示、查看、删除等功能。
1. RecyclerView重要组成部分
- Adapter:数据和视图对接的桥梁,定义了数据以怎样的视图形式进行展示。
- ViewHolder:Item视图的持有者。
- LayoutManager:管理视图渲染、Item复用回收等功能,有LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager等。
- ItemDecoration:常用来实现列表的分割线等功能。
- ItemAnimator:用来实现Item的动画功能。
2. 引入库
implementation 'androidx.recyclerview:recyclerview:1.0.0'
3. 图片和视频列表功能实现
话不多说,先上效果图。图中可以看到,支持查看图片和视频两种列表,且列表中以日期进行分类显示。
- 布局activity_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:fitsSystemWindows="true" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="60dp" android:background="@color/colorBlack"> <LinearLayout android:id="@+id/llayMediaAction" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="90dp" android:layout_marginTop="15dp" android:orientation="horizontal" android:visibility="invisible"> <Button android:id="@+id/btnDelete" android:layout_width="60dp" android:layout_height="30dp" android:background="@drawable/xml_media_btn_action" android:text="删除" android:textColor="@color/colorWhite" /> <Button android:id="@+id/btnCancel" android:layout_width="60dp" android:layout_height="30dp" android:layout_marginLeft="10dp" android:background="@drawable/xml_media_btn_action" android:text="取消" android:textColor="@color/colorWhite" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal"> <TextView android:id="@+id/txtMediaMid" android:layout_width="wrap_content" android:layout_height="wrap_content" android:enabled="false" android:text="@string/media_photo" android:textColor="#00ff00" android:textSize="20sp" /> </LinearLayout> <ImageView android:id="@+id/imgBack" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginLeft="15dp" android:layout_marginTop="15dp" android:background="@drawable/xml_media_back" /> <TextView android:id="@+id/txtMediaRight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginTop="15dp" android:layout_marginRight="15dp" android:text="@string/media_video" android:textColor="@color/colorWhite" android:textSize="20sp" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyListMedia" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="@dimen/common_split_10" android:layout_marginRight="@dimen/common_split_10" /> </LinearLayout> </LinearLayout>
- 布局media_item_title.xml(效果图中日期的布局)
-
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="40dp"> <TextView android:id="@+id/txtTitle" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="7dp" android:text="TextView" android:textColor="@color/colorFontCommon" android:textSize="20sp" /> </RelativeLayout>
- 布局media_item_photo.xml(效果图中图片的布局),布局中添加了复选框,用来做删除功能时使用。
-
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/imgPhoto" android:layout_width="200dp" android:layout_height="200dp" android:layout_marginStart="1dp" android:layout_marginLeft="1dp" android:layout_marginTop="1dp" android:layout_marginEnd="1dp" android:layout_marginRight="1dp" android:layout_marginBottom="1dp" app:srcCompat="@drawable/app_deploy" /> <CheckBox android:id="@+id/chkSelect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentBottom="true" android:visibility="invisible" /> </RelativeLayout>
- 布局media_item_video.xml(视频的布局)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<ImageView
android:id="@+id/imgVideo"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginStart="1dp"
android:layout_marginLeft="1dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_marginBottom="1dp"
app:srcCompat="@drawable/app_deploy" />
<ImageButton
android:id="@+id/btnPlay"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/btn_play" />
<CheckBox
android:id="@+id/chkSelect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:visibility="invisible" />
</RelativeLayout>
-
MediaActivity.java
由于列表中小图查看不方便,此部分中还使用Mango实现大图的滑动查看。
public class MediaActivity extends FragmentActivity
{
private MediaActivity _this;
@BindView(R.id.imgBack)
ImageView imgBack;
@BindView(R.id.txtMediaMid)
TextView txtMediaMid;
@BindView(R.id.txtMediaRight)
TextView txtMediaRight;
@BindView(R.id.btnDelete)
Button btnDelete;
@BindView(R.id.btnCancel)
Button btnCancel;
@BindView(R.id.llayMediaAction)
LinearLayout llayMediaAction;
@BindView(R.id.recyListMedia)
RecyclerView recyListMedia;
private MediaListAdapter mediaListAdapter;
//实际展示的图片列表,包括日期和图片两种数据类型
private List<Object> listMediaBean = new ArrayList<Object>();
//图片预览的数据源,不包括日期类型,所以比listMediaBean size小
private List<MultiplexImage> listMangoImage = new ArrayList<MultiplexImage>();
private AtomicBoolean readyDelete = new AtomicBoolean(false);
private AtomicInteger count = new AtomicInteger(0);
private Unbinder unbinder;
private int currentShowMedia = MediaService.Photo;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media);
unbinder = ButterKnife.bind(this);
EventBus.getDefault().register(this);
_this = this;
this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
txtMediaRight.setOnClickListener(new TxtMediaClickHandler());
imgBack.setOnClickListener(new ImgBackClickHandler());
MediaActionHandler mediaActionHandler = new MediaActionHandler();
btnDelete.setOnClickListener(mediaActionHandler);
btnCancel.setOnClickListener(mediaActionHandler);
initRecyclerView();
showMedias(MediaService.Photo);
}
@Override
protected void onResume()
{
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
EventBus.getDefault().unregister(this);
}
private void initRecyclerView()
{
mediaListAdapter = new MediaListAdapter(this, listMediaBean, readyDelete, count);
final GridLayoutManager gridLayoutManager = new GridLayoutManager(this, Constants.MediaList.SpanCount);
gridLayoutManager.setSpanSizeLookup(new MediaSpanSizeLookup());
recyListMedia.setLayoutManager(gridLayoutManager);
recyListMedia.setHasFixedSize(true);
recyListMedia.setNestedScrollingEnabled(false);
recyListMedia.setItemViewCacheSize(200);
recyListMedia.setRecycledViewPool(new RecyclerView.RecycledViewPool());
recyListMedia.setAdapter(mediaListAdapter);
}
private class MediaSpanSizeLookup extends GridLayoutManager.SpanSizeLookup
{
@Override
public int getSpanSize(int position)
{
Object obj = listMediaBean.get(position);
if(obj instanceof String)
{
return Constants.MediaList.SpanCount;
}
else if(obj instanceof MediaBean)
return 1;
return 0;
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMediaItemClick(MediaItemClickEvent event)
{
try
{
int position = event.getPosition();
int mediaType = event.getMediaType();
if(mediaType == MediaService.Photo)
{
//使用Mango实现大图活动查看
Mango.setImages(listMangoImage);
Mango.setPosition(position);
Mango.open(this);
}
else if(mediaType == MediaService.Video)
{
MediaBean videoBean = (MediaBean) listMediaBean.get(position);
String path = videoBean.getPath();
File file = new File(path);
if(!file.exists() || file.length() <= 0)
{
CustomToast.showToast(this, "视频文件不存在。");
return;
}
//调用系统播放视频
Uri uri = Uri.parse(path);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "video/mp4");
startActivity(intent);
}
else
{/*Do nothing.*/}
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onShowDelete(MessageShowDeleteEvent event)
{
if(event.isShow())
llayMediaAction.setVisibility(View.VISIBLE);
else
llayMediaAction.setVisibility(View.INVISIBLE);
}
private class MediaActionHandler implements View.OnClickListener
{
@Override
public void onClick(View v)
{
try
{
switch (v.getId())
{
case R.id.btnCancel : {
for (Object obj : listMediaBean)
{
if(obj instanceof MediaBean)
{
MediaBean bean = (MediaBean) obj;
CheckBox chkSelect = bean.getChkSelect();
if(chkSelect != null)
{
chkSelect.setChecked(false);
chkSelect.setVisibility(View.INVISIBLE);
}
}
}
readyDelete.set(false);
llayMediaAction.setVisibility(View.INVISIBLE);
mediaListAdapter.notifyDataSetChanged();
break;
}
case R.id.btnDelete : {
//删除照片或视频
boolean isSelect = false;
for (Object obj : listMediaBean)
{
if(obj instanceof MediaBean)
{
MediaBean bean = (MediaBean) obj;
CheckBox chkSelect = bean.getChkSelect();
if(chkSelect != null && chkSelect.isChecked())
{
isSelect = true;
String path = bean.getPath();
File file = new File(path);
file.delete();
}
}
}
if(!isSelect)
{
if(currentShowMedia == MediaService.Photo)
CustomToast.showToast(_this, "未选择图片");
else
CustomToast.showToast(_this, "未选择视频");
return;
}
clearPreMediaInfo();
if(currentShowMedia == MediaService.Photo)
{
showMedias(MediaService.Photo);
}
else
showMedias(MediaService.Video);
llayMediaAction.setVisibility(View.INVISIBLE);
break;
}
default:break;
}
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
}
private class ImgBackClickHandler implements View.OnClickListener
{
@Override
public void onClick(View view)
{
try
{
finish();
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
}
private class TxtMediaClickHandler implements View.OnClickListener
{
@Override
public void onClick(View view)
{
try
{
String txt = txtMediaRight.getText().toString();
if(txt.equals("图片"))
{
txtMediaMid.setText(R.string.media_photo);
txtMediaRight.setText(R.string.media_video);
showMedias(MediaService.Photo);
}
else if(txt.equals("视频"))
{
txtMediaMid.setText(R.string.media_video);
txtMediaRight.setText(R.string.media_photo);
showMedias(MediaService.Video);
}
else
{/*Do nothing.*/}
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
}
private void showMedias(int mediaType)
{
try
{
clearPreMediaInfo();
currentShowMedia = mediaType;
Map<String, List<MediaBean>> mapMedia = MediaService.findAllMedia(mediaType);
if(mapMedia == null || mapMedia.size() == 0)
{
mediaListAdapter.notifyDataSetChanged();
return;
}
Set<String> setMedia = mapMedia.keySet();
Set<String> sortSet = new TreeSet<String>(new Comparator<String>() {
@Override
public int compare(String s1, String s2)
{
return s2.compareTo(s1);
}
});
sortSet.addAll(setMedia);
Iterator<String> it = sortSet.iterator();
while (it.hasNext())
{
String key = it.next();
listMediaBean.add(key);
List<MediaBean> listBean = mapMedia.get(key);
Collections.sort(listBean, new Comparator<MediaBean>() {
@Override
public int compare(MediaBean o1, MediaBean o2)
{
return o1.getPath().compareTo(o2.getPath());
}
});
int count = listBean.size();
for (int i = 0; i < count; i++)
{
MediaBean bean = listBean.get(count - 1 - i);
listMediaBean.add(bean);
if(mediaType == MediaService.Photo)
listMangoImage.add(new MultiplexImage(bean.getPath(), bean.getPath(), MultiplexImage.ImageType.NORMAL));
}
}
mediaListAdapter.notifyDataSetChanged();
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
private void clearPreMediaInfo()
{
listMediaBean.clear();
listMangoImage.clear();
count.set(0);
readyDelete.set(false);
}
}
MediaListAdapter.java
public class MediaListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
private static final int TypeTitle = 1;
private static final int TypeImage = 2;
private static final int TypeVideo = 3;
private Context context;
private List<Object> listMediaBean;
private SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
private SimpleDateFormat newFormat = new SimpleDateFormat("yyyy年MM月dd日");
private AtomicBoolean readyDelete;
private AtomicInteger count;
public MediaListAdapter(Context context, List<Object> listMediaBean, AtomicBoolean readyDelete, AtomicInteger count)
{
this.context = context;
this.listMediaBean = listMediaBean;
this.readyDelete = readyDelete;
this.count = count;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
//此处根据数据的类型(日期、图片、还是视频),生成对应的视图
int width = parent.getWidth();
View view;
if(viewType == TypeTitle)
{
view = LayoutInflater.from(context).inflate(R.layout.media_item_title, parent, false);
ImageTitleViewHolder imageTitleViewHolder = new ImageTitleViewHolder(view);
return imageTitleViewHolder;
}
else if(viewType == TypeImage)
{
view = LayoutInflater.from(context).inflate(R.layout.media_item_photo, parent, false);
ImageListViewHolder imageListViewHolder = new ImageListViewHolder(view, width);
return imageListViewHolder;
}
else if(viewType == TypeVideo)
{
view = LayoutInflater.from(context).inflate(R.layout.media_item_video, parent, false);
VideoListViewHolder videoListViewHolder = new VideoListViewHolder(view, width);
return videoListViewHolder;
}
return null;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position)
{
try
{
if(holder instanceof ImageTitleViewHolder)
{
count.set(count.get() + 1);
ImageTitleViewHolder imageTitleViewHolder = (ImageTitleViewHolder)holder;
String title = (String) listMediaBean.get(position);
Date date = format.parse(title);
imageTitleViewHolder.txtTitle.setText(newFormat.format(date));
}
else
{
ImageView imgPhoto = null;
CheckBox chkSelect = null;
int parentWidth = 0;
MediaBean bean = (MediaBean)listMediaBean.get(position);
if(holder instanceof ImageListViewHolder)
{
ImageListViewHolder imageListViewHolder = (ImageListViewHolder)holder;
imgPhoto = imageListViewHolder.imgPhoto;
chkSelect = imageListViewHolder.chkSelect;
parentWidth = imageListViewHolder.parentWidth;
}
else if(holder instanceof VideoListViewHolder)
{
VideoListViewHolder videoListViewHolder = (VideoListViewHolder)holder;
imgPhoto = videoListViewHolder.imgVideo;
chkSelect = videoListViewHolder.chkSelect;
parentWidth = videoListViewHolder.parentWidth;
}
bean.setChkSelect(chkSelect);
//此处需注意,由于图片或视频量可能很大,因此使用Glide库进行图片和视频的处理。
Glide.with(context).load(bean.getPath()).thumbnail(0.1F).into(imgPhoto);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) imgPhoto.getLayoutParams();
int width = parentWidth / Constants.MediaList.SpanCount;
int height = width * 1200 / 1920;
params.width = width;
params.height = height;
imgPhoto.setLayoutParams(params);
ItemClickHandler clickHandler = new ItemClickHandler(count.get(), position);
ItemLongClickHandler longClickHandler = new ItemLongClickHandler(count.get(), position);
if(holder instanceof VideoListViewHolder)
{
VideoListViewHolder videoListViewHolder = (VideoListViewHolder)holder;
params = (RelativeLayout.LayoutParams) videoListViewHolder.btnPlay.getLayoutParams();
int wh = DensityUtil.dip2px(40);
params.leftMargin = (width - wh) / 2;
params.topMargin = (height - wh) / 2;
videoListViewHolder.btnPlay.setLayoutParams(params);
videoListViewHolder.btnPlay.setOnClickListener(clickHandler);
videoListViewHolder.btnPlay.setOnLongClickListener(longClickHandler);
}
imgPhoto.setOnClickListener(clickHandler);
imgPhoto.setOnLongClickListener(longClickHandler);
chkSelect.setChecked(false);
if(readyDelete.get())
chkSelect.setVisibility(View.VISIBLE);
else
chkSelect.setVisibility(View.INVISIBLE);
}
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
@Override
public int getItemViewType(int position)
{
Object obj = listMediaBean.get(position);
if(obj instanceof String)
return TypeTitle;
else
{
if(obj instanceof MediaBean)
{
MediaBean bean = (MediaBean)obj;
int type = bean.getType();
if(type == MediaService.Photo)
return TypeImage;
else if(type == MediaService.Video)
return TypeVideo;
}
}
return super.getItemViewType(position);
}
@Override
public int getItemCount()
{
return listMediaBean.size();
}
private class ItemLongClickHandler implements View.OnLongClickListener
{
private int titleCount;
private int position;
public ItemLongClickHandler(int titleCount, int position)
{
this.titleCount = titleCount;
this.position = position;
}
@Override
public boolean onLongClick(View v)
{
try
{
if(readyDelete.get())
{
MediaBean bean = (MediaBean) listMediaBean.get(position);
bean.getChkSelect().setChecked(!bean.getChkSelect().isChecked());
return true;
}
for (Object obj: listMediaBean)
{
if(obj instanceof MediaBean)
{
MediaBean bean = (MediaBean) obj;
CheckBox chkSelect = bean.getChkSelect();
if(chkSelect == null)
break;
chkSelect.setVisibility(View.VISIBLE);
}
}
MediaBean bean = (MediaBean) listMediaBean.get(position);
bean.getChkSelect().setChecked(true);
readyDelete.set(true);
EventBus.getDefault().post(new MessageShowDeleteEvent(true));
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
return true;
}
}
private class ItemClickHandler implements View.OnClickListener
{
private int titleCount;
private int position;
public ItemClickHandler(int titleCount, int position)
{
this.titleCount = titleCount;
this.position = position;
}
@Override
public void onClick(View view)
{
try
{
if(readyDelete.get())
{
MediaBean bean = (MediaBean) listMediaBean.get(position);
bean.getChkSelect().setChecked(!bean.getChkSelect().isChecked());
return;
}
int tempPos = 0;
int mediaType = 0;
switch (view.getId())
{
case R.id.imgPhoto : {
tempPos = position - titleCount;
mediaType = MediaService.Photo;
break;
}
case R.id.imgVideo : {
tempPos = position;
mediaType = MediaService.Video;
break;
}
case R.id.btnPlay : {
tempPos = position;
mediaType = MediaService.Video;
}
default:break;
}
EventBus.getDefault().post(new MediaItemClickEvent(tempPos, mediaType));
}
catch (Exception e)
{
LOGUtil.e(e.getMessage());
}
}
}
public class ImageTitleViewHolder extends RecyclerView.ViewHolder
{
private View view;
private TextView txtTitle;
public ImageTitleViewHolder(View view)
{
super(view);
this.view = view;
txtTitle = (TextView) view.findViewById(R.id.txtTitle);
}
}
public class ImageListViewHolder extends RecyclerView.ViewHolder
{
private View view;
private ImageView imgPhoto;
private CheckBox chkSelect;
private int parentWidth;
public ImageListViewHolder(View view, int parentWidth)
{
super(view);
this.view = view;
this.parentWidth = parentWidth;
imgPhoto = (ImageView) view.findViewById(R.id.imgPhoto);
chkSelect = (CheckBox) view.findViewById(R.id.chkSelect);
}
}
public class VideoListViewHolder extends RecyclerView.ViewHolder
{
private View view;
private ImageView imgVideo;
private ImageButton btnPlay;
private CheckBox chkSelect;
private int parentWidth;
public VideoListViewHolder(View view, int parentWidth)
{
super(view);
this.view = view;
this.parentWidth = parentWidth;
imgVideo = (ImageView) view.findViewById(R.id.imgVideo);
btnPlay = (ImageButton) view.findViewById(R.id.btnPlay);
chkSelect = (CheckBox) view.findViewById(R.id.chkSelect);
}
}
}
-
MediaBean.java
MediaBean用来存储列表中的数据,界面更新时,根据type字段判断数据是图片、还是视频,从而根据数据生成对应的视图。
public class MediaBean
{
private String parentDir;
private String path;
private int type;//图片或视频
private CheckBox chkSelect;//用于删除时标定是不是选中
public CheckBox getChkSelect() {
return chkSelect;
}
public void setChkSelect(CheckBox chkSelect) {
this.chkSelect = chkSelect;
}
public String getParentDir() {
return parentDir;
}
public void setParentDir(String parentDir) {
this.parentDir = parentDir;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}