RecyclerView 知識梳理(2) Adapter

澤毛發表於2017-12-21

一、概述

當我們使用RecyclerView時,第一件事就是要繼承於RecyclerView.Adapter,實現其中的抽象方法,來處理資料的展示邏輯,今天,我們就來介紹一下Adapter中的相關方法。

二、基礎用法

我們從一個簡單的線性列表佈局開始,介紹RecyclerView.Adapter的基礎用法。 首先,需要匯入遠端依賴包:

 compile'com.android.support:recyclerview-v7:25.3.1'
複製程式碼

接著,繼承於RecyclerView.Adapter來實現自定義的NormalAdapter

public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> {

    private List<String> mTitles = new ArrayList<>();

    public NormalAdapter(List<String> titles) {
        mTitles = titles;
    }

    @Override
    public NormalViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item, parent, false);
        return new NormalViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(NormalViewHolder holder, int position) {
        holder.setTitle(mTitles.get(position));
    }

    @Override
    public int getItemCount() {
        return mTitles.size();
    }

    class NormalViewHolder extends RecyclerView.ViewHolder {

        private TextView mTextView;

        NormalViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.tv_title);
        }

        void setTitle(String title) {
            mTextView.setText(title);
        }

    }
}
複製程式碼

當我們實現自己的Adapter時,至少要做四個工作:

  • 第一:繼承於RecyclerView.ViewHolder,編寫自己的ViewHolder
  • 這個子類用來描述RecyclerView中每個Item的佈局以及和它關聯的資料,它同時也是RecyclerView.Adapter<VH>中需要指定的VH型別。
  • 在構造方法中,除了需要呼叫super(View view)方法來傳入Item的跟佈局來給基類中itemView變數賦值,還應當提前執行findViewById來獲得其中的子View以便我們之後對它們進行更新。
  • 第二:實現onCreateViewHolder(ViewGroup parent, int viewType)
  • RecyclerView需要我們提供型別為viewType的新ViewHolder時,會回撥這個方法。
  • 在這裡,我們例項化出了Item的根佈局,並返回一個和它繫結的ViewHolder
  • 第三:實現onBindViewHolder(VH viewHolder, int position)
  • RecyclerView需要展示對應position位置的資料時會回撥這個方法。
  • 通過viewHolder中持有的對應position上的View,我們可以更新檢視。
  • 第四:實現getItemCount()
  • 返回Item的總數。

Activity中,我們給Adapter傳遞資料,使用方法和ListView基本相同,只是多了一句在設定LayoutManager的操作,這個我們後面再分析。

    private void init() {
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
        mTitles = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mTitles.add("My name is " + i);
        }
        NormalAdapter normalAdapter = new NormalAdapter(mTitles);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(normalAdapter);
    }
複製程式碼

這樣,一個RecyclerView的例子就完成了:

RecyclerView 知識梳理(2)   Adapter

三、只有一種ViewType下的複用情況分析

下面,我們來分析一下兩個關鍵方法的呼叫時機:

  • onCreateViewHolder
  • onBindViewHolder

通過這兩個方法回撥的時機,我們可以對RecyclerView複用的機制有一個大概的瞭解。

3.1 初始進入

剛開始進入介面的時候,我們只展示了3Item,此時這兩個方法的呼叫情況如下,可以看到,RecyclerView只例項化了螢幕內可見的ViewHolder,並且onBindViewHolder是在對應的onCreateViewHolder呼叫完後立即呼叫的:

RecyclerView 知識梳理(2)   Adapter

3.2 開始滑動

當我們手指觸控到螢幕,並開始向下滑動,我們會發現,雖然position=3Item還沒有展示出來,但是這時候它的onCreateViewHolderonBindViewHolder就被回撥了,也就是說,我們會預載入一個螢幕以外的Item

RecyclerView 知識梳理(2)   Adapter

3.3 繼續滑動

當我們繼續往下滑動,position=3Item一被展示,那麼position=4Item的兩個方法就會被回撥。

3.4 複用

postion=6Item被展示之後,按照前面的分析,這時候就應當回撥position=7onCreateViewHolderonBindViewHolder方法了,但是我們發現,這時候只回撥了onBindViewHolder方法,而傳入的ViewHolder其實是position=0ViewHolder,也就是我們所說的複用:

RecyclerView 知識梳理(2)   Adapter
此時,螢幕中Items的展現情況為:
RecyclerView 知識梳理(2)   Adapter
目前不可見的Itemposition=0,1,2,所以,我們可以得出結論:在單一佈局的情況,RecyclerView在複用的時候,會取相反方向中超出顯示範圍的第3Item來複用,而並不是超出顯示範圍的第一個Item進行復用。

四、多種型別的佈局

4.1 基本使用

當我們需要在列表當中展示不同型別的Item時,我們一般需要重寫下面的方法,告訴RecyclerView在對應的position上需要展示什麼型別的Item

  • public int getItemViewType(int position)

RecyclerView在回撥onCreateViewHolder的時候,同時也會把viewType傳遞進來,我們根據viewType來建立不同的佈局。 下面,我們就來演示一下它的用法,這裡我們返回三種不同型別的item

public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> {

    private List<String> mTitles = new ArrayList<>();

    public NormalAdapter(List<String> titles) {
        mTitles = titles;
    }

    @Override
    public NormalViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = null;
        switch (viewType) {
            case 0:
                itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_1, parent, false);
                break;
            case 1:
                itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_2, parent, false);
                break;
            case 2:
                itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_item_3, parent, false);
                break;

        }
        NormalViewHolder viewHolder = new NormalViewHolder(itemView);
        Log.d("NormalAdapter", "onCreateViewHolder, address=" + viewHolder.toString());
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(NormalViewHolder holder, int position) {
        Log.d("NormalAdapter", "onBindViewHolder, address=" + holder.toString() + ",position=" + position);
        int viewType = getItemViewType(position);
        String title = mTitles.get(position);
        holder.setTitle1("title=" + title + ",viewType=" + viewType);
    }

    @Override
    public int getItemCount() {
        return mTitles.size();
    }

    @Override
    public int getItemViewType(int position) {
        return position % 3;
    }

    class NormalViewHolder extends RecyclerView.ViewHolder {

        private TextView mTv1;

        NormalViewHolder(View itemView) {
            super(itemView);
            mTv1 = (TextView) itemView.findViewById(R.id.tv_title_1);
        }

        void setTitle1(String title) {
            mTv1.setText(title);
        }

    }
}
複製程式碼

最終,會得到下面的介面:

RecyclerView 知識梳理(2)   Adapter

4.2 多種viewType下的複用情況分析

前面,我們已經研究過一種viewType下的複用情況,現在,我們再來分析一下多種viewType時候的複用情況。

4.2.1 初始進入

此時,我們螢幕中展示了postion=0~6這七個ItemonCreateViewHolderonBindViewHolder的回撥和之前相同,只會生成螢幕內可見的ViewHolder

RecyclerView 知識梳理(2)   Adapter

4.2.2 開始滑動和繼續滑動

這兩種情況都和單個viewType時相同,會預載入螢幕以外的一個Item

RecyclerView 知識梳理(2)   Adapter

4.2.3 複用

關鍵,我們看一下何時會複用position=0/viewType=1Item

RecyclerView 知識梳理(2)   Adapter
此時,螢幕內最上方的Itemposition=4/viewType=1,最下方的Itemposition=11/viewType=2,按照之前的分析,RecyclerView會保留相反方向的2ViewHolder,也就是保留postion=2,3ViewHolder,並複用position=1ViewHolder,但是現在position=0ViewHolderviewType=1,不可以複用,因此,會繼續往上尋找,這時候就找到了position=0ViewHolder進行復用。

五、資料更新

5.1 更新方式

當資料來源發生變化的時候,我們一般會通過Adatper. notifyDataSetChanged()來進行介面的重新整理,RecyclerView.Adapter也提供了相同的方法:

public final void notifyDataSetChanged() 
複製程式碼

除此之外,它還提供了下面幾種方法,讓我們進行區域性的重新整理:

