Android进阶宝典 -- 利用装饰器为RecyclerView添加头部尾部

293 阅读6分钟

装饰器模式,如果伙伴们对于这个概念不熟悉的话,但你们一定在日常的工作中用到过,例如

val inputStream = BufferedInputStream(FileInputStream("abc.txt"))

我们常用的文件读写流,经常会用多个流相互包裹最终生成我们想要的流,这种设计模式其实就是装饰器模式。那么对于装饰器模式的概念,就是不通过继承的方式,来扩展某个对象的功能,达到我们想要实现的效果

1 装饰器模式引入

首先,我们通过一个简单的示例,来了解装饰器设计模式的精妙之处。

interface ICar {
    fun run()
}

这是一个经典的车类接口,其中Car是一个基础车类,功能比较单一

class Car : ICar {
    override fun run() {
        Log.e("TAG","car can run")
    }
}

另外几个Car,功能逐渐多了起来

class BenzCar : ICar{
    override fun run() {
        Log.e("TAG","car can run")
        Log.e("TAG","car has fly")
    }
}
class AudiCar : ICar {
    override fun run() {
        Log.e("TAG","car can run")
        Log.e("TAG","car can fly")
        Log.e("TAG","car can swim")
    }
}

如果我们想要使用某个Car,可以直接实例化

val car = BenzCar()
car.run()

伙伴们想一想,如果想要在原来的基础上拓展功能,岂不是每次都需要创建一个实例类,并实现其中的功能,如此一来会写很多模版代码,那么装饰器设计模式恰好能够解决这个问题。

装饰器模式,我们可以认为是锦上添花,在原来的基础上扩展某个对象的能力。例如AudiCar是在BenzCar的基础上做了扩展,那么就可以将BenzCar作为参数传入到AudiCar的构造方法中

class AudiCar : ICar {

    private var car: ICar? = null

    constructor(car: BenzCar) {
        this.car = car
    }

    override fun run() {
        car?.run()
        Log.e("TAG", "car can swim")
    }
}

在最终调用的时候,就可以创建BenzCar实例,传入AudiCar构造方法中,在执行run方法时,会兼并执行BenzCar的run方法。

val car = BenzCar()
val audiCar = AudiCar(car)
audiCar.run()

2 RecyclerView添加头部和尾部

与ListView不同的是,RecyclerView并不支持添加头部和尾部,但是RecyclerView使用的场景是比ListView要多的多,因此在实际的开发中,我们经常需要添加尾部,例如列表滑动到底部的时候,需要提示用户列表已经滑到了底部不能再滑动了。

2.1 设计思路

我们在使用RecyclerView的时候,经常使用RecyclerView.Adapter,它其实是展示的就是我们请求到的列表数据,但是并不包含头部和尾部。

image.png 因此,我们就可以在RecyclerView.Adapter的基础上做两个功能扩展,给RecyclerView加上头部和尾部

2.2 源码实现

2.2.1 基础框架搭建

看下上图,因为是要在RecyclerView.Adapter的基础上做装饰,因此在WrapperListAdapter的构造方法中传入原始的RecyclerView Adapter。

/**

 * 该类为RecyclerView的装饰器Aadpter,可添加头部与尾部
 * 支持添加多个头部与尾部
 * 支持删除头部与尾部
 *
 * @param realAdapter 该Adapter仅展示数据,具体添加头尾部操作在WrapperListAdapter中完成
 */
class WrapperListAdapter(
    val realAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), IWrapperList {

    /**存储头部View的集合*/
    private val headerList: MutableList<View> by lazy {
        mutableListOf()
    }

    /**存储尾部View的集合*/
    private val footerList: MutableList<View> by lazy {
        mutableListOf()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    }

    override fun getItemCount(): Int {
        return realAdapter.itemCount + headerList.size + footerList.size
    }

    override fun addHeaderView(header: View) {
        if (!headerList.contains(header)) {
            headerList.add(header)
            refreshList()
        }
    }

    override fun removeHeaderView(header: View) {
        if (headerList.contains(header)) {
            headerList.remove(header)
            refreshList()
        }
    }

    override fun addFooterView(foot: View) {
        if (!footerList.contains(foot)) {
            footerList.add(foot)
            refreshList()
        }
    }

    override fun removeFooterView(foot: View) {
        if (footerList.contains(foot)) {
            footerList.remove(foot)
            refreshList()
        }
    }

    override fun refreshList() {
        notifyDataSetChanged()
    }
}

因为我们支持添加多个头部和尾部,因此可以存储在List集合中,最终getItemCount返回的Item个数就是原始适配器的itemCount(网络请求获取到的列表数据个数)+ headerList的个数 + footerList的个数。

2.2.2 核心逻辑处理

image.png

假设当前的场景,相当于一共有3种样式,其中position 0-2是头部样式,3-200是主体样式,201为尾部样式,因此可以通过position区分不同的样式,从而在onCreateViewHolder中返回。

override fun onCreateViewHolder(parent: ViewGroup, position: Int): RecyclerView.ViewHolder {

    /**判断头部样式展示*/
    if (headerList.isNotEmpty() && position in 0 until headerList.size) {
        return createHeaderFooterViewHolder(headerList[position])
    }

    /**判断主体样式展示*/
    val startPosition = if (headerList.isNotEmpty()) headerList.size else 0
    val endPosition =
        if (headerList.isNotEmpty()) headerList.size + realAdapter.itemCount else realAdapter.itemCount
    if (position in startPosition until endPosition) {
        return realAdapter.onCreateViewHolder(parent, position)
    }

    /**判断尾部样式展示,eg 如果能走到这里,说明尾部还有view没有展示*/
    return createHeaderFooterViewHolder(footerList[position - endPosition]) /**注意这里的取值*/
}

/**
 * 创建头部或者尾部的ViewHolder
 */
private fun createHeaderFooterViewHolder(view: View): RecyclerView.ViewHolder {
    return HeaderFooterViewHolder(view)
}

其实RecyclerView.Adapter的onCreateViewHolder方法中第二个参数为viewType类型,而不是position,这个需要做一次转换,将每个ItemView的索引作为ItemViewType返回。

override fun getItemViewType(position: Int): Int {
    return position
}

那么我们先创建一个初级的Adapter,通过加载后展示效果

class SimpleAdapter : RecyclerView.Adapter<SimpleItemHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleItemHolder {
        return SimpleItemHolder(
            LayoutSimpleItemBinding.bind(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.layout_simple_item, parent, false)
            )
        )
    }

    override fun onBindViewHolder(holder: SimpleItemHolder, position: Int) {
        holder.binding.itemView.text = "position==$position"
    }

    override fun getItemCount(): Int {
        return 100
    }
}

class SimpleItemHolder(
    val binding: LayoutSimpleItemBinding
) : RecyclerView.ViewHolder(binding.root)

image.png

使用WrapperListAdapter包裹,然后就可以添加头部和尾部

rv_wrapper = findViewById(R.id.rv_wrapper)
val wrapperAdapter = WrapperListAdapter(SimpleAdapter())
rv_wrapper.adapter = wrapperAdapter
rv_wrapper.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

wrapperAdapter.addHeaderView(
    LayoutInflater.from(this).inflate(R.layout.layout_header, rv_wrapper, false)
)

image.png

这里我们看到,虽然头部已经添加上了,但是主体列表的数据没有展示,这就是因为没有实现onBindViewHolder。

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

    if (headerList.isNotEmpty() && position in 0 until headerList.size) {
        return
    }
    /**主体数据展示*/
    val startPosition = if (headerList.isNotEmpty()) headerList.size else 0
    val endPosition =
        if (headerList.isNotEmpty()) headerList.size + realAdapter!!.itemCount else realAdapter!!.itemCount
    if (position in startPosition until endPosition) {
        realAdapter?.onBindViewHolder(holder, position)
    }
}

image.png

通过上图我们看到,数据已经展示出来了,但是还有一个问题就是,因为主体列表中,加载数据时还是会从请求的列表中加载数据,因此我们在传入position的时候,需要考虑是否存在头部,不然会导致数组越界。

val startPosition = if (headerList.isNotEmpty()) headerList.size else 0
val endPosition =
    if (headerList.isNotEmpty()) headerList.size + realAdapter.itemCount else realAdapter.itemCount
if (position in startPosition until endPosition) {
    realAdapter.onBindViewHolder(holder, position - headerList.size) /**注意这里的position*/
}

2.2.3 RecyclerView重新改造

既然我们现在已经封装好了可装饰头部和尾部的Adapter,是不是也可以像ListView那样,作为一个控件来提供支持添加头部和尾部

class WrapperRecyclerView : RecyclerView {

    private var wrapperListAdapter: WrapperListAdapter? = null

    @JvmOverloads
    constructor(context: Context, attributes: AttributeSet? = null) : super(context, attributes)

    override fun setAdapter(adapter: Adapter<ViewHolder>?) {
        wrapperListAdapter = WrapperListAdapter(adapter!!)
        super.setAdapter(wrapperListAdapter)
    }

    fun addHeaderView(header: View) {
        wrapperListAdapter?.addHeaderView(header)
    }

    fun removeHeaderView(header: View) {
        wrapperListAdapter?.removeHeaderView(header)
    }

    fun addFooterView(foot: View) {
        wrapperListAdapter?.addFooterView(foot)
    }

    fun removeFooterView(foot: View) {
        wrapperListAdapter?.removeFooterView(foot)
    }
}

这样在调用setAdapter的时候,在这个方法内部做了Adapter的装饰处理,最终我们在调用的时候,只需要关系主体列表的实现,与之前使用RecyclerView并无二异,但是却支持了添加头部和尾部。

rv_wrapper = findViewById(R.id.rv_wrapper)
rv_wrapper.adapter = SimpleAdapter()
rv_wrapper.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

rv_wrapper.addHeaderView(
    LayoutInflater.from(this).inflate(R.layout.layout_header, rv_wrapper, false)
)
rv_wrapper.addFooterView(
    LayoutInflater.from(this).inflate(R.layout.layout_footer, rv_wrapper, false)
)

3 总结

其实这里只是简单地实现一个装饰器模式案例,在实际的场景中并不止这些,其实还需要配合分页来实现,如果有这些需求的伙伴可以评论下方留言,我可以整理后上传github