Android RecyclerView — 实现自动加载更多

在App中,使用列表来显示数据是十分常见的。使用列表来展示数据,最好不要一次加载太多的数据,特别是带图片时,页面渲染的时间会变长,常见的做法是进行分页加载。本文介绍一种无感实现自动加载更多的实现方式。

实现自动加载更多

自动加载更多这个功能,其实就是在滑动列表的过程中加载分页数据,这样在加载完所有分页数据之前就可以不停地滑动列表。

计算刷新临界点

手动加载更多一般是当列表滑动到当前最后一个Item后,再向上拖动RecyclerView控件来触发。不难看出来,最后一个Item就是一般加载更多功能的临界点,当达到临界点之后,继续滑动就加载分页数据。对于自动加载更多这个功能来说,如果使用最后一个Item作为临界点,就无法做到在加载完所有分页数据之前不停地滑动列表。那么自动加载更多这个功能的临界点应该是什么呢?

RecyclerView在手机屏幕上一次可显示的Item数量是有限的,相当于对所有Item进行了分页。当倒数第二页Item的最后一个Item显示在屏幕上时,是一个不错的加载下一分页数据的时机。

  • 获取RecyclerView的可视Item数量

通过LayoutManagerfindLastVisibleItemPosition()findFirstVisibleItemPosition()方法,可以计算出可视Item数量。

kotlin 复制代码
private fun calculateVisibleItemCount() {
    (recyclerView.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
        // 可视Item数量
        val visibleItemCount = linearLayoutManager.findLastVisibleItemPosition() - linearLayoutManager.findFirstVisibleItemPosition()
    }
}
  • 计算临界点

通过LayoutManagergetItemCount()方法,可以获取Item的总量。Item总量减一再减去可视Item数量就是倒数第二页Item的最后一个Item的位置。然后通过LayoutManagerfindViewByPosition()方法来获取临界点Item控件,当Item未显示时,返回值为null

kotlin 复制代码
private fun calculateCriticalPoint() {
    (binding.rvExampleDataContainerVertical.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
        // 可视Item数量
        val visibleItemCount = linearLayoutManager.findLastVisibleItemPosition() - linearLayoutManager.findFirstVisibleItemPosition()
        // 临界点位置
        val criticalPointPosition = (linearLayoutManager.itemCount - 1) - visibleItemCount
        // 获取临界点Item的控件,未显示时返回null。
        val criticalPointItemView = linearLayoutManager.findViewByPosition(criticalPointPosition)
    }
}

监听列表滑动

通过RecyclerViewaddOnScrollListener()方法,可以对RecyclerView添加滑动监听。在滑动监听中的回调里,可以对RecyclerView的滑动方向以及是否达到了临界点进行判断,当达到临界点时就可以加载下一页的分页数据。代码如下:

kotlin 复制代码
private fun checkLoadMore() {
    binding.rvExampleDataContainerVertical.addOnScrollListener(object : RecyclerView.OnScrollListener() {

        private var scrollToEnd = false

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            (recyclerView.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
                // 判断是拖动或者惯性滑动
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    // 可视Item数量
                    val visibleItemCount = linearLayoutManager.findLastVisibleItemPosition() - linearLayoutManager.findFirstVisibleItemPosition()
                    // 临界点位置
                    val criticalPointPosition = (linearLayoutManager.itemCount - 1) - visibleItemCount
                    // 获取临界点Item的控件,未显示时返回null。
                    val criticalPointItemView = linearLayoutManager.findViewByPosition(criticalPointPosition)
                    // 判断是向着列表尾部滚动,并且临界点已经显示,可以加载更多数据。
                    if (scrollToEnd && criticalPointItemView != null) {
                        // 加载更多数据
                        ......
                    }
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            (recyclerView.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
                scrollToEnd = if (linearLayoutManager.orientation == LinearLayoutManager.VERTICAL) {
                    // 竖向列表判断向下滑动
                    dy > 0
                } else {
                    // 横向列表判断向右滑动
                    dx > 0
                }
            }
        }
    })
}

完整演示代码

  • 适配器
