RecyclerView 系列一 (基础使用)

311 阅读5分钟

RecyclerView基础使用

备注:推荐两个开源库

BaseRecyclerViewAdapterHelper

PathLayoutmanager

本系列博客代码位置: gitee.com/jixiaoxin-5…

RecyclerView作为Android最常用控件之一,在使用上其实还是蛮复杂的,能用是一回事,但要用的好却不容易了

一 RecyclerView能做什么

image.png

image.png

各种各样的数据列表,聊天列表,使用RecyclerView都可以实现

基础使用,必要条件

必要条件就两个,Adapter 和 LayoutManager

Adapter: 主要负责将数据转换成view上的内容

LayoutManager: 我们都知道,ViewGroup有onLayout()方法,用于决定子View的排列位置, RecyclerView是一个ViewGroup,所以LayoutManager 就是用来Manager(管理)子View的Layout(排列方式)的

其他的内容还有很多:

ItemDecoration 负责添加分割线

ItemAnimator 设置Item增加、移除动画

Item 的拖拽

Tree 树列表 多级列表 收起与展开

DiffUtil 差异化更新

快速滑动,重item优化 滑动完毕再加载网络资源

这些都在后面的系列博客再说

二 Adapter 基础方法

1 各个方法的用途

Adapter需要继承 RecyclerView.Adapter 同时有一个 RecyclerView.ViewHolder 的泛型

ViewHolder 的职责是创建并持有view视图,可以把它理解成一个个RecyclerView里的子View,RecyclerView的缓存复用其实就是复用一个个的ViewHolder

然后就是Adapter的各个基础方法的用处

class Demo0Adapter(private val mContext: Context): RecyclerView.Adapter<Demo0Adapter.ItemViewHolder>() {

    private var items = mutableListOf<String>()      //数据源
    fun clearList(){
        items.clear()
        notifyDataSetChanged() //刷新所有列表
    }
    fun addList(list: MutableList<String>){
        items.addAll(list)
        notifyItemRangeChanged(items.size - list.size - 1,list.size)  //刷新对应下标范围的数据  第一个参数的开始刷新的位置,第二个刷新item的个数
    }
    fun setNewList(list: MutableList<String>){
        items.clear()
        items.addAll(list)
        notifyDataSetChanged()
    }

    fun getItems(): MutableList<String>{
        return items
    }

    private lateinit var demo0AdapterListener: Demo0AdapterListener
    fun setDemo0AdapterListener(mainAdapterListener: Demo0AdapterListener){
        this.demo0AdapterListener = mainAdapterListener
    }

    inner class ItemViewHolder(view: View): RecyclerView.ViewHolder(view){
        val textView: TextView = view.findViewById(R.id.textView)
        val rootView: ConstraintLayout = view.findViewById(R.id.rootView)
    }

    /**
     * 用于onCreateViewHolder时创建不同的 ViewHolder
     * */
    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    /**
     * @param parent 这里应该是RecyclerView
     * @param viewType getItemViewType(position: Int)传过来的
     * 创建 ViewHolder
     * */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = LayoutInflater.from(mContext).inflate(R.layout.item_demo0, parent,false)
        val itemViewHolder = ItemViewHolder(view)
        itemViewHolder.rootView.setOnClickListener {
            if(::demo0AdapterListener.isInitialized){
                demo0AdapterListener.clickItems(itemViewHolder.adapterPosition)
            }
        }
        return itemViewHolder

    }

    /**
     * 用于差异化更新
     * 使用payloads进行数据对比,再酌情进行数据的刷新
     * */
    override fun onBindViewHolder(
        holder: ItemViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        super.onBindViewHolder(holder, position, payloads)
    }

    /**
     * 对ViewHolder进行数据绑定
     * */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.textView.text = items[position]
    }

    /**
     * 决定 RecyclerView 的子View个数
     * */
    override fun getItemCount(): Int {
        return items.size
    }

}

我会比较推荐数据源让Adapter持有,这样操作数据的时候会记得调用对应的刷新方法,事实上,在我待过的所有公司他们都并不这么做,很容易出现数据改变了,但是RecyclerView显示的列表没跟着改变,这个时候如果去操作item,很容易出现 NullPointerException 操作空对象 引用 和 数组越界等问题

同时,如果setNewList方法是如下这么写的话

fun setNewList(list: MutableList<String>){
    items = list
    notifyDataSetChanged()
}

其实指向的是外部的引用,如果外部的数据之后进行其他操作,也容易出现上述问题

2 创建ViewHolder

getItemViewType 获取设置的 ViewType之后才会 执行 onCreateViewHolder 创建 VieHolder , 多Type的界面就是根据不同ViewType创建的不同的ViewHolder

3 刷新视图

notify--- 开头的这些刷新方法一般在数据改变后调用,adaper会执行相应的onBindViewHolder,要注意的就是千万不要一概的调用 notifyDataSetChanged 他会直接刷新所有,如果此时列表有几百几千条数据,很容易造成卡顿。 然后就是DiffUtil差异化更新,还有刷新相关的动画,这个之后会专门写一篇

4 设置点击事件

点击事件的设置不要在onBindViewHolder里设置,只需要在视图创建(onCreateViewHolder)的时候设置一次就行,而onBindViewHolder很可能会多次调用

itemViewHolder.layoutPosition
itemViewHolder.adapterPosition
  • getLayoutPosition 和 getAdapterPosition 通常情况下是一样的,只有当 Adapter 里面的内容改变了,而 Layout 还没来得及绘制的这段时间之内才有可能不一样,这个时间小于16ms
  • 如果调用的是 notifyDataSetChanged(),因为要重新绘制所有 Item,所以在绘制完成之前 RecyclerView 是不知道 adapterPosition 的,这时会返回-1(NO_POSITION)
  • 但如果用的是 notifyItemInserted(0),那立即就能获取到正确的 adapterPosition,即使新的 Layout 还没绘制完成,比如之前是0的现在就会变成1,因为插入了0, 相当于 RecyclerView 提前帮你计算的,此时getLayoutPosition 还只能获取到旧的值。
  • 总的来说,大多数情况下用 getAdapterPosition,只要不用 notifyDataSetChanged() 来刷新数据就总能立即获取到正确 position 值。

5 与DataBinding的使用

有的item布局较为复杂,随着item内控件与item类型的增多,每次findViewById其实也很麻烦,而如果使用DataBinding,就会方便很多 (ViewBinding也差不多)

class Demo0DataBindingAdapter(private val mContext: Context): RecyclerView.Adapter<Demo0DataBindingAdapter.ItemViewHolder>() {

    private var items = mutableListOf<String>()      //数据源
    fun clearList(){
        items.clear()
        notifyDataSetChanged() //刷新所有列表
    }
    fun addList(list: MutableList<String>){
        items.addAll(list)
        notifyItemRangeChanged(items.size - list.size - 1,list.size)  //刷新对应下标范围的数据  第一个参数的开始刷新的位置,第二个刷新item的个数
    }
    fun setNewList(list: MutableList<String>){
        items.clear()
        items.addAll(list)
        notifyDataSetChanged()
    }

    fun getItems(): MutableList<String>{
        return items
    }

    private lateinit var demo0AdapterListener: Demo0AdapterListener
    fun setDemo0AdapterListener(mainAdapterListener: Demo0AdapterListener){
        this.demo0AdapterListener = mainAdapterListener
    }

    inner class ItemViewHolder(val binding: ItemDataBindingBinding): RecyclerView.ViewHolder(binding.root)

    /**
     * 用于onCreateViewHolder时创建不同的 ViewHolder
     * */
    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    /**
     * @param parent 这里应该是RecyclerView
     * @param viewType getItemViewType(position: Int)传过来的
     * 创建 ViewHolder
     * */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {

        val itemViewHolder = ItemViewHolder(
            DataBindingUtil.inflate(
                LayoutInflater.from(mContext),
                R.layout.item_data_binding,
                parent,
                false
            )
        )

        with(itemViewHolder.binding){
            rootView.setOnClickListener {
                if(::demo0AdapterListener.isInitialized){
                    demo0AdapterListener.clickItems(itemViewHolder.adapterPosition)
                }
            }
        }
        return itemViewHolder

    }

    /**
     * 用于差异化更新
     * 使用payloads进行数据对比,再酌情进行数据的刷新
     * */
    override fun onBindViewHolder(
        holder: ItemViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        super.onBindViewHolder(holder, position, payloads)
    }

    /**
     * 对ViewHolder进行数据绑定
     * */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        //holder.textView.text = items[position]
        holder.binding.textView.text = items[position]
    }

    /**
     * 决定 RecyclerView 的子View个数
     * */
    override fun getItemCount(): Int {
        return items.size
    }

}

不仅不用写 findViewById,也避免引用到别的布局的id导致异常,同时dataBinding还有数据绑定的功能 ,上面只是最浅显的应用,databinding的数据绑定更大的好处应该是,我们只需要改变集合中的某些数据,对应列表的值也会相应改变,如果删除或添加某个item,列表也做相应动画,这才叫数据绑定,不过这些功能实现起来比较麻烦,这里就先不说了,之后有时间再专门出一篇RecyclerVIew的数据绑定文章

三 各种显示状态的添加

1 空数据状态