Android app 优雅展示复杂列表

问题描述

Android 应用列表展示功能非常常见。特别是电商 新闻 这类的应用。一般主页就是一个复杂的列表。复杂列表一般包含多种展示样式,每一种样式或多种样式 对应一种数据结构,每种样式包含N个N>=0 item。

示例效果图

为了简化问题,每种颜色对应一种展示效果。

实现思路

经过不断的重构迭代,实现了一个非常优雅的 RecyclerView Adapter。可以展示各种复杂列表,而且代码简洁易懂,易扩展,代码高度复用。
列表中每一个样式的核心逻辑有两个,一个是样式对应的布局,也就是展示效果,另一个是布局中控件如何和数据关联。这两个功能对应接口ViewTypeDelegateAdapter 中 onCreateViewHolder和onBindViewHolder。

 interface ViewTypeDelegateAdapter {
        fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder
        fun onBindViewHolder(holder: CommonViewHolder, position: Int, data: Any?)
    }

总的实现思路是基于委托模式,每一种展示样式委托给一个轻量级的DelegateAdapter,实现展示效果。这个类非常简单,只需要实现接口中的两个函数。

核心功能由MultiTypeAdapter 实现,它负责根据每种展示样式委托给对应的代理。还有RecyclerView.Adapter
中的一些其他逻辑

为了实现委托定义了一个泛型数据结构

    data class CommonAdapterItem(val data: Any, val type: Int, var spanSize: Int = 1)

把要展示的数据使用CommonAdapterItem 包装一下,设置对应的展示样式和GridLayoutManager 对应的spanCount。然后把数据塞给 MultiTypeAdapter ,由它委托给对应的实现类。

核心代码和demo

代码已经在多个项目中使用过,经过多次优化已经非常的简单高效。为了方便查看,把所有的功能放在了一个类中,即使这样也不到200行代码

const val RED = 1
const val GREEN = 2
const val BLUE = 3
const val YELLOW = 4
const val PURPLE = 5


class MultiTypeAdapter : RecyclerView.Adapter<CommonViewHolder>() {

    interface ViewTypeDelegateAdapter {
        fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder
        fun onBindViewHolder(holder: CommonViewHolder, position: Int, data: Any?)
    }

    private val mContent: MutableList<CommonAdapterItem> = mutableListOf()

    private val mFactory = DelegateAdapterFactory()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder {
        return mFactory.getDelegateAdapter(viewType).onCreateViewHolder(parent, viewType)
    }

    override fun onBindViewHolder(holder: CommonViewHolder, position: Int) {
        val type = mContent[position].type
        mFactory.getDelegateAdapter(type)
            .onBindViewHolder(holder, position, mContent[position].data)
    }

    override fun getItemViewType(position: Int): Int = mContent[position].type

    override fun getItemCount(): Int = mContent.size

    fun addItems(
        items: Collection<Any>?,
        type: Int = RED,
        append: Boolean = false,
        spanSize: Int = 1
    ) {
        items?.let {
            if (!append) {
                mContent.clear()
            }
            mContent.addAll(transform(items, type, spanSize))
            notifyDataSetChanged()
        }
    }

//    fun setOnItemClick(callback: (position: Int, data: Any?, action: Int, extra: Any?) -> Unit) {
//        mFactory.onItemClick = callback
//    }
//
//    fun setOnItemClick(callback: (position: Int, data: Any?, action: Int) -> Unit) {
//        mFactory.onItemClick = { position, data, action, _ ->
//            callback(position, data, action)
//        }
//    }

    fun getItemType(position: Int): Int = mContent[position].type

    fun clear() {
        mContent.clear()
        notifyDataSetChanged()
    }

    fun removeItem(position: Int) {
        mContent.removeAt(position)
        notifyItemRemoved(position)
    }

    fun changeItem(position: Int, item: Any?, type: Int, spanSize: Int = 1) {
        item?.let {
            if (position < mContent.size) {
                mContent[position] = transform(item, type, spanSize)
                notifyItemChanged(position)
            }
        }
    }

//    fun addItem(item: Collection<Any>?, type: Int = RED, append: Boolean = false) {
//        item?.let {
//            if (!append) {
//                mContent.clear()
//            }
//            mContent.add(CommonAdapterItem(item.toMutableList(), type))
//            notifyDataSetChanged()
//        }
//    }

