RecyclerView優雅的實現複雜列表

前言

在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);
        }
       // 省略其他代碼...... 
    }
}

點擊查看源碼

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