Android RecyclerView — 实现自动加载更多

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

实现自动加载更多

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

计算刷新临界点

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

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

  • 获取RecyclerView的可视Item数量

通过LayoutManager的findLastVisibleItemPosition()和findFirstVisibleItemPosition()方法,可以计算出可视Item数量。

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

通过LayoutManager的getItemCount()方法,可以获取Item的总量。Item总量减一再减去可视Item数量就是倒数第二页Item的最后一个Item的位置。然后通过LayoutManager的findViewByPosition()方法来获取临界点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)
    }
}

监听列表滑动

通过RecyclerView的addOnScrollListener()方法,可以对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

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui