Android UI RecyclerView讲解

前言

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复用机制。

主要重写的方法有onCreateViewHolderonBindViewHoldergetItemCount,分别用于创建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的使用步骤:

  1. 准备列表项布局文件
  2. 实现适配器
  3. 为RecyclerView设置布局管理器
  4. 为RecyclerView设置适配器

效果截图:

Android UI RecyclerView讲解_第1张图片

监听列表项的点击事件

和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();
    }
});

使用不同的LayoutManager

在上面的例子中,我们使用LinearLayoutManager实现了类似ListView的效果。实际上,RecyclerView一共提供了三种LayoutManger,用于实现多种布局效果。下面简单介绍一下这几种布局管理器:

  1. LinearLayoutManager:线性布局管理器,有横向和纵向两种布局方向,可以通过setOrientation方法设置布局方向。
  2. GridLayoutManager:网格布局管理器,可以实现类似GridView的排列效果,属于LinearLayoutManager的子类。
  3. StaggeredGridLayoutManager:可以实现瀑布流的布局管理器。

注意:如果要实现瀑布流式布局,要求Item的高度不同(纵向排列时),否则StaggeredGridLayoutManager的显示效果和GridLayoutManager相同。

GridLayoutManager使用示例:

GridLayoutManager gridLayoutManager=new GridLayoutManager(RecyclerViewActivity.this,3);//3列
recyclerView.setLayoutManager(gridLayoutManager);

效果截图:

Android UI RecyclerView讲解_第2张图片

StaggeredGridLayoutManager使用示例:

//垂直排列、4列
StaggeredGridLayoutManager staggeredGridLayoutManager=new StaggeredGridLayoutManager(4,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredGridLayoutManager);

效果截图:

Android UI RecyclerView讲解_第3张图片

相关方法

RecyclerView

添加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),两者的返回结果将不同。

LinearLayoutManager

构造方法:

//默认纵向排列
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();//获取最后一个完整可见的列表项位置

GridLayoutManger

构造方法:

//默认纵向排列
//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);

GridLayoutMangerLinearLayoutManager的子类,因此继承了LinearLayoutManager的所有方法,这里不再赘述。不过要注意,GridLayoutManger并不支持setStackFromEnd方法。

StaggeredGridLayoutManager

构造方法:

//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设置列表头和列表尾,这时只需要将列表头和列表尾视为两种独立的布局方式即可。在这里,将介绍如何实现一个简单的多布局列表,最终的效果如下:

Android UI RecyclerView讲解_第4张图片

准备布局文件

在本例中,主要有两种列表项,即标题项和内容项。因此,准备两个对应的布局文件,分别命名为recycler_view_multi_title.xmlrecycler_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.xmlrecycler_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;
    }
}

可以看到,基类中主要是封装了实体的类型属性,这一属性将用于确定要使用的列表项布局。然后,再建立两个继承自基类的实体类,分别对应标题项和内容项,本例中命名为TitleBeanItemBean,代码如下:

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设置适配器

完成前面的准备工作后,就可以着手为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.AdapteronAttachedToRecyclerView方法,并在显示列表头/尾的时候让其占据整行,就可以保证列表头/为尾正常显示。

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.AdapteronViewAttachedToWindow方法,并在显示列表头/尾的时候让其占据整行,就可以保证列表头/为尾正常显示。

常用技巧

实现局部刷新

除了使用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 payloads)方法,这个方法会在onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法之前调用。示例代码如下:

//在这个方法中实现Item的局部更新(比如只更新ViewHolder中的一个View)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
    if(payloads.isEmpty()){//如果payloads为空,就调用默认实现
        super.onBindViewHolder(holder,position,payloads);
    }
    else{//在payloads不为空的时候实现ViewHolder中的部分更新
        if("TYPE_CONTENT".equals(payloads.get(0))){
            ContentViewHolder contentViewHolder= (ContentViewHolder) holder;
            ItemBean itemBean= (ItemBean) getItem(position);
            contentViewHolder.itemContentView.setText(itemBean.getContent());
        }
    }
} 
  

这个方法中的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");

注意:如果不使用局部更新的方式,即使列表项中的图片并未发生改变,在刷新过程中图片区域依旧会出现短暂的闪烁现象,使用局部更新就可以解决这一问题。

普通刷新效果截图:
Android UI RecyclerView讲解_第5张图片

局部更新效果截图:
Android UI RecyclerView讲解_第6张图片

通知指定范围内的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);

效果截图:

Android UI RecyclerView讲解_第7张图片

通知有数据被移除:

public final void notifyItemRangeRemoved(int positionStart, int itemCount);
public final void notifyItemRemoved(int position);

效果截图:

Android UI RecyclerView讲解_第8张图片

通知有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属性,在一些简单的场景下这样做代价更小。

添加头部和尾部

请参考上文:

[实现多布局列表(包括列表头和列表尾)]

设置EmptyView

个人并不推荐通过重写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有三种三种取值,含义如下:

  1. SCROLL_STATE_IDLE:静止状态
  2. SCROLL_STATE_DRAGGING:滑动状态(用户此时触碰着屏幕且在滑动)
  3. SCROLL_STATE_SETTLING:惯性滑动状态(用户此时未触碰屏幕,RecyclerView借助上一次滑动的惯性滑动)

onScrolled方法会在滑动过程中将反复触发,dx和dy的含义如下:

  1. dx:水平方向的滑动距离。如果dx大于0,代表手指向左滑动;如果dx小于0,代表手指向右滑动。如果RecyclerView是垂直布局(只能上下滑动),则dx始终为0。
  2. dy:垂直方向的滑动距离。如果dy大于0,代表手指向上滑动;如果dy小于0,代表手指向下滑动。如果RecyclerView是水平布局(只能左右滑动),则dy始终为0。

注意:如果可见列表项发生了变化,onScrolled方法也会回调,此时dx和dy都为0。

判断RecyclerView是否已经滚动到底部或顶部

需要使用的关键方法是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。

demo下载地址

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

你可能感兴趣的:(Android,UI)