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"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="60dp"android:background="@color/colorBlack"><LinearLayoutandroid: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"><Buttonandroid:id="@+id/btnDelete"android:layout_width="60dp"android:layout_height="30dp"android:background="@drawable/xml_media_btn_action"android:text="删除"android:textColor="@color/colorWhite" /><Buttonandroid: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><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="horizontal"><TextViewandroid: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><ImageViewandroid: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" /><TextViewandroid: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><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff"android:orientation="vertical"><androidx.recyclerview.widget.RecyclerViewandroid: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"><TextViewandroid: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"><ImageViewandroid: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" /><CheckBoxandroid: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"><ImageViewandroid: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" /><ImageButtonandroid:id="@+id/btnPlay"android:layout_width="40dp"android:layout_height="40dp"android:background="@drawable/btn_play" /><CheckBoxandroid: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;@Overrideprotected 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);}@Overrideprotected void onResume(){super.onResume();}@Overrideprotected 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{@Overridepublic 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);elsellayMediaAction.setVisibility(View.INVISIBLE);}private class MediaActionHandler implements View.OnClickListener{@Overridepublic 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, "未选择图片");elseCustomToast.showToast(_this, "未选择视频");return;}clearPreMediaInfo();if(currentShowMedia == MediaService.Photo){showMedias(MediaService.Photo);}elseshowMedias(MediaService.Video);llayMediaAction.setVisibility(View.INVISIBLE);break;}default:break;}}catch (Exception e){LOGUtil.e(e.getMessage());}}}private class ImgBackClickHandler implements View.OnClickListener{@Overridepublic void onClick(View view){try{finish();}catch (Exception e){LOGUtil.e(e.getMessage());}}}private class TxtMediaClickHandler implements View.OnClickListener{@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic 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@Overridepublic 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;}@Overridepublic 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);elsechkSelect.setVisibility(View.INVISIBLE);}}catch (Exception e){LOGUtil.e(e.getMessage());}}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}
}