    fun addItem(item: Any?, type: Int = RED, append: Boolean = false, spanSize: Int = 1) {
        item?.let {
            if (!append) {
                mContent.clear()
            }
            mContent.add(transform(item, type, spanSize))
            notifyDataSetChanged()
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        val manager = recyclerView.layoutManager
        if (manager is GridLayoutManager) {
            manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    return mContent[position].spanSize
                }
            }
        }
    }

    private fun transform(item: Any, type: Int, spanSize: Int = 1): CommonAdapterItem {
        return CommonAdapterItem(item, type, spanSize)
    }

    private fun transform(
        items: Collection<Any>,
        type: Int,
        spanSize: Int = 1
    ): List<CommonAdapterItem> {
        return items.map { CommonAdapterItem(it, type, spanSize) }
    }

    data class CommonAdapterItem(val data: Any, val type: Int, var spanSize: Int = 1)

    open class BaseDelegateAdapter(protected val layoutId: Int) : ViewTypeDelegateAdapter {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
            return CommonViewHolder(view)
        }

        override fun onBindViewHolder(holder: CommonViewHolder, position: Int, data: Any?) {}
    }

    class DelegateAdapterFactory {
        private val adapterCache = SparseArray<ViewTypeDelegateAdapter>() //提高性能

        fun getDelegateAdapter(type: Int): ViewTypeDelegateAdapter {
            var adapter = adapterCache[type]
            if (adapter == null) {
                adapter = when (type) {
                    RED, GREEN, BLUE, YELLOW, PURPLE -> object :
                        BaseDelegateAdapter(R.layout.viewholder_example) {
                        override fun onBindViewHolder(
                            holder: CommonViewHolder,
                            position: Int,
                            data: Any?
                        ) {
                            super.onBindViewHolder(holder, position, data)
                            if(data is String){
                                holder.get<View>(R.id.content).setBackgroundColor(Color.parseColor(data))
                            }
                        }
                    }
                    else -> {
                        BaseDelegateAdapter(android.R.layout.simple_list_item_1)
                    }
                }
                adapterCache.put(type, adapter)

            }
            return adapter
        }
    }
}

demo对应的示例代码

       val adapter = MultiTypeAdapter()
        recyclerView.layoutManager = GridLayoutManager(this,4)
        recyclerView.adapter = adapter
        adapter.addItem("#FF0000",RED, true,4)
        val greenList = MutableList(10){
            "#00FF00"
        }
        val blueList = MutableList(5){
            "#0000FF"
        }
        val yellowList = MutableList(5){
            "#FFFF00"
        }
        val purpleList = MutableList(5){
            "#AA66CC"
        }
        adapter.addItems(greenList, GREEN,true,1)
        adapter.addItems(blueList, BLUE,true,4)
        adapter.addItems(yellowList, YELLOW,true,2)
        adapter.addItems(purpleList, PURPLE, true,3)      
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • RecyclerView控件从2014发布以来,目前已经普遍用于项目中,来承载各种列表内容。同时,列表样式也随着项...
    Dragon_Boat阅读 5,618评论 12 8
  • 内容 抽屉菜单 ListView WebView SwitchButton 按钮 点赞按钮 进度条 TabLayo...
    8ba406212441阅读 5,464评论 0 5
  • 懒得处理样式了, 将就着看吧. 官网地址: https://developer.android.com/topic...
    Reddington_604e阅读 1,640评论 0 1
  • 秋天的风携着微凉的雨 仪态万千地走在丰腴的土地上 那些饱满的收获已经悄然囤起 遍地落红和满天飞卷的黄叶 正在上演着...
    一弯虹阅读 644评论 11 24
  • 雨天散步,和平时没什么不同,只是多了一个移动的屋顶。彩色的。 大雨如瀑,小雨如幕。 想想白娘子,为了成就自己的爱情...
    炉子河南阅读 516评论 0 1