RecyclerView是Android 5.0之后推出的列表类控件,具有高度的解耦性和灵活性。通过使用合适的LayoutManager,可以实现ListView、横向ListView、GridView和瀑布流列表的效果。本文将对RecyclerView的相关知识点进行详细讲解。
RecyclerView是支持库中的控件,因此在使用前需要先在build.gradle
文件中添加依赖,如下:
implementation 'com.android.support:recyclerview-v7:26.0.0-beta1'
注意: AndroidStudio在升级到3.0
版本后,不再使用compile
关键字引入依赖库,而改用implementation
关键字。
配置好依赖后,就可以正式开始使用RecyclerView了。首先,提供列表项(Item)的布局文件,本例中命名为recycler_view_item.xml
,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f36c60">
<TextView
android:id="@+id/text_view_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textSize="16sp"
android:textColor="#fff"
android:gravity="center"/>
LinearLayout>
RecyclerView和ListView类似,都是借助Adapter访问数据源,因此还需要实现自己的适配器,示例代码如下:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>{
private List dataList;//数据源
private LayoutInflater inflater;//布局解析器
public RecyclerViewAdapter(List dataList){
this.dataList = dataList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
if(inflater==null){//避免多次初始化
inflater=LayoutInflater.from(parent.getContext());
}
View itemView=inflater.inflate(R.layout.recycler_view_item,parent,false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position){
final String itemContent=dataList.get(position);
holder.textView.setText(itemContent);
}
@Override
public int getItemCount() {
return dataList.size();
}
//自定义ViewHolder
static class ViewHolder extends RecyclerView.ViewHolder{
private TextView textView;
public ViewHolder(View itemView) {
super(itemView);
textView=itemView.findViewById(R.id.text_view_recycler);
}
}
}
可以看到,RecyclerViewAdapter继承自RecyclerView.Adapter
,并通过继承RecyclerView.ViewHolder
实现了静态类ViewHolder,这是为了充分利用RecyclerView的View复用机制。
主要重写的方法有onCreateViewHolder
、onBindViewHolder
和getItemCount
,分别用于创建ViewHolder、绑定数据和返回数据总数量。
在为RecyclerView设置Adapter之前,我们先为RecyclerView设置合适的LayoutManager。LayoutManager用于管理列表项的排列方式,通过使用不同的LayoutManager,可以在不改变适配器的情况下随意改变列表排列方式,这也是RecyclerView得以解耦合的原因。示例代码如下:
LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);//设置为纵向排列
recyclerView.setLayoutManager(linearLayoutManager);//设置布局管理器
在本例中使用LinearLayoutManager
。这是一个线性的布局管理器,可以设置为横向或纵向排列,选择为纵向排列其实就实现了ListView的效果。
最后,再为RecyclerView设置好适配器就行了,示例代码如下:
//生成随机数据
private List createDataList(){
List list=new ArrayList<>();
String[] rootArray={"Java","Android","Swift","Python","Ruby"};
for(int i=0;i<60;i++){
list.add(rootArray[i%rootArray.length]+i);
}
return list;
}
List<String> dataList=createDataList();//数据源
RecyclerViewAdapter recyclerViewAdapter=new RecyclerViewAdapter(dataList);
recyclerView.setAdapter(recyclerViewAdapter);//设置适配器
最后,总结一下RecyclerView的使用步骤:
效果截图:
和ListView不同,RecyclerView并没有提供为列表项设置点击监听器的方法,因此我们需要自己去实现这一需求。
首先,在Adapter类中定义一个内部接口,并将其作为Adapter的成员变量,以及实现相应的setter方法,代码如下:
...
private ItemClickListener itemClickListener;//列表项点击监听器
//为RecyclerView设置点击监听器
public void setItemClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
//自定义的点击监听器接口
public interface ItemClickListener{
void onItemClick(String clickItem);//单击事件
void onItemLongClick(String clickItem);//长按事件
}
...
之后,在onBindViewHolder
方法中为列表项设置点击监听器,并调用ItemClickListener
中相应的方法,代码如下:
@Override
public void onBindViewHolder(ViewHolder holder, int position){
....
//为列表项设置点击监听
if(itemClickListener!=null){
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
itemClickListener.onItemClick(itemContent);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
itemClickListener.onItemLongClick(itemContent);
return true;
}
});
}
}
最后,只需要为RecyclerView设置相应的接口,就轻松地实现了监听列表项点击事件的需求,代码如下:
recyclerViewAdapter.setItemClickListener(new RecyclerViewAdapter.ItemClickListener() {
@Override
public void onItemClick(String clickItem) {
Toast.makeText(RecyclerViewActivity.this,"点击:"+clickItem,
Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(String clickItem) {
Toast.makeText(RecyclerViewActivity.this,"长按:"+clickItem,
Toast.LENGTH_SHORT).show();
}
});
在上面的例子中,我们使用LinearLayoutManager实现了类似ListView的效果。实际上,RecyclerView一共提供了三种LayoutManger,用于实现多种布局效果。下面简单介绍一下这几种布局管理器:
setOrientation
方法设置布局方向。注意:如果要实现瀑布流式布局,要求Item的高度不同(纵向排列时),否则StaggeredGridLayoutManager的显示效果和GridLayoutManager相同。
GridLayoutManager使用示例:
GridLayoutManager gridLayoutManager=new GridLayoutManager(RecyclerViewActivity.this,3);//3列
recyclerView.setLayoutManager(gridLayoutManager);
效果截图:
StaggeredGridLayoutManager使用示例:
//垂直排列、4列
StaggeredGridLayoutManager staggeredGridLayoutManager=new StaggeredGridLayoutManager(4,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
效果截图:
添加Item装饰器:
public void addItemDecoration(ItemDecoration decor);
//index:指定位置
public void addItemDecoration(ItemDecoration decor, int index);
判断RecyclerView是否在执行动画:
public boolean isAnimating();
获取指定位置的ViewHolder:
public RecyclerView.ViewHolder findViewHolderForAdapterPosition(int position);
public RecyclerView.ViewHolder findViewHolderForLayoutPosition(int position);
这两个方法都是返回指定位置的ViewHolder,如果指定位置的View还不存在,则会返回null
。这两者的区别在于,findViewHolderForAdapterPosition
以Adapter中的最新数据为基准,而findViewHolderForLayoutPosition
以已布局的旧数据为基准。在数据源发生改变而这一改变还没有更新到RecyclerView中的这一小段时间里(16ms),两者的返回结果将不同。
构造方法:
//默认纵向排列
public LinearLayoutManager(Context context);
//orientation:布局方向(横向或纵向)
//reverseLayout:是否逆序排列
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout);
如果reverseLayout
为true,那么列表将对数据源进行逆序排列。以纵向排列为例,列表将从底部开始依次加载数据,并且将首先显示列表末尾的内容而不是头部内容(感觉就像列表自动滑到了列表末尾)。
设置是否对数据逆序排列:
public void setReverseLayout(boolean reverseLayout);
设置布局方向:
//orientation:布局方向 可选值:[LinearLayoutManager.HORIZONTAL|LinearLayoutManager.VERTICAL]
public void setOrientation(int orientation);
设置是否优先展示列表尾部内容:
public void setStackFromEnd(boolean stackFromEnd);
以纵向排列为例,如果stackFromEnd设置为true,那么打开RecyclerView首先看到的就是最底部的内容,看起来就像是RecyclerView已经滚动到了最后一行;如果设置为false,就和默认状态一样,首先看到第一行的内容。
跳转到指定位置:
public void scrollToPosition(int position);
//offset:偏移量
public void scrollToPositionWithOffset(int position, int offset);
注意:这两个方法都只保证指定位置的列表项可见,并不保证该列表项处于第一个可见位置。实际上,这两个方法都会尽量只滑动最小的距离。
平滑移动到指定位置:
//recyclerView:目标recyclerView
//state:可以传入null
//position:指定位置
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,int position)
和scrollToPosition
方法不同,这个方法可以实现平滑移动,因此移动过程不会显得那么突兀。
获取可见的列表项:
public int findFirstVisibleItemPosition();//获取第一个可见的列表项位置
public int findFirstCompletelyVisibleItemPosition();//获取第一个完整可见的列表项位置
public int findLastVisibleItemPosition();//获取最后一个可见的列表项位置
public int findLastCompletelyVisibleItemPosition();//获取最后一个完整可见的列表项位置
构造方法:
//默认纵向排列
//spanCount:列数
public GridLayoutManager(Context context, int spanCount);
//orientation:排列方向(横向或纵向)
//spanCount:行数或列数(取决于排列方向)
//reverseLayout:是否倒序排列
public GridLayoutManager(Context context, int spanCount, int orientation,boolean reverseLayout);
注意:如果orientation为纵向,spanCount就代表列数;如果orientation为横向,spanCount就代表行数。
设置行数和列数:
public void setSpanCount(int spanCount);
GridLayoutManger
是LinearLayoutManager
的子类,因此继承了LinearLayoutManager的所有方法,这里不再赘述。不过要注意,GridLayoutManger并不支持setStackFromEnd
方法。
构造方法:
//orientation:排列方向(横向或纵向)
//spanCount:行数或列数(取决于排列方向)
public StaggeredGridLayoutManager(int spanCount, int orientation);
注意:如果orientation为纵向,spanCount就代表列数;如果orientation为横向,spanCount就代表行数。
其他方法:
public void setOrientation(int orientation);//设置布局方向
public void setSpanCount(int spanCount);//设置行数或列数
public void setReverseLayout(boolean reverseLayout);//设置是否对数据逆序排列
public void scrollToPosition(int position);//跳转到指定位置
public void scrollToPositionWithOffset(int position, int offset);//带偏移量跳转到指定位置
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,int position)//平滑移动到指定位置
public int findFirstVisibleItemPosition();//获取第一个可见的列表项位置
public int findFirstCompletelyVisibleItemPosition();//获取第一个完整可见的列表项位置
public int findLastVisibleItemPosition();//获取最后一个可见的列表项位置
public int findLastCompletelyVisibleItemPosition();//获取最后一个完整可见的列表项位置
在实际开发中,列表项可能并不是只有一种布局方式。通过重写Adapter的getItemViewType
方法,可以在不同的情形下构建合适的布局。此外,通过这种方式还可以为RecyclerView设置列表头和列表尾,这时只需要将列表头和列表尾视为两种独立的布局方式即可。在这里,将介绍如何实现一个简单的多布局列表,最终的效果如下:
在本例中,主要有两种列表项,即标题项和内容项。因此,准备两个对应的布局文件,分别命名为recycler_view_multi_title.xml
和recycler_view_multi_item.xml
,代码如下:
recycler_view_multi_title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp" />
LinearLayout>
recycler_view_multi_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_image"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:textAllCaps="false"
android:textSize="16sp"
android:textColor="#000000"/>
LinearLayout>
此外,也为列表头和列表尾准备两个布局文件,本例中命名为recycler_view_header.xml
和recycler_view_footer.xml
,代码如下:
recycler_view_header.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/recycler_view_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginBottom="8dp"
android:textSize="20sp"
android:text="HeaderView"/>
LinearLayout>
recycler_view_footer.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/recycler_view_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:textSize="20sp"
android:text="FooterView"/>
LinearLayout>
对于不同的布局而言,应该使用不同的实体类。在本例中,有两种列表项,因此需要两个实体类。首先可以建立一个基类,本例中命名为BaseMultiBean
,代码如下:
public abstract class BaseMultiBean {
public static final int TYPE_TITLE=0;//标题项
public static final int TYPE_ITEM=1;//内容项
protected int type;//类型
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
可以看到,基类中主要是封装了实体的类型属性,这一属性将用于确定要使用的列表项布局。然后,再建立两个继承自基类的实体类,分别对应标题项和内容项,本例中命名为TitleBean
和ItemBean
,代码如下:
TitleBean
public class TitleBean extends BaseMultiBean{
private String title;
public TitleBean(String title) {
this.title = title;
this.type=TYPE_TITLE;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
ItemBean
public class ItemBean extends BaseMultiBean{
private int imageRes;//图片资源
private String content;//内容
public ItemBean(int imageRes, String content) {
this.imageRes = imageRes;
this.content = content;
this.type=TYPE_ITEM;
}
public int getImageRes() {
return imageRes;
}
public void setImageRes(int imageRes) {
this.imageRes = imageRes;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
有了布局和实体类,就可以开始着手创建适配器了,本例中命名为StyleRecyclerViewAdapter
,代码如下:
public class StyleRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
public static final int TYPE_TITLE=0;//标题形式的列表项
public static final int TYPE_CONTENT=1;//内容形式的列表项
public static final int TYPE_HEADER=2;//列表头
public static final int TYPE_FOOTER=3;//列表尾
private View headerView;//头部View
private View footerView;//尾部View
private int headerCount;//头部View数量(0或1)
private List dataList;//数据源
private LayoutInflater inflater;//布局解析器
public StyleRecyclerViewAdapter(List dataList) {
this.dataList = dataList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(inflater==null){//只初始化一次
inflater=LayoutInflater.from(parent.getContext());
}
switch (viewType){//根据布局类型创建合适的ViewHolder
case TYPE_HEADER:
return new HeaderFooterViewHolder(headerView);
case TYPE_FOOTER:
return new HeaderFooterViewHolder(footerView);
case TYPE_TITLE:
View titleView=inflater.inflate(R.layout.recycler_view_multi_title,parent,false);
return new TitleViewHolder(titleView);
case TYPE_CONTENT:
View contentView=inflater.inflate(R.layout.recycler_view_multi_item,parent,false);
return new ContentViewHolder(contentView);
default:break;
}
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType=getItemViewType(position);
if(viewType==TYPE_TITLE){//为标题形式的列表项绑定数据
TitleBean titleBean= (TitleBean) getItem(position);
TitleViewHolder titleViewHolder= (TitleViewHolder) holder;
titleViewHolder.titleView.setText(titleBean.getTitle());
}
if(viewType==TYPE_CONTENT){//为内容形式的列表项绑定数据
ItemBean itemBean= (ItemBean) getItem(position);
ContentViewHolder contentViewHolder= (ContentViewHolder) holder;
contentViewHolder.itemImageView.setImageResource(itemBean.getImageRes());
contentViewHolder.itemContentView.setText(itemBean.getContent());
}
}
@Override
public int getItemCount() {//计算列表项的真正数量
int count=dataList.size();
if(headerView!=null){
count++;
}
if(footerView!=null){
count++;
}
return count;//返回列表头、列表尾和列表项的总数量
}
@Override
public int getItemViewType(int position) {
if(headerView!=null&&position==0){
return TYPE_HEADER;
}
if(footerView!=null&&position==headerCount+dataList.size()){
return TYPE_FOOTER;
}
BaseMultiBean baseMultiBean=dataList.get(position-headerCount);
return baseMultiBean.getType();
}
//设置列表头
public void setHeaderView(View headerView){
this.headerView=headerView;
headerCount=1;
}
//移除列表头
public void removeHeaderView(){
headerView=null;
headerCount=0;
}
//设置列表尾
public void setFooterView(View footerView){
this.footerView=footerView;
}
//移除列表尾
public void removeFooterView(){
footerView=null;
}
//获取数据源中的真实数据(避免HeaderView的影响)
private BaseMultiBean getItem(int position){
return dataList.get(position-headerCount);
}
//内容Item的ViewHolder
static class ContentViewHolder extends RecyclerView.ViewHolder{
private TextView itemContentView;
private ImageView itemImageView;
public ContentViewHolder(View itemView) {
super(itemView);
itemContentView=itemView.findViewById(R.id.item_content);
itemImageView=itemView.findViewById(R.id.item_image);
}
}
//标题Item的ViewHolder
static class TitleViewHolder extends RecyclerView.ViewHolder{
private TextView titleView;
public TitleViewHolder(View itemView) {
super(itemView);
titleView=itemView.findViewById(R.id.item_title);
}
}
//头部和尾部布局的ViewHolder
static class HeaderFooterViewHolder extends RecyclerView.ViewHolder{
public HeaderFooterViewHolder(View itemView) {
super(itemView);
}
}
}
可以看到,我们为标题形式的列表项、内容形式的列表项、列表头/尾分别定义了ViewHolder类,并在onCreateViewHolder
方法中根据viewType
返回对应的ViewHolder对象。而在onBindViewHolder
方法中,则根据viewType
的值进行数据绑定。
注意:在获取列表项对象时,要排除HeaderView对position的影响,即当HeaderView存在时让position减去1。
完成前面的准备工作后,就可以着手为RecyclerView设置适配器了,代码如下:
//初始化列表头和列表尾
headerView=LayoutInflater.from(this).inflate(R.layout.recycler_view_header,null);
footerView=LayoutInflater.from(this).inflate(R.layout.recycler_view_footer,null);
//初始化多布局的RecyclerView
List multiDataList=new ArrayList<>();
multiDataList.add(new TitleBean("第一个区域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《小王子》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《狮子王》"));
multiDataList.add(new TitleBean("第二个区域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《资本论》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《三体》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《孤独的进化者》"));
styleRecyclerViewAdapter=new StyleRecyclerViewAdapter(multiDataList);
//设置列表头和列表尾
styleRecyclerViewAdapter.setHeaderView(headerView);
styleRecyclerViewAdapter.setFooterView(footerView);
//设置布局管理器和适配器
LinearLayoutManager styleLayoutManager=new LinearLayoutManager(this);
styleRecyclerView.setLayoutManager(styleLayoutManager);
styleRecyclerView.setAdapter(styleRecyclerViewAdapter);
上文介绍了添加列表头和列表尾的方法,但针对的只是垂直排列的LinearLayoutManager
。如果使用GridLayoutManager或StaggeredGridLayoutManager,列表头/尾就会显示异常。因此针对这两种管理器,还需要使用额外的布局措施。
GridLayoutManager
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager();
//针对网格型的布局管理器进行额外处理,避免头/尾布局显示异常
if(layoutManager instanceof GridLayoutManager){
final GridLayoutManager gridLayoutManager= (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup=gridLayoutManager
.getSpanSizeLookup();//保存旧的布局方式
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int viewType=getItemViewType(position);
if(viewType==TYPE_HEADER||viewType==TYPE_FOOTER){
return gridLayoutManager.getSpanCount();//返回当前网格的列数(即让列表头/尾占据一行)
}
return spanSizeLookup.getSpanSize(position);
}
});
}
}
针对GridLayoutManager,需要重写RecyclerView.Adapter
的onAttachedToRecyclerView
方法,并在显示列表头/尾的时候让其占据整行,就可以保证列表头/为尾正常显示。
StaggeredGridLayoutManager
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
int viewType=holder.getItemViewType();
if(viewType==TYPE_HEADER||viewType==TYPE_FOOTER){
ViewGroup.LayoutParams layoutParams=holder.itemView.getLayoutParams();
//针对瀑布流式的布局管理器进行额外处理,避免头/尾布局显示异常
if(layoutParams instanceof StaggeredGridLayoutManager.LayoutParams){
StaggeredGridLayoutManager.LayoutParams staggerLayoutParams=
(StaggeredGridLayoutManager.LayoutParams) layoutParams;
staggerLayoutParams.setFullSpan(true);//列表头/尾占据一行
}
}
}
针对StaggeredGridLayoutManager,需要重写RecyclerView.Adapter
的onViewAttachedToWindow
方法,并在显示列表头/尾的时候让其占据整行,就可以保证列表头/为尾正常显示。
除了使用notifyDatasetChanged
方法通知整个列表刷新外,RecyclerView.Adapter
还提供了多个局部刷新的方法,说明如下:
通知指定位置的Item已经改变:
public final void notifyItemChanged(int position);
public final void notifyItemChanged(int position, Object payload);
这里需要重点说明payload
参数的作用,简单来说就是实现列表项的局部更新。在很多情况下,一个列表项中可能存在多个View,典型的例子如朋友圈中的一条动态,就有图片、头像、点赞、评论等多个组成部分。如果只是点赞数发生了变化,就没有必要更新整个列表项,而只需更新点赞区域即可。此时,只需要为payload
传入一个不为null的参数,就可以做到局部更新。
以上文介绍的多布局RecyclerView为例,我们来实现局部更新内容列表项的文字部分。首先,重写ViewHolder中的onBindViewHolder(RecyclerView.ViewHolder holder,int position,List
方法,这个方法会在onBindViewHolder(RecyclerView.ViewHolder holder, int position)
方法之前调用。示例代码如下:
//在这个方法中实现Item的局部更新(比如只更新ViewHolder中的一个View)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List
这个方法中的payloads参数是一个不为null的List,里面就包含在notifyItemChanged
方法中传入的payload参数。通过判断payloads是否为空,就知道是否需要进行局部更新了。
随后,在代码中调用相应的notifyItemChanged方法,并传入payload参数,示例代码如下:
ItemBean itemBean= (ItemBean) multiDataList.get(2);
itemBean.setContent("《通过局部更新获得的内容》");
multiDataList.set(2,itemBean);
//这里的payload用于标识要更新的列表项类型
styleRecyclerViewAdapter.notifyItemChanged(3,"TYPE_CONTENT");
注意:如果不使用局部更新的方式,即使列表项中的图片并未发生改变,在刷新过程中图片区域依旧会出现短暂的闪烁现象,使用局部更新就可以解决这一问题。
通知指定范围内的Item已经改变:
//itemCount:改变的Item数量
public final void notifyItemRangeChanged(int positionStart, int itemCount);
public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload);
payload
参数的作用上面已经说明了,这里不再赘述。
通知有新的数据插入:
public final void notifyItemInserted(int position);
public final void notifyItemRangeInserted(int positionStart, int itemCount);
效果截图:
通知有数据被移除:
public final void notifyItemRangeRemoved(int positionStart, int itemCount);
public final void notifyItemRemoved(int position);
效果截图:
通知有Item发生了移动:
public final void notifyItemMoved(int fromPosition, int toPosition);
以上这些方法都只会对RecyclerView进行局部刷新,优化了运行效率,同时也会触发动画效果,大幅度改善了用户体验。
注意:以上这些局部刷新方法中的position
位置参数应该传入正确的值,否则可能导致RecyclerView显示异常。
调用RecyclerView的setItemAnimator
方法就可以设置动画效果,这个方法原型如下:
public void setItemAnimator(ItemAnimator animator);
参数的类型是RecyclerView.ItemAnimator
,系统已经提供了一个默认实现类DefaultItemAnimator
,使用方式如下:
recyclerView.setItemAnimator(new DefaultItemAnimator());//设置默认的动画效果
除此之外,还可以通过继承RecyclerView.ItemAnimator
实现自定义动画效果,这里推荐使用开源的动画库:
recyclerview-animators
RecyclerView中的列表项默认是没有分割线的,如果想要实现这一需求,就要通过继承RecyclerView.ItemDecoration
这个抽象类实现我们自己的列表项装饰器。这个类需要实现的主要方法如下:
public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
onDraw
方法会在绘制列表项之前调用,因此绘制的内容会在列表项之下;而onDrawOver
会在绘制列表项之后调用,因此绘制的内容会在列表项之上(只可以用于实现角标等需求);getItemOffsets
方法可以通过outRect.set()
的方式为列表项设置偏移量。
这里推荐一个第三方的开源库:
列表项装饰器:RecyclerItemDecoration
小提示:如果仅仅想要在列表项之间增加一些间隔,也可以简单地在Item的布局文件中设置margin
属性,在一些简单的场景下这样做代价更小。
请参考上文:
[实现多布局列表(包括列表头和列表尾)]
个人并不推荐通过重写RecyclerView的方式实现EmptyView,因此后续会写一篇博客介绍如何通过自定义View的方式实现一个通用的多状态布局(加载中、无数据、加载错误等)。
《Android 通过自定义View实现通用的多状态布局》(待填坑)
这里先推荐两个简单的多布局开源库:
loadinglayout
MaterialPageStateLayout
监听滚动状态需要使用RecyclerView的addOnScrollListener
方法,示例代码如下:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//滑动状态发生改变
//newState的可能值:[SCROLL_STATE_IDLE|SCROLL_STATE_DRAGGING|SCROLL_STATE_SETTLING]
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//滑动过程中将反复触发
//dx:水平方向的滑动距离
//dy:垂直方向的滑动距离
}
});
onScrollStateChanged方法会在滑动状态发生改变时回调,newState有三种三种取值,含义如下:
onScrolled方法会在滑动过程中将反复触发,dx和dy的含义如下:
注意:如果可见列表项发生了变化,onScrolled方法也会回调,此时dx和dy都为0。
需要使用的关键方法是canScrollVertically
,该方法的原型如下:
//direction:传入正数代表是否还能向下滚动;传入负数代表是否还能向上滚动
public boolean canScrollVertically(int direction);
比如调用recyclerView.canScrollVertically(1),返回false就代表RecyclerView已经滚动到底部;调用recyclerView.canScrollVertically(-1),返回false就表示RecyclerView已经滚动到顶部。
同理,canScrollHorizontally
用于判断RecyclerView是否已经滚动到最左端或最右端。
//direction:传入正数代表是否还能向右滚动;传入负数代表是否还能向左滚动
public boolean canScrollHorizontally(int direction);
比如调用recyclerView.canScrollHorizontally(1),返回false就代表RecyclerView已经滚动到最右端;调用recyclerView.canScrollHorizontally(-1),返回false就表示RecyclerView已经滚动到最左端。
《Android UI ListView讲解》:详细讲解ListView的使用和常用技巧。
《 Android UI GridView讲解》:详细讲解GridView的使用方法和常用技巧。
《 Android UI 常用控件讲解》:包括CheckBox、RadioButton、ToggleButton、Switch、ProgressBar、SeekBar、RatingBar、Spinner、ImageButton。
https://github.com/CodingEnding/UISystemDemo [ 持续更新中 ]
动画效果库:recyclerview-animators
列表项装饰器:RecyclerItemDecoration
https://blog.csdn.net/qq_26585943/article/details/73739427
https://blog.csdn.net/lmj623565791/article/details/45059587
https://stackoverflow.com/questions/33176336/need-an-example-about-recyclerview-adapter-notifyitemchangedint-position-objec
https://www.jianshu.com/p/ce347cf991db