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<String> dataList;//數據源
    private LayoutInflater inflater;//佈局解析器

    public RecyclerViewAdapter(List<String> 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<String> createDataList(){
    List<String> 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設置適配器

效果截圖:

監聽列表項的點擊事件

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

效果截圖:

StaggeredGridLayoutManager使用示例:

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

效果截圖:

相關方法

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設置列表頭和列表尾,這時只需要將列表頭和列表尾視爲兩種獨立的佈局方式即可。在這裏,將介紹如何實現一個簡單的多佈局列表,最終的效果如下:

準備佈局文件

在本例中,主要有兩種列表項,即標題項和內容項。因此,準備兩個對應的佈局文件,分別命名爲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<BaseMultiBean> dataList;//數據源

    private LayoutInflater inflater;//佈局解析器

    public StyleRecyclerViewAdapter(List<BaseMultiBean> 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<BaseMultiBean> 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<Object> payloads)方法,這個方法會在onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法之前調用。示例代碼如下:

//在這個方法中實現Item的局部更新(比如只更新ViewHolder中的一個View)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> 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");

注意:如果不使用局部更新的方式,即使列表項中的圖片並未發生改變,在刷新過程中圖片區域依舊會出現短暫的閃爍現象,使用局部更新就可以解決這一問題。

普通刷新效果截圖:

局部更新效果截圖:

通知指定範圍內的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屬性,在一些簡單的場景下這樣做代價更小。

添加頭部和尾部

請參考上文:

[實現多佈局列表(包括列表頭和列表尾)]

設置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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章