一、介紹一下
MultiAdapter是一個輕鬆優雅實現RecyclerView多樣式的強大組件!它將item的行為及表現抽象為一個ItemType,不同類型的item都有着自己獨立的點擊事件處理及視圖綁定行為,極大地降低了耦合度,採用反射、ViewBinding及泛型等技術極大地簡化了item相關點擊事件處理過程。其內部封裝了若干實用的工具組件以滿足RecyclerView日常需求,如列表的單選/多選。正是因為有了上述功能支持,我們在給RecyclerView添加頭布局、腳布局、嵌套RecyclerView布局的時候,就簡單的太多了!依賴詳見GitHub:https://github.com/censhengde/MultiAdapter
二、用法(這裡主推ViewBinding用法,原理在後面講)2.1 列表Item多樣式實現
先看成品效果,如圖:

圖 2.0.1
使用步驟:
Step1 在app build.gradle文件開啟ViewBinding:1android{2//......3viewBinding{4enabledtrue5}6}Step 2 創建item的實體類 :
ItemBean.java:
1publicclassItemBean{ 2 3//所有Item類型都在這裡定義 4publicstaticfinalintTYPE_A=0; 5publicstaticfinalintTYPE_B=1; 6publicstaticfinalintTYPE_C=2; 7 8publicintid; 9//Item類型標識(很關鍵!)10publicintviewType;111213//item具體業務數據字段14publicStringtext="";151617publicItemBean(intviewType,Stringtext){18this.viewType=viewType;19this.text=text;20}2122publicItemBean(intid,intviewType,Stringtext){23this.viewType=viewType;24this.text=text;25this.id=id;26}27282930}
ItemBean的關鍵點就是對viewType字段與TYPE_A、TYPE_B、TYPE_C標識位的理解,viewType字段表示當前item實體對象所要表現的item樣式,比如當viewType=TYPE_A時,表示該ItemBean實例想表現A類型Item樣式布局,其他同理。總而言之,這裡秉持一個理念,那就是RecyclerView某position上所表現的item樣式由item實體對象決定,切記!後面講原理時候會再次提到這個理念。(注意:給item實體類添加viewType字段用於指示其表現的item類型是典型用法之一,但並不唯一!)
Step 3 聲明各個item類型布局文件:
item_a.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3xmlns:tools="http://schemas.android.com/tools" 4android:layout_width="match_parent" 5android:layout_height="100dp" 6android:background="#FF5722" 7android:orientation="vertical"> 8 9<TextView10android:id="@+id/tv_a"11android:layout_width="wrap_content"12android:layout_height="match_parent"13android:layout_gravity="center"14android:gravity="center"15android:textSize="18sp"16tools:text="A類Item"/>1718</LinearLayout>
item_b.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3android:orientation="horizontal" 4android:layout_width="match_parent" 5android:background="@color/colorAccent" 6android:layout_height="120dp"> 7 8<Button 9android:id="@+id/btn_b"10android:layout_width="wrap_content"11android:layout_height="wrap_content"12android:layout_gravity="center_vertical"13android:background="@android:color/transparent"14android:gravity="center"15android:text="Button"16android:textAllCaps="false"17android:textSize="18sp"/>18<TextView19android:id="@+id/tv_b"20android:layout_width="wrap_content"21android:layout_height="wrap_content"22android:textSize="18sp"23android:gravity="center"24android:text="B類Item"/>2526</LinearLayout>
item_c.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3android:orientation="vertical" 4android:layout_width="match_parent" 5android:background="#9C27B0" 6android:layout_height="150dp"> 7<TextView 8android:id="@+id/tv_c" 9android:layout_width="wrap_content"10android:layout_height="wrap_content"11android:textSize="18sp"12android:gravity="center"13android:layout_gravity="center|center_vertical"14android:text="C類Item"/>15<ImageView16android:id="@+id/iv_c"17android:layout_width="72dp"18android:layout_height="72dp"19android:layout_gravity="center_horizontal"20android:layout_marginTop="5dp"21android:src="https:@mipmap/ic_launcher"/>22</LinearLayout>Step 4 創建各個item類型的ItemType實現類:
AItemType.java:
1publicclassAItemTypeextendsMultiVBItemType<ItemBean,ItemABinding>{ 2 3/** 4*@paramdata當前position對應的實體對象 5*@paramposition 6*@returntrue表示成功匹配到對應的ItemType 7*/ 8@Override 9publicbooleanmatchItemType(@NullableItemBeandata,intposition){10returndata==null||ItemBean.TYPE_A==data.viewType;//這句話的含義是:當前position 的ItemBean想要表現的item類型是哪一種,11//以本例為例,會依次遍歷A、B、C三個Item類型,直到返回true為止。(詳見MultiHelper getItemViewType方法實現)12}1314/**15*@return返回當前item類型的布局文件id16*/17@Override18publicintgetItemLayoutRes(){19returnR.layout.item_a;20}2122/**23*給當前item類型布局視圖設置數據,意義基本與RecyclerView.Adapter onBindViewHolder 相同。24*@paramvb25*@paramposition26*/2728@Override29protectedvoidonBindViewHolder(@NonNullItemABindingvb,30@NonNullItemBeanitemBean,31intposition){32//直接從ViewBinding獲取控件設置數據33vb.tvA.setText(itemBean.text);34}3536}
BItemType.java:
1publicclassBItemTypeextendsMultiVBItemType<ItemBean,ItemBBinding>{ 2 3@Override 4publicbooleanmatchItemType(ItemBeandata,intposition){ 5returnItemBean.TYPE_B==data.viewType; 6} 7 8@Override 9publicintgetItemLayoutRes(){10returnR.layout.item_b;11}121314@Override15protectedvoidonBindViewHolder(@NonNullItemBBindingvb,16@NonNullItemBeanitemBean,17intposition){18vb.tvB.setText(itemBean.text);19}2021}
CItemType.java:
1publicclassCItemTypeextendsMultiVBItemType<ItemBean,ItemCBinding>{ 2 3 4@Override 5publicbooleanmatchItemType(ItemBeandata,intposition){ 6returnItemBean.TYPE_C==data.viewType; 7} 8 9@Override10publicintgetItemLayoutRes(){11returnR.layout.item_c;12}131415@Override16protectedvoidonBindViewHolder(@NonNullItemCBindingvb,17@NonNullItemBeanbean,18intposition){19vb.tvC.setText(bean.text);20}21}
ItemType是本項目的核心概念之一,如前文所說,ItemType是一類item的抽象,其擁有獨立的視圖綁定和點擊事件處理過程,並且它接管了RecyclerView.Adapter的生命周期業務;ItemType是一個接口,MultiVBItemType是其子類,實現了itemview點擊事件回調等一系列核心功能,具體說明後面講原理時候再詳說。一種類型item對應一個ItemType,切記!先簡單說明下各個方法含義:
public boolean matchItemType:判斷當前 position 的item樣式是否對應當前的ItemType。如此例判斷的依據就是實體對象的viewType字段取值。(這個方法是實現RecyclerView item多樣式的核心,單樣式item無需重寫此方法,具體含義後面會再講。)
2)public int getItemLayoutRes:返回該類item的布局資源文件id。
3)protected void onBindViewHolder:視圖數據綁定。含義基本與RecyclerView Adapter 的onBindViewHolder方法相同,不同的是ItemType的這個方法僅進行當前item類型的視圖數據綁定。ItemABinding、ItemBBinding和ItemCBinding等類都是gradle根據item_a.xml、item_b.xml、item_c.xml布局文件自動生成的ViewBinding實現類。
Step 5 在Activity里的初始化
創建Activity布局文件 activity_multi_item.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3xmlns:app="http://schemas.android.com/apk/res-auto" 4xmlns:tools="http://schemas.android.com/tools" 5android:layout_width="match_parent" 6android:layout_height="match_parent" 7tools:context="com.tencent.multiadapter.example.ui.MultiItemActivity"> 8 9<androidx.recyclerview.widget.RecyclerView10android:id="@+id/rv_list"11android:layout_width="match_parent"12android:layout_height="match_parent"13android:orientation="vertical"14app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>1516</RelativeLayout>
在Activity onCreate方法中初始化RecyclerView:
1classMultiItemActivity:AppCompatActivity(){ 2 3lateinitvaradapter:MultiAdapter<ItemBean,MultiViewHolder> 4 5overridefunonCreate(savedInstanceState:Bundle?){ 6super.onCreate(savedInstanceState) 7setContentView(R.layout.activity_multi_item) 8//初始化ItemType 9valaItemType=AItemType()10valbItemType=BItemType()11valcItemType=CItemType()1213/*初始化Adapter;MultiAdapter是內部封裝好了的Adapter,簡單使用無需新建其子類*/14adapter=MultiAdapter<ItemBean,MultiViewHolder>()15/*將所有ItemType添加到Adapter中*/16adapter.addItemType(aItemType)17.addItemType(bItemType)18.addItemType(cItemType)19/*設置數據*/20adapter.setData(getData())21rv_list.adapter=adapter2223}2425/**26*模擬數據27*/28privatefungetData():List<ItemBean>{29valbeans=ArrayList<ItemBean>()30for(iin0..5){31beans.add(ItemBean(ItemBean.TYPE_A,"我是A類Item$i"))32beans.add(ItemBean(ItemBean.TYPE_B,"我是B類Item${i+1}"))33beans.add(ItemBean(ItemBean.TYPE_C,"我是C類Item${i+2}"))34}35returnbeans36}3738}
至此,點擊運行就可以看到圖2.0.1的效果了!
2.2 在Step 5 的基礎上 給item view設置點擊事件監聽
與item相關的點擊監聽方案支持反射和監聽器兩種方式。
本文主要介紹反射方式:
反射方案是參考了系統的 xml android:onClick="目標方法名" 實現思路,但View並沒有對外提供獲取android:onClick屬性值的方法,故「目標方法名」需另闢蹊徑傳入,由於ItemType擁有獨立的點擊事件處理過程,所以在其實現類AbstractItemType中提供了registerItemViewClickListener和registerItemViewLongClickListener等工具方法用以註冊指定itemview點擊事件監聽。方法原型及其參數說明如下:
1protectedfinalvoidregisterItemViewClickListener(@NonNullVHholder,//當前item類型的ViewHolder。2@NonNullMultiHelper<T,VH>helper,//幫助類對象,通過它可以訪問到item實體對象。3@NullableStringtarget,//目標方法名,可null,當採用監聽器方式時,就傳null。4@IdResint...viewIds)//item 指定view 的id,可同時指定多個,因為存在多個view響應同一套邏輯的情況。5//不傳id時默認是給item根布局設置監聽。
例如:給 A類item 的item view設置監聽:這裡先回顧一下我們使用原生RecyclerViewAdapter給item設置點擊事件監聽的過程,大致就是在onCreateViewHolder方法中xxxxxxxxx一頓操作……還有的朋友在onBindViewHolder方法中獲取指定view再xxxxxx一頓操作(這裡順帶提一嘴,在onBindViewHolder方法進行事件監聽設置絕對是不專業的!)……,如前文所說,ItemType接管了Adapter生命周期,我們先看ItemType接口聲明:ItemType.java:
1publicinterfaceItemType<T,VHextendsRecyclerView.ViewHolder>{ 2 3/** 4*當前position是否匹配當前的ItemType 5* 6*@paramdata 當前position對應的實體對象,當是依賴paging3 getItem()方法返回時,有可能為null。 7*@parampositionadapterposition 8*@returntrue 表示匹配,false:不匹配。 9*/10booleanmatchItemType(@NullableTdata,intposition);111213/**14*創建當前ItemType的ViewHolder15*16*@paramparentparent17*@returnViewHolder18*/19@NonNull20VHonCreateViewHolder(@NonNullViewGroupparent);2122/**23* ViewHolder已經創建完成,在這裡可以註冊Item及其子View點擊事件監聽器,但不要做數據的綁定。24*/25voidonViewHolderCreated(@NonNullVHholder,@NonNullMultiHelper<T,VH>helper);2627/**28*意義與Adapter onBindViewHolder 基本相同,表示當前ItemType的數據綁定過程。29*30*@paramholder31*@paramposition32*/33voidonBindViewHolder(@NonNullVHholder,@NonNullMultiHelper<T,VH>helper,intposition,34@NonNullList<Object>payloads)throwsException;353637voidonBindViewHolder(@NonNullVHholder,@NonNullMultiHelper<T,VH>helper,intposition)throwsException;3839}
ItemType生命周期是以RecyclerView Adapter生命周期為前提的,如果有對RecyclerViewadapter相關方法調用流程還不熟的朋友,建議先去找資料研究一下。既然ItemType接管了adapter生命周期,那其所有方法必定都對應在adapter幾個關鍵方法中被調用,其中在adapteronCreateViewHolder方法中調用到的ItemType的方法有:onCreateViewHolder方法、onViewHolderCreated方法,這裡可能有朋友有疑問:為什麼要把adapteronCreateViewHolder方法拆分為ItemType的兩個階段方法?答案就是出於職責單一編程原則考慮。創建ViewHolder就是創建ViewHolder,給ViewHolderview設置監聽或者進行其他操作又是另一回事了。所以最終我們是在ItemType onViewHolderCreated方法給item相關view設置監聽:
1.在AItemType中重寫onViewHolderCreated方法:
1publicclassAItemTypeextendsMultiItemType<ItemBean>{ 2 3 4/** 5*@paramdata當前position對應的實體對象 6*@paramposition 7*@returntrue表示成功匹配到對應的ItemType 8*/ 9@Override10publicbooleanmatchItemType(@NullableItemBeandata,intposition){11returndata==null||ItemBean.TYPE_A==data.viewType;//這句話的含義是:當前position 的ItemBean想要表現的item類型是哪一種,12//以本例為例,會依次遍歷A、B、C三個Item類型,直到返回true為止。(詳見MultiHelper getItemViewType方法實現)13}1415/**16*@return返回當前item類型的布局文件17*/18@Override19publicintgetItemLayoutRes(){20returnR.layout.item_a;21}2223/**24*表示ViewHolder已經創建完成。本方法最終是在RecyclerView.Adapter onCreateViewHolder方法中被調用,25*所以所有的與item相關的點擊事件監聽器都應在這裡註冊。26*27*@paramholder28*@paramhelper29*/30@Override31publicvoidonViewHolderCreated(@NonNullMultiViewHolderholder,32@NonNullMultiHelper<ItemBean,MultiViewHolder>helper){33/*註冊監聽器,不傳viewId則默認是給item根布局註冊監聽*/34registerItemViewClickListener(holder,helper,"onClickItem");35}36//其他代碼不變,這裡省略。37}
2. 在Activity onCreate方法調用AItemType inject方法注入事件接收者:
1classMultiItemActivity:AppCompatActivity(){ 2 3lateinitvaradapter:MultiAdapter<ItemBean,MultiViewHolder> 4 5overridefunonCreate(savedInstanceState:Bundle?){ 6super.onCreate(savedInstanceState) 7setContentView(R.layout.activity_multi_item) 8//初始化ItemType 9valaItemType=AItemType()10//注入事件接收對象11aItemType.inject(this)12//其他代碼不變,略。1314}15//......16}
3.在Activity中聲明目標方法(注意,方法名一定要與剛才傳入的target值對應!參數列表順序不能亂!方法訪問修飾符任意。):
1/**2*item點擊事件3*/4privatefunonClickItem(view:View,itemBean:ItemBean,position:Int){5Toast.makeText(this,"ItemBean:${itemBean.text},position:$position",Toast.LENGTH_SHORT).show()6}
附Activity完整代碼:
1classMultiItemActivity:AppCompatActivity(){ 2 3lateinitvaradapter:MultiAdapter<ItemBean,MultiViewHolder> 4 5overridefunonCreate(savedInstanceState:Bundle?){ 6super.onCreate(savedInstanceState) 7setContentView(R.layout.activity_multi_item) 8//初始化ItemType 9valaItemType=AItemType()10//注入事件接收對象11aItemType.inject(this)1213valbItemType=BItemType()14valcItemType=CItemType()15bItemType.inject(this)16cItemType.inject(this)17/*初始化Adapter*/18adapter=MultiAdapter<ItemBean,MultiViewHolder>()19/*將所有ItemType添加到Adapter中*/20adapter.addItemType(aItemType)21.addItemType(bItemType)22.addItemType(cItemType)23/*設置數據*/24adapter.setData(getData())25rv_list.adapter=adapter26}2728/**29*模擬數據30*/31privatefungetData():List<ItemBean>{32valbeans=ArrayList<ItemBean>()33for(iin0..5){34beans.add(ItemBean(ItemBean.TYPE_A,"我是A類Item$i"))35beans.add(ItemBean(ItemBean.TYPE_B,"我是B類Item${i+1}"))36beans.add(ItemBean(ItemBean.TYPE_C,"我是C類Item${i+2}"))37}38returnbeans39}4041/**42*item點擊事件43*/44privatefunonClickItem(view:View,itemBean:ItemBean,position:Int){45Toast.makeText(this,"ItemBean:${itemBean.text},position:$position",Toast.LENGTH_SHORT).show()46}4748}
完畢,點擊運行之。效果如圖:

圖 2.0.2
其他item相關點擊事件如item子view點擊事件、長點擊事件等監聽實現同理,詳見工程用例。採用反射方式實現item點擊監聽務必注意代碼混淆後無法找到目標方法的問題,這裡附上兩種避免方式:例1.在module 的proguard-rules.pro文件配置:
1-keepclassmembersclasscom.tencent.multiadapter.example.ui.MultiItemActivity{privatevoidonClickItem(...);}
例2.在方法聲明上標記@Keep註解(推薦):
1/**2*item點擊事件3*/4@Keep5privatefunonClickItem(view:View,itemBean:ItemBean,position:Int){6Toast.makeText(this,"ItemBean:${itemBean.text},position:$position",Toast.LENGTH_SHORT).show()7}2.3 列表單選/多選實現
列表單/多選實現主要依靠CheckingHelper核心類來實現。MultiAdapter集成了CheckingHelper。其核心api說明如下:
void checkItem(int position,@Nullable Object payload):選中item。position:當前位置;payload:用於局部刷新的參數,與 RecyclerView.Adapter notifyItemChanged方法的意義相同。
void uncheckItem(int position,@Nullable Object payload):取消選中item。與checkItem相反。
void checkAll(@Nullable Object payload):全選。payload:同上。
void cancelAll(@Nullable Object payload):取消全選。
void setOnCheckingFinishedCallback(OnCheckingFinishedCallbackcallback):設置完成選擇後的回調接口。OnCheckingFinishedCallback接口方法說明如下:
public interface OnCheckingFinishedCallback{
1/**2*@paramchecked被選中的Item集合3*/4voidonCheckingFinished(@NonNullList<T>checked);
}
6)void finishChecking():完成選擇。調用這個方法將觸發OnCheckingFinishedCallback接口 回調。
一般的簡單列表選擇是當列表是單樣式item的時候,實現比較簡單,這裡先不介紹。我們重點關注一下當列表是多樣式item的時候(如當列表存在頭布局腳布局的時候),我們應該如何排除掉無效item。如圖:

圖 2.3.1
某一條item的選中狀態,它是否是符合我們預期的,需要定義一個符合我們預期的規則,比如當列表存在頭布局腳布局的時候,我們點擊全選,最後只有中間的item是可選中的,點擊完成,最後只有被選中的item集合回調出來,這才符合我們的預期。某一item是否是可選的,以及它符合什麼樣的規則才被認為是選中的,我們抽象出一個接口,名叫Checkable,意為可選的。CheckingHelper將會依據這個接口做統一判斷處理item。Checkable聲明如下:
1publicinterfaceCheckable{2/*設置是否被選中,注意,複雜的item是否被選中規則一定要注意此方法的實現,3*不要局限於單純搞個boolean 變量做判斷。4*/5voidsetChecked(booleanchecked);6/*判斷是否被選中*/7booleanisChecked();8}
現在我們開始實現複雜列表多選功能。
實現步驟:
step 1:新建item實體類CheckableItem.java,並實現Checkable接口,代碼如下:
1publicclassCheckableItemimplementsCheckable{ 2 3publicstaticfinalintVIEW_TYPE_HEADER=1;/*頭布局標識位*/ 4publicstaticfinalintVIEW_TYPE_CHECKABLE=0;/*可選中的Item標識位*/ 5publicstaticfinalintVIEW_TYPE_FOOTER=2;/*腳布局標識位*/ 6publicintviewType=VIEW_TYPE_CHECKABLE;/*默認是可選中item*/ 7 8privatebooleanmIsChecked;/*判斷當前item是否被選中*/ 910publicStringtext="";11publicCheckableItem(intviewType,Stringtext){12this.viewType=viewType;13this.text=text;14}1516@Override17publicvoidsetChecked(booleanchecked){18/*頭布局和腳布局是不可選的。注意,只有這裡的被選中規則定義得準確,19*後面調用CheckingHelperfinishedChecking才能準確甄選出被選中的item20*/21mIsChecked=checked&&viewType==VIEW_TYPE_CHECKABLE;22}2324@Override25publicbooleanisChecked(){26returnmIsChecked;27}28}
注意看setChecked方法的實現,這裡再次強調一個理念:RecyclerView 某position上的item所表現的樣式是由item實體對象決定的。這個理念的另一個解讀含義是:儘管RecyclerView item是多樣式的,但外層實體類的類型是一致的!當用戶點擊全選時,所有類型item實體類(Checkable類型)的setChecked方法被調用且checked參數都傳入true,當用戶點擊完成時,所有類型item實體類(Checkable類型)的isChecked方法被調用,依據其返回值最終判斷當前item最終選中狀態。
step 2:編寫各類item布局文件:頭布局:item_checking_header.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3android:layout_width="match_parent" 4android:layout_height="100dp"> 5<TextView 6android:layout_width="wrap_content" 7android:layout_height="wrap_content" 8android:textSize="18sp" 9android:layout_gravity="center"10android:text="頭布局"/>11</FrameLayout>
可選的item布局:item_checking_checkable.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3android:layout_width="match_parent" 4android:layout_height="120dp"> 5 6<TextView 7android:id="@+id/tv" 8android:layout_width="wrap_content" 9android:layout_height="wrap_content"10android:textSize="18sp"11android:layout_centerVertical="true"12android:layout_marginStart="50dp"/>1314<CheckBox15android:id="@+id/checkbox"16android:layout_width="wrap_content"17android:layout_height="wrap_content"18android:clickable="false"19android:layout_alignParentEnd="true"20android:layout_centerVertical="true"21android:layout_marginEnd="20dp"/>22</RelativeLayout>
腳布局:item_checking_footer.xml:
1<?xmlversion="1.0"encoding="utf-8"?> 2<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" 3android:layout_width="match_parent" 4android:layout_height="100dp" 5android:background="@android:color/darker_gray"> 6<TextView 7android:layout_width="wrap_content" 8android:layout_height="wrap_content" 9android:textSize="18sp"10android:layout_gravity="center|top"11android:textColor="@color/colorAccent"12android:text="腳布局"/>13</FrameLayout>
step 3:創建各個item類型的ItemType實現類:頭布局:HeaderItemType.kt:
1classHeaderItemType:MultiItemType<CheckableItem>(){ 2 3overridefungetItemLayoutRes():Int=R.layout.item_checking_header 4 5overridefunmatchItemType(data:CheckableItem,position:Int):Boolean 6=data.viewType==CheckableItem.VIEW_TYPE_HEADER 7 8overridefunonBindViewHolder(holder:MultiViewHolder,helper:MultiHelper<CheckableItem,MultiViewHolder>,position:Int){ 910}11}
可選item布局:CheckableItemType.kt:
1classCheckableItemType:MultiItemType<CheckableItem>(){ 2 3overridefungetItemLayoutRes():Int=R.layout.item_checking_checkable 4 5overridefunmatchItemType(data:CheckableItem,position:Int):Boolean=data.viewType==CheckableItem.VIEW_TYPE_CHECKABLE 6 7overridefunonViewHolderCreated(holder:MultiViewHolder,helper:MultiHelper<CheckableItem,MultiViewHolder>){ 8registerItemViewClickListener(holder,helper,"onClickItem") 9}10111213/**14*只有局部刷新才會回調到這裡,RecyclerView上下滑動則不會,有區別於RecyclerView.Adapter中的實現.15*/16overridefunonBindViewHolder(holder:MultiViewHolder,17helper:MultiHelper<CheckableItem,MultiViewHolder>,18position:Int,19payloads:MutableList<Any>){20payloads.forEach{21if(itisInt)22if(it==R.id.checkbox){23valitem=helper.getItem(position)?:return24valcheckbox=holder.getView<CheckBox>(R.id.checkbox)25checkbox.isChecked=item.isChecked26}27}28}2930overridefunonBindViewHolder(holder:MultiViewHolder,helper:MultiHelper<CheckableItem,MultiViewHolder>,position:Int){31valitem=helper.getItem(position)?:return3233valtv=holder.getView<TextView>(R.id.tv)34tv.text=item.text35//CheckBox36valcheckbox=holder.getView<CheckBox>(R.id.checkbox)37checkbox.isChecked=item.isChecked3839}40}
注意圖中兩個onBindViewHolder重載方法的實現,清楚RecyclerViewitem局部刷新的朋友應該了解這兩個方法的用法及區別,本文不做贅述。
腳布局:FooterItemType.kt:
1classFooterItemType:MultiItemType<CheckableItem>(){ 2 3overridefungetItemLayoutRes():Int=R.layout.item_checking_footer 4 5overridefunmatchItemType(data:CheckableItem,position:Int):Boolean=data.viewType==CheckableItem.VIEW_TYPE_FOOTER 6 7overridefunonBindViewHolder(holder:MultiViewHolder,helper:MultiHelper<CheckableItem,MultiViewHolder>,position:Int){ 8 9}10}
step 4:在activity初始化:
1classCheckItemActivity:AppCompatActivity(),OnCheckingFinishedCallback<CheckableItem>{ 2valadapter=MultiAdapter<CheckableItem,MultiViewHolder>() 3valdataSize=30 4overridefunonCreate(savedInstanceState:Bundle?){ 5super.onCreate(savedInstanceState) 6setContentView(R.layout.activity_check_item) 7valcheckableItemType=CheckableItemType() 8/*註冊item點擊監聽*/ 9checkableItemType.inject(this)1011/*添加ItemType*/12adapter.addItemType(HeaderItemType())13.addItemType(checkableItemType)14.addItemType(FooterItemType())1516//設置完成選擇的回調17adapter.checkingHelper.setOnCheckingFinishedCallback(this)1819adapter.setData(getData())20rv_list.adapter=adapter21}2223/*模擬數據(頁面狀態的改變可能會導致列表選擇狀態丟失,建議在ViewModel或其他序列化手段保存數據以便恢復列表選擇狀態)24**/25privatefungetData():MutableList<CheckableItem>{26valdata=ArrayList<CheckableItem>(dataSize+2)27/*頭布局item實體對象*/28data.add(CheckableItem(CheckableItem.VIEW_TYPE_HEADER,""))29/*中間可選的item實體對象*/30for(iin0untildataSize){31data.add(CheckableItem(CheckableItem.VIEW_TYPE_CHECKABLE,"可選的Itemposition=${i}"))32}33/*腳布局item實體對象*/34data.add(CheckableItem(CheckableItem.VIEW_TYPE_FOOTER,""))35returndata36}3738/*點擊完成*/39funonClickFinished(view:View){40adapter.checkingHelper.finishChecking()41}4243/*點擊全選、取消*/44funonClickCheckAll(view:View){45valbtn=(viewasButton)46when(btn.text){47"全選"->{48btn.text="取消"49adapter.checkingHelper.checkAll(R.id.checkbox)5051}52"取消"->{53btn.text="全選"54adapter.checkingHelper.cancelAll(R.id.checkbox)55}56}57}5859/*點擊可選的item*/60privatefunonClickItem(view:View,item:CheckableItem,position:Int){61if(item.isChecked){62adapter.checkingHelper.uncheckItem(position,R.id.checkbox)63}else{64adapter.checkingHelper.checkItem(position,R.id.checkbox)65}66/*當你想實現列表單選時,請調用adapter.checkingHelper.singleCheckItem(position,R.id.checkbox)*/67}6869/*點擊完成時的數據回調*/70overridefunonCheckingFinished(checked:List<CheckableItem>){71checked.forEach{72Log.e("被選中的item:",it.text)73}74}75}
這裡注意payload參數要與CheckableItemType onBindViewHolder 3參數方法中的對應。
完畢,點擊運行之,效果如圖2.3.1
三、擴展
MultiAdapter庫由於其內部組件的高度解耦性,可將其復用於其它RecyclerView.Adapter。當我們想要實現RecyclerView分頁功能的時候、也許我們會選擇Google的paging1、2、3的解決方案,這時候MultiAdapter庫提供的MultiAdapter將不再適用,但我們可以模仿MultiAdapter的構建過程,利用MultiHelper、CheckingHelper組件輕鬆地完成其他任意RecyclerView.Adapter改造。
以改造paging3 的PagingDataAdapter為例,代碼如下:
1openclassMultiPagedAdapter<T:Any,VH:RecyclerView.ViewHolder>(diffCallback:DiffUtil.ItemCallback<T>) 2:PagingDataAdapter<T,VH>(diffCallback){ 3 4valmultiHelper=object:MultiHelper<T,VH>(this){ 5 6overridefungetItem(p0:Int):T?{ 7returnthis@MultiPagedAdapter.getItem(p0) 8} 910}11valcheckingHelper=object:CheckingHelper<T>(this){12overridefungetItem(position:Int):T?=this@MultiPagedAdapter.getItem(position)13overridefungetDataSize():Int=this@MultiPagedAdapter.itemCount1415}1617overridefungetItemViewType(position:Int):Int{18returnmultiHelper.getItemViewType(position)19}2021overridefunonCreateViewHolder(parent:ViewGroup,viewType:Int):VH{22returnmultiHelper.onCreateViewHolder(parent,viewType)23}2425overridefunonBindViewHolder(holder:VH,position:Int,payloads:List<Any?>){26multiHelper.onBindViewHolder(holder,position,payloads)27}2829overridefunonBindViewHolder(holder:VH,position:Int){3031}3233}
可以看到,MultiHelper的本質就是代理的Adapter的生命周期。這裡注意onBindViewHolder方法,我們僅需要代理上面3參數的即可,因為內部做了處理,讓執行流程直接調用到ItemType的兩個重載onBindViewHolder方法。
四、原理篇
多樣式item實現的核心邏輯封裝在了MultiHelper類中,MultiHelper本質是接管了RecyclerView.Adapter生命周期,代理了RecyclerView.Adapter的getItemViewType、onCreateViewHolder與onBindViewHolder三個核心方法,並將其生命周期事件分發給了position對應的ItemType,最終轉換成了ItemType的生命周期。(有對RecyclerView.Adapter生命周期流程不熟的同學建議先去了解一下,網上博客資料都有的)原理篇主要依照RecyclerView.Adapter生命周期這條主線來講解,其他細節由於篇幅有限,建議讀者去閱讀工程源碼,注釋解釋思想一應俱全!
這裡先瞜一眼MultiHelper整體源碼:
1publicabstractclassMultiHelper<T,VHextendsRecyclerView.ViewHolder>{ 2 3 4/** 5*ItemType集合. 6*/ 7privatefinalSparseArray<ItemType<T,VH>>mItemTypePool=newSparseArray<>(); 8 9publicfinalintgetItemViewType(intposition){10if(position==RecyclerView.NO_POSITION){11returnRecyclerView.INVALID_TYPE;12}13finalTdata=getItem(position);14finalItemType<T,VH>currentType=findCurrentItemType(data,position);15returncurrentType==null?RecyclerView.INVALID_TYPE:currentType.getClass().hashCode();16}171819/**20*遍歷查找當前position對應的ItemType。21*22*@paramdata23*@paramposition24*@return25*/26@Nullable27privateItemType<T,VH>findCurrentItemType(Tdata,intposition){28//為當前position匹配它的ItemType29for(inti=0;i<mItemTypePool.size();i++){30finalItemType<T,VH>type=mItemTypePool.valueAt(i);31if(type.matchItemType(data,position)){32returntype;33}34}35returnnull;36}3738@NotNull39publicfinalVHonCreateViewHolder(@NonNullViewGroupparent,intviewType){40finalItemType<T,VH>type=mItemTypePool.get(viewType);41if(viewType==RecyclerView.INVALID_TYPE||type==null){//表示無效42thrownewIllegalStateException("ItemType 不合法:viewType=="+viewType+"ItemType=="+type);43}44finalVHholder=type.onCreateViewHolder(parent);45type.onViewHolderCreated(holder,this);46returnholder;47}4849publicfinalvoidonBindViewHolder(@NonNullVHholder,50intposition,51@NonNullList<Object>payloads){52if(position==RecyclerView.NO_POSITION){53return;54}55/*統一捕獲由position引發的可能異常*/56try{57finalItemType<T,VH>currentType=mItemTypePool.get(holder.getItemViewType());58finalTbean=getItem(position);59if(bean==null||currentType==null){60return;61}62if(payloads.isEmpty()){63currentType.onBindViewHolder(holder,bean,position);64}65/*item局部刷新*/66else{67currentType.onBindViewHolder(holder,bean,position,payloads);68}69}catch(Exceptione){70e.printStackTrace();71}7273}7475@Nullable76publicabstractTgetItem(intposition);777879/**80*註冊ItemType81*82*@paramtype83*@return84*/85publicfinalvoidaddItemType(ItemType<T,VH>type){86if(type==null){87return;88}89//getClass().hashCode():確保一種item類型只有一個對應的ItemType實例。90mItemTypePool.put(type.getClass().hashCode(),type);91}929394}
如前文所說,MultiHelper類代理了RecyclerViewAdapter,為什麼採用這種設計模式呢???答案就是為了之後改造任意RecyclerView Adapter達到代碼復用的目的,如前文改造goolepaging3 PagingDataAdapter就是個例子。RecyclerView有很多框架組件,不同框架組件可能有不同的Adapter實現,我們需要考慮兼容人家的東西。
可以看到,這裡又一大堆與RecyclerView Adapter相似的方法,注意,這裡既然是代理Adapter,那這些方法及其參數含義就肯定與Adapter的一致了!
先解釋幾個核心點:
1 、mItemTypePool:SparseArray類型,以ItemType Class對象的hashCode()方法返回值為key,ItemType 實例為value進行存儲。在addItemType方法進行賦值。(它的key含義具有重大意義!這裡先留個神!)
有多少種item類型就有多少個ItemType實例被存儲。同一種item類型只有一個ItemType實例,理解了有沒有?
2、findCurrentType(data, position)方法源碼:
1@Nullable 2privateItemType<T,VH>findCurrentType(Tdata,intposition){ 3//為當前position匹配它的ItemType 4for(inti=0;i<mItemTypes.size();i++){ 5finalItemType<T,VH>type=mItemTypes.valueAt(i); 6if(type.matchItemType(data,position)){ 7returntype; 8} 9}10returnnull;11}
可以看到,其實就是遍歷mItemTypePool集合,逐個調用ItemType的matcItemType()方法進行判斷,如果返回true則表示匹配成功,返回ItemType。用戶不必擔心item的增刪改會導致item樣式表現錯亂問題。這裡又再次強調了那個理念:RecyclerView 某position 對應的item 所表達的類型由其實體對象決定!實體對象怎麼決定的?請回顧前文ItemBean 的 int 類型的viewType字段、ItemType的matchItemType()方法的實現!!!你品!你細細品!!!
經此getItemViewType方法調用,進而得到position對應的viewType值,再返回,接着RecyclerViewAdapter生命周期就走到了 onCreateViewHolder 方法:
1@NotNull 2publicfinalVHonCreateViewHolder(@NonNullViewGroupparent,intviewType){ 3finalItemType<T,VH>type=mItemTypePool.get(viewType); 4if(viewType==INVALID_VIEW_TYPE||type==null){//表示無效 5thrownewIllegalStateException("ItemType 不合法:viewType=="+viewType+"ItemType=="+type); 6} 7finalVHholder=type.onCreateViewHolder(parent); 8type.onViewHolderCreated(holder,this); 9returnholder;10}
注意看mItmTypePool集合,暈了就再回顧前文介紹。這裡通過viewType值直接拿到了對應的ItemType,接着回調其onCreateViewHolder、onViewHolderCreated方法,返回ViewHolder,end!
最後最後,RecyclerView adapter生命周期走到了 onBindViewHolder方法(注意是3參數的),看代碼:
1publicfinalvoidonBindViewHolder(@NonNullVHholder, 2intposition, 3@NonNullList<Object>payloads){ 4if(position==RecyclerView.NO_POSITION){ 5return; 6} 7/*統一捕獲由position引發的可能異常*/ 8try{ 9//這裡通過 ViewHolder getItemType()方法又直接拿到對應的ItemType。10finalItemType<T,VH>currentType=mItemTypePool.get(holder.getItemViewType());11finalTbean=getItem(position);12if(bean==null||currentType==null){13return;14}15if(payloads.isEmpty()){16currentType.onBindViewHolder(holder,bean,position);17}18/*item局部刷新*/19else{20currentType.onBindViewHolder(holder,bean,position,payloads);21}22}catch(Exceptione){23e.printStackTrace();24}25}
這裡可以看到,mItemTypePool集合從MultiHelper. addItemType方法開始,到MultiHelper.getItemViewType方法、再到MultiHelper. onCreateViewHolder方法,最後到MultiHelper.onBindViewHolder方法,貫穿一路!全篇始終圍繞着它的key含義進行構造,可以說這個集合是全篇的靈魂!……至此RecyclerView.Adapter生命周期流程就梳理完了。其他細枝末節諸如ItemType幾個關鍵子類MultiItemType、MultiVBItemType、AbstrcactItemType的實現詳見工程源碼,注釋解釋思想一應俱全!
行文至此,已傾盡所有。MultiAdapter庫歷經項目多個版本雕琢,已趨於穩定。眾道友如有在使用過程中不幸踩坑,務必先冷靜三分,反饋與我,雖忙必復!
推薦閱讀
我的5年Android學習之路,那些年一起踩過的坑
耗時一周,我解決了微信 Matrix 增量編譯的 Bug,已提 PR
Android QMUI實戰:實現APP換膚功能,並自動適配手機深色模式