//position的資料變化
notifyItemChanged(int postion)
//在position的下方插入了一條資料
notifyItemInserted(int position)
//移除了position的資料
notifyItemRemoved(int postion)
//從position開始,往下n條資料發生了改變
notifyItemRangeChanged(int postion, int n)
//從position開始,插入了n條資料
notifyItemRangeInserted(int position, int n)
//從position開始,移除了n條資料
notifyItemRangeRemoved(int postion, int n)
複製程式碼

下面是一些簡單的使用方法:

   //在頭部新增多個資料.
   public void addItems() {
        mTitles.add(0, "add Items, name=0");
        mTitles.add(0, "add Items, name=1");
        mNormalAdapter.notifyItemRangeInserted(0, 2);
    }
    //移除頭部的多個資料.
    public void removeItems() {
        mTitles.remove(0);
        mTitles.remove(0);
        mNormalAdapter.notifyItemRangeRemoved(0, 2);
    }
    //移動資料.
    public void moveItems() {
        mTitles.remove(1);
        mTitles.add(2, "move Items name=0");
        mNormalAdapter.notifyItemMoved(1, 2);
    }
複製程式碼

5.2 比較

資料的更新分為兩種:

  • Item changes:除了Item所對應的資料被更新外,沒有其它的變化,對應notifyXXXChanged()方法。
  • Structural changesItems在資料集中被插入、刪除或者移動,對應notifyXXXInsert/Removed/Moved方法。

notifyDataSetChanged會把當前所有的Item和結構都視為已經失效的,因此它會讓LayoutManager重新繫結Items,並對他們重新佈局,這在我們知道已經需要更新某個Item的時候,其實是不必要的,這時候就可以選擇進行區域性更新來提高效率。

六、監聽ViewHolder的狀態

RecyclerView.Adapter中還提供了一些回撥,讓我們能夠監聽某個ViewHolder的變化:

    @Override
    public void onViewRecycled(NormalViewHolder holder) {
        Log.d("NormalAdapter", "onViewRecycled=" + holder);
        super.onViewRecycled(holder);
    }

    @Override
    public void onViewDetachedFromWindow(NormalViewHolder holder) {
        Log.d("NormalAdapter", "onViewDetachedFromWindow=" + holder);
        super.onViewDetachedFromWindow(holder);
    }

    @Override
    public void onViewAttachedToWindow(NormalViewHolder holder) {
        Log.d("NormalAdapter", "onViewAttachedToWindow=" + holder);
        super.onViewAttachedToWindow(holder);
    }
複製程式碼

下面,我們就從例項來講解這幾個方法的呼叫時機,初始時刻,我們的介面為:

RecyclerView 知識梳理(2)   Adapter

  • 初始進入時,position=0~6onViewAttachedToWindow被回撥:
    RecyclerView 知識梳理(2)   Adapter
  • 當滑動到postion=7可見時,它的onViewAttachedToWindow被回撥:
    RecyclerView 知識梳理(2)   Adapter
  • postion=0被移出螢幕可視範圍內,它的onViewDetachedFromWindow被回撥:
    RecyclerView 知識梳理(2)   Adapter
  • 而當我們繼續往下滑動,當position=2被移出螢幕之後,此時position=0onViewRecycled被回撥:
    RecyclerView 知識梳理(2)   Adapter
    現在回憶一下之前我們對複用情況的分析,RecyclerView最多會保留相反方向上的兩個ViewHolder,此時雖然position=1,2不可見,但是依然需要保留它們,這時候會回收position=0ViewHolder以備之後被複用。

七、監聽RecyclerViewRecyclerView.Adapter的關係

RecyclerViewAdapter是通過setAdapter方法來繫結的,因此在Adapter中也通過了繫結的監聽:

public void onAttachedToRecyclerView(RecyclerView recyclerView) {}
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {}
複製程式碼

八、小結

這篇文章,主要總結了一些RecyclerView.Adapter中平時我們不常注意的細節問題,也通過例項瞭解到了關鍵方法的含義,最後,推薦一個Adapter的開源庫:BaseRecyclerViewAdapterHelper


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章