前言
在RecyclerView實現多種Item類型列表時,有很多種實現方式,這裏結合 AsyncListDiffer+DataBinding+Lifecycles 實現一種簡單,方便,快捷並以數據驅動UI變化的MultiTypeAdapter
- AsyncListDiffer 一個在後臺線程中使用DiffUtil計算兩組新舊數據之間差異性的輔助類。
- DataBinding 以聲明方式將可觀察的數據綁定到界面元素。
- Lifecycles 管理您的 Activity 和 Fragment 生命週期。
Tip: 對DataBinding和Lifecycles不熟悉的小夥伴可點擊查看官方介紹。
效果圖
1. 定義一個基類MultiTypeBinder方便統一實現與管理
MultiTypeBinder中部分函數說明:
- layoutId():初始化xml。
- areContentsTheSame():該方法用於數據內容比較,比較兩次內容是否一致,刷新UI時用到。
- onBindViewHolder(binding: V):與RecyclerView.Adapter中的onBindViewHolder方法功能一致,在該方法中做一些數據綁定與處理,不過這裏推薦使用DataBinding去綁定數據,以數據去驅動UI。
- onUnBindViewHolder():該方法處理一些需要釋放的資源。
繼承MultiTypeBinder後進行Layout初始化和數據綁定及解綁處理
abstract class MultiTypeBinder<V : ViewDataBinding> : ClickBinder() {
/**
* BR.data
*/
protected open val variableId = BR.data
/**
* 被綁定的ViewDataBinding
*/
open var binding: V? = null
/**
* 給綁定的View設置tag
*/
private var bindingViewVersion = (0L until Long.MAX_VALUE).random()
/**
* 返回LayoutId,供Adapter使用
*/
@LayoutRes
abstract fun layoutId(): Int
/**
* 兩次更新的Binder內容是否相同
*/
abstract fun areContentsTheSame(other: Any): Boolean
/**
* 綁定ViewDataBinding
*/
fun bindViewDataBinding(binding: V) {
// 如果此次綁定與已綁定的一至,則不做綁定
if (this.binding === binding && binding.root.getTag(R.id.bindingVersion) == bindingViewVersion) return
binding.root.setTag(R.id.bindingVersion, ++bindingViewVersion)
onUnBindViewHolder()
this.binding = binding
binding.setVariable(variableId, this)
// 給 binding 綁定生命週期,方便觀察LiveData的值,進而更新UI。如果不綁定,LiveData的值改變時,UI不會更新
if (binding.root.context is LifecycleOwner) {
binding.lifecycleOwner = binding.root.context as LifecycleOwner
} else {
binding.lifecycleOwner = AlwaysActiveLifecycleOwner()
}
onBindViewHolder(binding)
// 及時更新綁定數據的View
binding.executePendingBindings()
}
/**
* 解綁ViewDataBinding
*/
fun unbindDataBinding() {
if (this.binding != null) {
onUnBindViewHolder()
this.binding = null
}
}
/**
* 綁定後對View的一些操作,如:賦值,修改屬性
*/
protected open fun onBindViewHolder(binding: V) {
}
/**
* 解綁操作
*/
protected open fun onUnBindViewHolder() {
}
/**
* 爲 Binder 綁定生命週期,在 {@link Lifecycle.Event#ON_RESUME} 時響應
*/
internal class AlwaysActiveLifecycleOwner : LifecycleOwner {
override fun getLifecycle(): Lifecycle = object : LifecycleRegistry(this) {
init {
handleLifecycleEvent(Event.ON_RESUME)
}
}
}
}
在values中定義一個ids.xml文件,給 ViewDataBinding 中的 root View設置Tag
<resources>
<item name="bindingVersion" type="id" />
</resources>
2.處理MultiTypeBinder中View的點擊事件
在ClickBinder中提供了兩種事件點擊方式 onClick 和 onLongClick,分別提供了攜帶參數和未帶參數方法
open class ClickBinder {
protected open var mOnClickListener: ((view: View, any: Any?) -> Unit)? = null
protected open var mOnLongClickListener: ((view: View, any: Any?) -> Unit)? = null
/**
* 設置View點擊事件
*/
open fun setOnClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder {
this.mOnClickListener = listener
return this
}
/**
* 設置View長按點擊事件
*/
open fun setOnLongClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder {
this.mOnLongClickListener = listener
return this
}
/**
* 觸發View點擊事件時回調,攜帶參數
*/
open fun onClick(view: View) {
onClick(view, this)
}
open fun onClick(view: View, any: Any?) {
if (mOnClickListener != null) {
mOnClickListener?.invoke(view, any)
} else {
if (BuildConfig.DEBUG) throw NullPointerException("OnClick事件未綁定!")
}
}
/**
* 觸發View長按事件時回調,攜帶參數
*/
open fun onLongClick(view: View) {
onLongClick(view, this)
}
open fun onLongClick(view: View, any: Any?){
if (mOnLongClickListener != null) {
mOnLongClickListener?.invoke(view, any)
} else {
if (BuildConfig.DEBUG) throw NullPointerException("OnLongClick事件未綁定!")
}
}
}
定義接口 OnViewClickListener ,若是給 Binder 中的 View 添加點擊事件時,可實現此接口。
interface OnViewClickListener {
// 不需要額外參數事件時,默認轉發給帶額外參數事件
fun onClick(view: View) {
onClick(view, null)
}
fun onClick(view: View, any: Any?) {
}
}
3.定義MultiTypeViewHolder
MultiTypeViewHolder繼承自RecyclerView.ViewHolder,傳入一個ViewDataBinding對象,在這裏對MultiTypeBinder中的ViewDataBinding對象進行解綁和綁定操作。
class MultiTypeViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root), AutoCloseable {
private var mAlreadyBinding: MultiTypeBinder<ViewDataBinding>? = null
/**
* 綁定Binder
*/
fun onBindViewHolder(items: MultiTypeBinder<ViewDataBinding>) {
// 如果兩次綁定的 Binder 不一致,則直接銷燬
if (mAlreadyBinding != null && items !== mAlreadyBinding) close()
// 開始綁定
items.bindViewDataBinding(binding)
// 保存綁定的 Binder
mAlreadyBinding = items
}
/**
* 銷燬綁定的Binder
*/
override fun close() {
mAlreadyBinding?.unbindDataBinding()
mAlreadyBinding = null
}
}
4.使用DiffUtil.ItemCallback進行差異性計算
在刷新列表時這裏使用了DiffUtil.ItemCallback來做差異性計算,方法說明:
- areItemsTheSame(oldItem: T, newItem: T):比較兩次MultiTypeBinder是否時同一個Binder
- areContentsTheSame(oldItem: T, newItem: T):比較兩次MultiTypeBinder的類容是否一致。
class DiffItemCallback<T : MultiTypeBinder<*>> : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.layoutId() == newItem.layoutId()
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.hashCode() == newItem.hashCode() && oldItem.areContentsTheSame(newItem)
}
}
5.定義MultiTypeAdapter
在MultiTypeAdapter中的邏輯實現思路如下:
- 使用 LinkedHashMap 來存儲每個 Binder 和 Binder 對應的 Type 值,確保順序。
- 在 getItemViewType(position: Int) 函數中添加 Binder 類型
- 在 onCreateViewHolder(parent: ViewGroup, viewType: Int) 方法中對 Binder 的 Layout 進行初始化,其中 inflateDataBinding 爲 Kotlin 擴展,主要是將 Layout 轉換爲一個 ViewDataBinding 的對象。
- 在 onBindViewHolder(holder: MultiTypeViewHolder, position: Int) 方法中調用 Binder 中的綁定方法,用以綁定數據。
- 使用 AsyncListDiffer 工具返回當前列表數據和刷新列表,具體用法下文說明
class MultiTypeAdapter: RecyclerView.Adapter<MultiTypeViewHolder>(){
// 使用後臺線程通過差異性計算來更新列表
private val mAsyncListChange by lazy { AsyncListDiffer(this, DiffItemCallback<MultiTypeBinder<*>>()) }
// 存儲 Layout 和 Layout Type
private var mHashCodeViewType = LinkedHashMap<Int, MultiTypeBinder<*>>()
init {
setHasStableIds(true)
}
fun notifyAdapterChanged(binders: List<MultiTypeBinder<*>>) {
mHashCodeViewType = LinkedHashMap()
binders.forEach {
mHashCodeViewType[it.hashCode()] = it
}
mAsyncListChange.submitList(mHashCodeViewType.map { it.value })
}
fun notifyAdapterChanged(binders: MultiTypeBinder<*>) {
mHashCodeViewType = LinkedHashMap()
mHashCodeViewType[binders.hashCode()] = binders
mAsyncListChange.submitList(mHashCodeViewType.map { it.value })
}
override fun getItemViewType(position: Int): Int {
val mItemBinder = mAsyncListChange.currentList[position]
val mHasCode = mItemBinder.hashCode()
// 如果Map中不存在當前Binder的hasCode,則向Map中添加當前類型的Binder
if (!mHashCodeViewType.containsKey(mHasCode)) {
mHashCodeViewType[mHasCode] = mItemBinder
}
return mHasCode
}
override fun getItemId(position: Int): Long = position.toLong()
override fun getItemCount(): Int = mAsyncListChange.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTypeViewHolder {
try {
return MultiTypeViewHolder(parent.inflateDataBinding(mHashCodeViewType[viewType]?.layoutId()!!))
}catch (e: Exception){
throw NullPointerException("不存在${mHashCodeViewType[viewType]}類型的ViewHolder!")
}
}
@Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: MultiTypeViewHolder, position: Int) {
val mCurrentList = mAsyncListChange.currentList[position] as MultiTypeBinder<ViewDataBinding>
holder.itemView.tag = mCurrentList
holder.onBindViewHolder(mCurrentList)
}
}
6.定義擴展Adapters文件
/**
* 創建一個MultiTypeAdapter
*/
fun createMultiTypeAdapter(recyclerView: RecyclerView, layoutManager: RecyclerView.LayoutManager): MultiTypeAdapter {
recyclerView.layoutManager = layoutManager
val mMultiTypeAdapter = MultiTypeAdapter()
recyclerView.adapter = mMultiTypeAdapter
// 處理RecyclerView的觸發回調
recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View?) {
mMultiTypeAdapter.onDetachedFromRecyclerView(recyclerView)
}
override fun onViewAttachedToWindow(v: View?) { }
})
return mMultiTypeAdapter
}
/**
* MultiTypeAdapter擴展函數,重載MultiTypeAdapter類,使用invoke操作符調用MultiTypeAdapter內部函數。
*/
inline operator fun MultiTypeAdapter.invoke(block: MultiTypeAdapter.() -> Unit): MultiTypeAdapter {
this.block()
return this
}
/**
* 將Layout轉換成ViewDataBinding
*/
fun <T : ViewDataBinding> ViewGroup.inflateDataBinding(layoutId: Int): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, this, false)!!
/**
* RecyclerView方向註解
*/
@IntDef(
Orientation.VERTICAL,
Orientation.HORIZONTAL
)
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
annotation class Orientation{
companion object{
const val VERTICAL = RecyclerView.VERTICAL
const val HORIZONTAL = RecyclerView.HORIZONTAL
}
}
7.MultiTypeAdapter使用
- 創建 MultiTypeAdapter
private val mAdapter by lazy { createMultiTypeAdapter(binding.recyclerView, LinearLayoutManager(this)) }
- 將 Binder 添加到 Adapter 中
mAdapter.notifyAdapterChanged(mutableListOf<MultiTypeBinder<*>>().apply {
add(TopBannerBinder().apply {
setOnClickListener(this@MainActivity::onClick)
})
add(CategoryContainerBinder(listOf("男裝", "女裝", "鞋靴", "內衣內飾", "箱包", "美妝護膚", "洗護", "腕錶珠寶", "手機", "數碼").map {
CategoryItemBinder(it).apply {
setOnClickListener(this@MainActivity::onClick)
}
}))
add(RecommendContainerBinder((1..8).map { RecommendGoodsBinder().apply {
setOnClickListener(this@MainActivity::onClick)
} }))
add(HorizontalScrollBinder((0..11).map { HorizontalItemBinder("$it").apply {
setOnClickListener(this@MainActivity::onClick)
} }))
add(GoodsGridContainerBinder((1..20).map { GoodsBinder(it).apply {
setOnClickListener(this@MainActivity::onClick)
} }))
})
- 點擊事件處理,在Activity或Fragment中實現 OnViewClickListener 接口,重寫 onClick 方法
override fun onClick(view: View, any: Any?) {
when(view.id) {
R.id.top_banner -> {
any as TopBannerBinder
toast(view, "點擊Banner")
}
R.id.category_tab -> {
any as CategoryItemBinder
toast(view,"點擊分類+${any.title}")
}
R.id.recommend_goods -> {
any as RecommendGoodsBinder
toast(view, "點擊精選會場Item")
}
R.id.theme_index -> {
any as HorizontalItemBinder
toast(view, "點擊主題會場${any.index}")
}
R.id.goods_container -> {
any as GoodsBinder
toast(view, "點擊商品${any.index}")
}
}
}
8.AsyncListDiffer
一個在後臺線程中使用DiffUtil計算兩個列表之間的差異的輔助類。AsyncListDiffer 的計算主要submitList 方法中。
Tip: 調用submitList()方法傳遞數據時,需要創建一個新的集合。
public class AsyncListDiffer<T> {
// 省略其它代碼......
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList, @Nullable final Runnable commitCallback) {
// 定義變量 runGeneration 遞增生成,用於緩存當前預執行線程的次數的最大值
final int runGeneration = ++mMaxScheduledGeneration;
// 首先判斷 newList 與 AsyncListDiffer 中緩存的數據集 mList 是否爲同一個對象,如果是的話,直接返回。也就是說,調用 submitList() 方法所傳遞數據集時,需要new一個新的List。
if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
// 判斷 newList 是否爲null。若 newList 爲 null,將移除所有 Item 的操作並分發給 ListUpdateCallback,mList 置爲 null,同時將只讀List - mReadOnlyList 清空
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// 判斷 mList 是否爲null。若 mList 爲null,表示這是第一次向 Adapter 添加數據集,此時將添加最新數據集操的作分發給 ListUpdateCallback,將 mList 設置爲 newList, 同時將 newList 賦值給 mReadOnlyList
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
final List<T> oldList = mList;
// 通過AsyncDifferConfig獲取到一個後臺線程,在後臺線程中使用DiffUtil對兩個List進行差異性比較
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
}
// If both items are null we consider them the same.
return oldItem == null && newItem == null;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
}
if (oldItem == null && newItem == null) {
return true;
}
throw new AssertionError();
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
}
throw new AssertionError();
}
});
// 使用AsyncDifferConfig中的主線程更新UI,先判斷遞增生成的 runGeneration 變量是否與 AsyncListDiffer 中當前與執行線程的次數的最大值是否相等,如果相等,將 newList 賦值給 mList ,將 newList添加到只讀集合 mReadOnlyList 中,然後通知列表更新。
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult, @Nullable Runnable commitCallback) {
final List<T> previousList = mReadOnlyList;
mList = newList;
// 將 newList 添加到 mReadOnlyList 中
mReadOnlyList = Collections.unmodifiableList(newList);
// 通知列表更新
diffResult.dispatchUpdatesTo(mUpdateCallback);
onCurrentListChanged(previousList, commitCallback);
}
// 省略其它代碼......
}
9.ListUpdateCallback
操作列表更新的接口,此類可與DiffUtil一起使用,以檢測兩個列表之間的變化。至於ListUpdateCallback接口具體做了那些事兒,切看以下函數:
onInserted 在指定位置插入Item時調用,position 指定位置, count 插入Item的數量
void onInserted(int position, int count);
onRemoved 在刪除指定位置上的Item時調用,position 指定位置, count 刪除的Item的數量
void onRemoved(int position, int count);
onMoved 當Item更改其在列表中的位置時調用, fromPosition 當前Item在移動之前的位置,toPosition 當前Item在移動之後的位置
void onMoved(int fromPosition, int toPosition);
onChanged 在指定位置更新Item時調用,position 指定位置,count 要更新的Item個數,payload 可選參數,值爲null時表示全部更新,否則表示局部更新。
void onChanged(int position, int count, @Nullable Object payload);
10.ListUpdateCallback的實現類AdapterListUpdateCallback
AdapterListUpdateCallback的作用是將更新事件調度回調給Adapter,如下:
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final RecyclerView.Adapter mAdapter;
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
11.AsyncDifferConfig
AsyncDifferConfig的角色很簡單,是一個DiffUtil.ItemCallback的配置類,其內部創建了一個固定大小的線程池,提供了兩種線程,即後臺線程和主線程,主要用於差異性計算和更新UI。AsyncDifferConfig核心代碼如下:
public final class AsyncDifferConfig<T> {
// 省略其他代碼......
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Nullable
public Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public Executor getBackgroundThreadExecutor() {
return mBackgroundThreadExecutor;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public DiffUtil.ItemCallback<T> getDiffCallback() {
return mDiffCallback;
}
public static final class Builder<T> {
// 省略其他代碼......
@NonNull
public AsyncDifferConfig<T> build() {
if (mBackgroundThreadExecutor == null) {
synchronized (sExecutorLock) {
if (sDiffExecutor == null) {
// 創建一個固定大小的線程池
sDiffExecutor = Executors.newFixedThreadPool(2);
}
}
mBackgroundThreadExecutor = sDiffExecutor;
}
return new AsyncDifferConfig<>(
mMainThreadExecutor,
mBackgroundThreadExecutor,
mDiffCallback);
}
// 省略其他代碼......
}
}