kotlin 复制代码
class AutoLoadMoreExampleAdapter(private val vertical: Boolean = true) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private val containerData = ArrayList<String>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (vertical) {
            AutoLoadMoreItemVerticalViewHolder(LayoutAutoLoadMoreExampleItemVerticalBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else {
            AutoLoadMoreItemHorizontalViewHolder(LayoutAutoLoadMoreExampleItemHorizontalBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    }

    override fun getItemCount(): Int {
        return containerData.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is AutoLoadMoreItemVerticalViewHolder -> {
                holder.itemViewBinding.tvTextContent.text = containerData[position]
            }

            is AutoLoadMoreItemHorizontalViewHolder -> {
                holder.itemViewBinding.tvTextContent.text = containerData[position]
            }
        }
    }

    fun setNewData(newData: ArrayList<String>) {
        val currentItemCount = itemCount
        if (currentItemCount != 0) {
            containerData.clear()
            notifyItemRangeRemoved(0, currentItemCount)
        }
        if (newData.isNotEmpty()) {
            containerData.addAll(newData)
            notifyItemRangeChanged(0, itemCount)
        }
    }

    fun addData(newData: ArrayList<String>) {
        val currentItemCount = itemCount
        if (newData.isNotEmpty()) {
            this.containerData.addAll(newData)
            notifyItemRangeChanged(currentItemCount, itemCount)
        }
    }

    class AutoLoadMoreItemVerticalViewHolder(val itemViewBinding: LayoutAutoLoadMoreExampleItemVerticalBinding) : RecyclerView.ViewHolder(itemViewBinding.root)

    class AutoLoadMoreItemHorizontalViewHolder(val itemViewBinding: LayoutAutoLoadMoreExampleItemHorizontalBinding) : RecyclerView.ViewHolder(itemViewBinding.root)
}
  • 示例页面
kotlin 复制代码
class AutoLoadMoreExampleActivity : AppCompatActivity() {

    private val prePageCount = 20

    private var verticalRvVisibleItemCount = 0

    private val verticalRvAdapter = AutoLoadMoreExampleAdapter()

    private val verticalRvScrollListener = object : RecyclerView.OnScrollListener() {

        private var scrollToBottom = false

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            (recyclerView.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
                // 判断是拖动或者惯性滑动
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    if (verticalRvVisibleItemCount == 0) {
                        // 获取列表可视Item的数量
                        verticalRvVisibleItemCount = linearLayoutManager.findLastVisibleItemPosition() - linearLayoutManager.findFirstVisibleItemPosition()
                    }
                    // 判断是向着列表尾部滚动,并且临界点已经显示,可以加载更多数据。
                    if (scrollToBottom && linearLayoutManager.findViewByPosition(linearLayoutManager.itemCount - 1 - verticalRvVisibleItemCount) != null) {
                        loadData()
                    }
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            // 判断列表是向列表尾部滚动
            scrollToBottom = dy > 0
        }
    }

    private var horizontalRvVisibleItemCount = 0

    private val horizontalRvAdapter = AutoLoadMoreExampleAdapter(false)

    private val horizontalRvScrollListener = object : RecyclerView.OnScrollListener() {

        private var scrollToEnd = false

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            (recyclerView.layoutManager as? LinearLayoutManager)?.let { linearLayoutManager ->
                // 判断是拖动或者惯性滑动
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    if (horizontalRvVisibleItemCount == 0) {
                        // 获取列表可视Item的数量
                        horizontalRvVisibleItemCount = linearLayoutManager.findLastVisibleItemPosition() - linearLayoutManager.findFirstVisibleItemPosition()
                    }
                    // 判断是向着列表尾部滚动,并且临界点已经显示,可以加载更多数据。
                    if (scrollToEnd && linearLayoutManager.findViewByPosition(linearLayoutManager.itemCount - 1 - horizontalRvVisibleItemCount) != null) {
                        loadData()
                    }
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            // 判断列表是向列表尾部滚动
            scrollToEnd = dx > 0
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = LayoutAutoLoadMoreExampleActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.includeTitle.tvTitle.text = "AutoLoadMoreExample"

        binding.rvExampleDataContainerVertical.adapter = verticalRvAdapter
        binding.rvExampleDataContainerVertical.addOnScrollListener(verticalRvScrollListener)

        binding.rvExampleDataContainerHorizontal.adapter = horizontalRvAdapter
        binding.rvExampleDataContainerHorizontal.addOnScrollListener(horizontalRvScrollListener)

        loadData()
    }

    fun loadData() {
        val init = verticalRvAdapter.itemCount == 0
        val start = verticalRvAdapter.itemCount
        val end = verticalRvAdapter.itemCount + prePageCount

        val testData = ArrayList<String>()
        for (index in start until end) {
            testData.add("item$index")
        }
        if (init) {
            verticalRvAdapter.setNewData(testData)
            horizontalRvAdapter.setNewData(testData)
        } else {
            verticalRvAdapter.addData(testData)
            horizontalRvAdapter.addData(testData)
        }
    }
}

效果如图:

可以看见,分页设定为每页20条数据,列表可以在滑动中无感的实现加载更多。

示例Demo

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
檀越剑指大厂2 小时前
容器化 Android 开发效率:cpolar 内网穿透服务优化远程协作流程
android
MiyamuraMiyako3 小时前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel4 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥6 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走6 小时前
创建自定义语音录制View
android·前端
用户2018792831676 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831676 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker8 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong9 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil10 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin