如何用100行代碼構(gòu)建一個多樣式RecyclerView適配器

  • RecyclerView多樣式布局的框架有很多,大家熟悉已久的BaseRecyclerViewAdapterHelper摔吏,vlayout等等,包括google在新版本RecyclerView中推出的MergeAdapter物喷,ConcatAdapter等起宽。既然有這么多現(xiàn)成的框架,為什么還要去自己編寫一個呢?很多時候這些框架考慮的都是常見通用性場景瘫俊,在某些奇葩的產(chǎn)品設(shè)計需求中也許并不適用蛛芥,這也是這篇文章出現(xiàn)的原因。

  • 目前多樣式適配器框架總體的設(shè)計方案有兩種军援,一種是BaseRecyclerViewAdapterHelper這種極度簡化開發(fā)者編寫的代碼量仅淑,用最簡潔的方式去實現(xiàn)多樣式。另外一種就是vlayout這種胸哥,將每種樣式設(shè)計為單獨模塊(子適配器)涯竟,視圖創(chuàng)建,數(shù)據(jù)綁定都在該模塊內(nèi)部處理,再用一個主適配器將這些子適配器進(jìn)行包裝關(guān)聯(lián)庐船。本文采用的是第二種設(shè)計方式银酬。

由于代碼量極少,下面就不多說了筐钟,直接貼代碼:

  1. 視圖構(gòu)建器揩瞪,保持原生api命名方式
<ViewTypeCreator.kt>
abstract class ViewTypeCreator<T, VH : ViewHolder> {

    abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH

    abstract fun onBindViewHolder(holder: VH, data: T)

    abstract fun match(data: T): Boolean

    open fun getItemId(position: Int) = RecyclerView.NO_ID

}
  1. 數(shù)據(jù)適配器
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    private val dataCache: ArrayList<Class<*>> = ArrayList()
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()

    abstract fun getData(position: Int): Any

    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)
        val viewType = getCreatorViewType(data)
        return if (viewType != -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    override fun getItemId(position: Int): Long {
        val itemViewType = getItemViewType(position)
        val viewCreator = getViewCreatorByViewType(itemViewType)
        return viewCreator.getItemId(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)
    }

    private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data::class.java
        var viewType: Int
        val index = dataCache.indexOf(clazz)
        if (dataCache.size > 0 && index != -1) {
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {
                        viewType = id
                        if (viewTypeCache.indexOfKey(viewType) < 0) {
                            viewTypeCache.put(viewType, viewCreator)
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))
                }
                return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
    }

    private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }
}

好了,代碼就這么多篓冲,下面簡單介紹下原理:

  1. 視圖構(gòu)建器不過多介紹李破,主要就是抽象出視圖構(gòu)建的方法,這里只重點說下match這個方法:
fun match(data: T): Boolean
  • 這里面接收一個數(shù)據(jù)類型參數(shù)壹将,需要返回一個Boolean值嗤攻,通常的產(chǎn)品設(shè)計常見一種數(shù)據(jù)類型應(yīng)該是對應(yīng)一種視圖類型,但是就是存在這么奇葩的設(shè)計诽俯,比如返回一個Person數(shù)據(jù)類型妇菱,如果sex為男需要展示一種樣式(左右布局:頭像在左邊,右邊顯示簡介)暴区,如果sex為女則需要展示另一種樣式(上下布局:頭像在中間闯团,下面顯示簡介),這樣的場景就可以這樣定義兩種視圖:
<ManCreator.kt>
class ManCreator : ViewTypeCreator<Person, ManCreator.Holder>() {
    ...
    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_man, parent, false))
    }
    // 當(dāng)person為男性的時候會通過該Creator創(chuàng)建視圖
    override fun match(data: Person) = data.sex == Sex.MAN
}

<WomanCreator.kt>
class WomanCreator : ViewTypeCreator<Person, WomanCreator.Holder>() {
    ...
    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_female, parent, false))
    }
    // 當(dāng)person為女性的時候會通過該Creator創(chuàng)建視圖
    override fun match(data: Person) = data.sex == Sex.WOMAN
}
  1. 適配器仙粱,當(dāng)然是繼承RecyclerView.Adapter
  • 先介紹一下3個緩存:
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    // 按數(shù)據(jù)順序房交,存儲數(shù)據(jù)類型
    private val dataCache: ArrayList<Class<*>> = ArrayList()// [DataType]
    // 存儲ViewTypeCreator,索引為該數(shù)據(jù)類型在緩存的下標(biāo)缰盏,由于一個數(shù)據(jù)類型可對應(yīng)多個Creator涌萤,
    // 因此使用集合存儲
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()// DataTypeIndex - [ViewTypeCreators]
    // 存儲ViewTypeCreator,索引為對應(yīng)的viewType口猜,一對一的關(guān)系负溪,該緩存是為了快速查找
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()// ViewType - ViewTypeCreator
}
  • 接下來看下注冊視圖構(gòu)建器的方法:
<MultiTypeAdapter.kt>
    // 自動讀取泛型Data數(shù)據(jù)的類型進(jìn)行存儲
    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            // 如果沒有存儲過進(jìn)行緩存
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        // 初始化該Data數(shù)據(jù)類型對應(yīng)的ViewTypeCreator集合
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        // 構(gòu)造唯一標(biāo)識作為索引
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }
  • 最后按照RecyclerView.Adapter調(diào)用流程分析下原理:
<MultiTypeAdapter.kt>
    // 獲取當(dāng)前索引的數(shù)據(jù)
    abstract fun getData(position: Int): Any

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)// 1.獲取當(dāng)前索引對應(yīng)的數(shù)據(jù)
        val viewType = getCreatorViewType(data)// 2.根據(jù)當(dāng)前數(shù)據(jù)獲取ViewType
        return if (viewType != -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data::class.java// 獲取data數(shù)據(jù)的class類型
        var viewType: Int
        val index = dataCache.indexOf(clazz)// 查找出該data在緩存中的索引
        if (dataCache.size > 0 && index != -1) {// 判斷是否注冊過該data對應(yīng)的ViewTypeCreator
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]// 獲取該data對應(yīng)的ViewTypeCreator集合
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {// 一個data數(shù)據(jù)對應(yīng)多種viewType
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {// 遍歷ViewTypeCreator,并且判斷是否符合條件济炎,也就是上面說的match匹配方法
                        viewType = id// viewType就是上面根據(jù)ViewTypeCreator實例獲取到的唯一id:System.identityHashCode
                        if (viewTypeCache.indexOfKey(viewType) < 0) {// 如果viewTypeCache沒有緩存過川抡,則添加入緩存
                            viewTypeCache.put(viewType, viewCreator)// 這一級緩存是為了能夠快速根據(jù)viewType查找對應(yīng)的ViewTypeCreator
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {// 一個data數(shù)據(jù)對應(yīng)一種viewType
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))
                }
                return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator,并且通過onCreateViewHolder構(gòu)建Holer視圖
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator崖堤,并且通過onBindViewHolder綁定數(shù)據(jù)
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)
    }

    // 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator
    private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }

以上就是MultiTypeAdapter的所有代碼

  1. 下面舉個使用案例:
  • 先定義一個適配器,繼承MultiTypeAdapter
class SampleAdapter : MultiTypeAdapter() {

    val data = mutableListOf<Any>()

    override fun getData(position: Int) = data[position]

    override fun getItemCount() = data.size
}
  • 定義兩種展示文字和一種展示圖片的viewType
// 文字?jǐn)?shù)據(jù)類型定義:包含主標(biāo)題和副標(biāo)題
data class Title(val mainTitle: String = "", val subTitle: String = "")
// 文字樣式1
class MainTitleCreator : ViewTypeCreator<Title, MainTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.main_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_main_title, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Title) {
        holder.title.text = data.mainTitle
    }

    override fun match(data: Title): Boolean {
        return !TextUtils.isEmpty(data.mainTitle) && TextUtils.isEmpty(data.subTitle)
    }
}
// 文字樣式2
class SubTitleCreator : ViewTypeCreator<Title, SubTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.sub_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_sub_title, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Title) {
        holder.title.text = data.subTitle
    }

    override fun match(data: Title): Boolean {
        return !TextUtils.isEmpty(data.subTitle) && TextUtils.isEmpty(data.mainTitle)
    }
}

// 圖片樣式
class ImageCreator : ViewTypeCreator<Int, ImageCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val image: ImageView = itemView.findViewById(R.id.image)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_image, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Int) {
        holder.image.setImageResource(data)
    }

    override fun match(data: Int): Boolean {
        return false
    }
}
  • 注冊viewTypeCreator
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        val adapter = SampleAdapter()
        // image type
        adapter.registerCreator(ImageCreator())
        // the same bean but different view type
        adapter.registerCreator(MainTitleCreator())
        adapter.registerCreator(SubTitleCreator())
        for (i in 0..10) {
            adapter.data.add(R.drawable.test)
            adapter.data.add("I am string")
            adapter.data.add(Title("I am MainTitle"))
            adapter.data.add(Title("", "I am SubTitle"))
        }
        recycler_view.adapter = adapter
    }

最終展示效果如下:

device-2020-07-24-103721.png

如果后續(xù)產(chǎn)品設(shè)計新增了樣式木柬,只需要定義新的ViewTypeCreator淹办,再注冊到MultiTypeAdapter中,然后在數(shù)據(jù)集中添加對應(yīng)類型的數(shù)據(jù)即可速挑,適配器和數(shù)據(jù)梗摇,視圖構(gòu)建完全解耦。

可以直接遠(yuǎn)程依賴引入流纹,項目地址:https://github.com/seagazer/multitype

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诸迟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌壁公,老刑警劉巖快耿,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異搪花,居然都是意外死亡丁稀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門枯跑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敛助,“玉大人攻臀,你說我怎么就攤上這事设联』煌牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵粘招,是天一觀的道長啥寇。 經(jīng)常有香客問我,道長洒扎,這世上最難降的妖魔是什么辑甜? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮袍冷,結(jié)果婚禮上磷醋,老公的妹妹穿的比我還像新娘。我一直安慰自己胡诗,他們只是感情好邓线,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布淌友。 她就那樣靜靜地躺著,像睡著了一般骇陈。 火紅的嫁衣襯著肌膚如雪震庭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天你雌,我揣著相機(jī)與錄音器联,去河邊找鬼。 笑死婿崭,一個胖子當(dāng)著我的面吹牛拨拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播氓栈,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼渣磷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了授瘦?” 一聲冷哼從身側(cè)響起醋界,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奥务,沒想到半個月后物独,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袜硫,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡氯葬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了婉陷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帚称。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秽澳,靈堂內(nèi)的尸體忽然破棺而出闯睹,到底是詐尸還是另有隱情,我是刑警寧澤担神,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布楼吃,位于F島的核電站,受9級特大地震影響妄讯,放射性物質(zhì)發(fā)生泄漏孩锡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一亥贸、第九天 我趴在偏房一處隱蔽的房頂上張望躬窜。 院中可真熱鬧,春花似錦炕置、人聲如沸荣挨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽默垄。三九已至此虑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間口锭,已是汗流浹背寡壮。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留讹弯,地道東北人况既。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像组民,于是被迫代替她去往敵國和親棒仍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344