RecyclerView实现上拉加载功能

最近在做需求的时候,需要实现列表的上拉加载功能。在这里进行记录分享。

其实上拉加载功能只需要为RecyclerView的Item布局添加一个FooterView,然后通过判断是否滑动到最后一条Item来控制FooterView的显隐来做到一个上拉加载的功能。先看代码

class PaperListAdapter(private val context: Context, private var paperList: List, private val subjectId: Int) :
    RecyclerView.Adapter() {

    companion object {
        private const val TAG = "PaperListAdapter"
        const val TYPE_ITEM = 1
        const val TYPE_FOOTER = 2
    }

    //加载状态,默认加载中
    var loadState = 1

    //正在加载
    val loading = 1

    //加载完成
    val loaded = 2

    //加载到底
    val loadedEnd = 3

    inner class ViewHolder(val binding: PaperItemBinding) : RecyclerView.ViewHolder(binding.root)

    inner class FooterViewHolder(val footerBinding: PaperItemFooterBinding) :
        RecyclerView.ViewHolder(footerBinding.root)

    override fun getItemViewType(position: Int): Int {
        return if (position + 1 == itemCount) {
            TYPE_FOOTER
        } else {
            TYPE_ITEM
        }
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == TYPE_ITEM) {
            val binding = PaperItemBinding.inflate(LayoutInflater.from(context), parent, false)
            ViewHolder(binding)
        } else {
            val binding =
                PaperItemFooterBinding.inflate(LayoutInflater.from(context), parent, false)
            FooterViewHolder(binding)
        }
    }

    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 {
                    // 如果当前是footer的位置,那么该item占据4个单元格,正常情况下占据1个单元格
                    return if (getItemViewType(position) == TYPE_FOOTER) manager.spanCount else 1
                }
            }
        }

    }

  
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is ViewHolder) {
            //处理普通item的逻辑
        }

        if (holder is FooterViewHolder) {
            //处理footerView的逻辑
            when (loadState) {
                loading -> {
                    holder.footerBinding.paperLoadingAnim.visibility = View.VISIBLE
                    holder.footerBinding.paperLoadingText.visibility = View.VISIBLE
                    holder.footerBinding.paperBottomText.visibility = View.GONE
                }
                loaded -> {
                    holder.footerBinding.paperLoadingAnim.visibility = View.GONE
                    holder.footerBinding.paperLoadingText.visibility = View.GONE
                    holder.footerBinding.paperBottomText.visibility = View.GONE
                }
                loadedEnd -> {
                    holder.footerBinding.paperLoadingAnim.visibility = View.GONE
                    holder.footerBinding.paperLoadingText.visibility = View.GONE
                    holder.footerBinding.paperBottomText.visibility = View.VISIBLE
                }
            }
        }
    }

    override fun getItemCount() = paperList.size + 1


    fun setLoadingState(state: Int) {
        loadState = state
        notifyDataSetChanged()
    }

}

在getItemCount我们需要返回数据大小 + 1,来为FooterView腾出位置。可以看到,我们定义了两种布局标志,然后重写getItemViewType方法根据position来设置FooterView,然后在onCreateViewHolder方法中根据不同的ViewType来加载不同的布局。

在这里我采用的是网格布局,所以必须想办法让FooterView横跨我网格的列数。那么在这里需要重写一下onAttachedToRecyclerView方法。首先对当前的布局管理器做一个判断,如果是网格布局,那么需要设置一下spanSizeLookup,网格布局的这个方法代表每个Item占据的单元格数,然后设置在FooterView的情况下占据你设置的列数个单元格,我这里是4。普通Item占据一个单元格就可以了。这样我们就将我们的FooterView设置好了。

下面我们要判断一下什么时候展示FooterView,即滑动到最后一个Item的时候,加载下一页数据。因此我们要对RecyclerView设置滑动监听,代码如下

class BottomRecyclerViewOnScrollerListener : RecyclerView.OnScrollListener() {

    private var isSlidingUp = false

    private var onLoadMore: (() -> Unit)? = null

    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        val manager = recyclerView.layoutManager as LinearLayoutManager
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            val lastItemPosition: Int = manager.findLastCompletelyVisibleItemPosition()
            val itemCount = manager.itemCount
            // 判断是否滑动到了最后一个item,并且是向上滑动
            if (lastItemPosition == itemCount - 1 && isSlidingUp) {
                //加载更多
                onLoadMore?.invoke()
            }
        }
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        // 大于0表示正在向上滑动,小于等于0表示停止或向下滑动
        isSlidingUp = dy > 0
    }

    fun setOnLoadMoreListener(listener: () -> Unit) {
        onLoadMore = listener
    }
}

代码比较清晰,就不多做解释了,核心就是判断现在是否正在上滑以及是否滑动到了最后一个Item。

只需要给RecyclerView添加监听,做相应的逻辑即可,如下

val bottomListener = BottomRecyclerViewOnScrollerListener()
            bottomListener.setOnLoadMoreListener {
                if (paperListAdapter != null) {
                    paperListAdapter!!.setLoadingState(paperListAdapter!!.loading)
                    //请求下一页的逻辑
                    } else {
                        paperListAdapter!!.setLoadingState(paperListAdapter!!.loadedEnd)
                    }
                }
            }
            paperContentView.addOnScrollListener(bottomListener)

那么到这里,其实已经实现了上拉加载的功能,但是这里有一个小坑,就是在第二页请求数据返回之后,必须追加到第一页数据的List上,不可以重新设置Adapter,否则就每刷新一次就会闪到第一行,不符合预期效果。
除此之外,一般上拉加载同时会伴有下拉刷新以及其他筛选刷新的功能,我们使用的数据源必须是一个List才行,要不然处理逻辑会很麻烦,而且往往达不到我们的效果。

你可能感兴趣的:(android,kotlin)