如何用Kotlin为加载Firebase图片的RecyclerView实现分页(含类Pinterest预加载)
现有代码核心问题
- 滚动监听逻辑错误:
firstVisibleItems初始化为null,无法获取第一个可见项位置,分页触发条件永远不会生效 - Firebase数据拉取逻辑错误:使用全量实时监听,每次加载都会清空已有数据重新拉取全部内容,没有实现分页拉取
- 状态逻辑错误:数据加载完成后将
loading设为true,导致后续分页触发逻辑被拦截 - 缺少滚动速度检测逻辑,无法实现快慢滚动的差异化交互
基础分页实现(需求1)
Firebase Realtime Database 分页依赖游标实现,每次拉取指定数量的数据,记录最后一条数据的key作为下一次拉取的起始位置,避免全量拉取。
类Pinterest交互实现(需求2)
新增两项逻辑即可实现对应效果:
- 预加载阈值:设置为剩余10项时触发下一次拉取,保证慢速滚动时用户还未滑动到列表末尾就已完成数据加载,无感知
- 滚动速度检测:通过滚动位移与时间差计算滚动速度,仅当滚动速度超过阈值时显示加载进度条,慢速滚动时隐藏加载状态
修正后完整代码
Home_Fragment.kt
// 全局变量定义 private val PAGE_SIZE = 20 private val PRELOAD_THRESHOLD = 10 // 剩余10项时触发预加载 private val FAST_SCROLL_THRESHOLD = 2000 // 快速滚动阈值,单位px/s,可自行调整 private var isLoading = false private var lastKey: String? = null // 分页游标,记录最后一条数据的key private var isLastPage = false private var lastScrolledTime = System.currentTimeMillis() private lateinit var postsAdapter: PostAdapter_Home private lateinit var mUploads: MutableList<Upload?> // 在onCreateView中初始化 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_home, container, false) val staggeredGridLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL) postRecyclerView.layoutManager = staggeredGridLayoutManager mUploads = ArrayList() postsAdapter = PostAdapter_Home(requireContext(), mUploads) postRecyclerView.adapter = postsAdapter // 滚动监听 postRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) // 计算滚动速度 val currentTime = System.currentTimeMillis() val timeDiff = currentTime - lastScrolledTime var scrollSpeed = 0 if (timeDiff > 0) { scrollSpeed = Math.abs(dy) * 1000 / timeDiff } lastScrolledTime = currentTime val layoutManager = recyclerView.layoutManager as StaggeredGridLayoutManager val visibleItemCount = layoutManager.childCount val totalItemCount = layoutManager.itemCount val firstVisibleItems = IntArray(layoutManager.spanCount) layoutManager.findFirstVisibleItemPositions(firstVisibleItems) val firstVisibleItem = firstVisibleItems.maxOrNull() ?: 0 // 预加载触发条件:未加载、未到最后一页、剩余项小于预加载阈值、向上滑动 if (!isLoading && !isLastPage && dy > 0 && (totalItemCount - visibleItemCount) <= (firstVisibleItem + PRELOAD_THRESHOLD)) { // 仅快速滚动时显示加载进度条 if (scrollSpeed > FAST_SCROLL_THRESHOLD) { // 此处替换为你自己的加载进度条控件显示逻辑 loadingProgressBar.visibility = View.VISIBLE } loadNextPage() } } }) // 加载第一页数据 loadFirstPage() shimmerFrameLayout.startShimmer() return view } // 加载第一页数据 private fun loadFirstPage() { isLoading = true databaseReference.orderByKey().limitToFirst(PAGE_SIZE).addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { mUploads.clear() if (snapshot.exists()) { for (dataSnapshot in snapshot.children) { val upload = dataSnapshot.getValue(Upload::class.java)!! upload.setmKey(dataSnapshot.key) mUploads.add(upload) lastKey = dataSnapshot.key } } isLoading = false shimmerFrameLayout.stopShimmer() shimmerFrameLayout.visibility = View.GONE postsAdapter.notifyDataSetChanged() isLastPage = mUploads.size < PAGE_SIZE } override fun onCancelled(error: DatabaseError) { isLoading = false shimmerFrameLayout.stopShimmer() shimmerFrameLayout.visibility = View.GONE } }) } // 加载下一页数据 private fun loadNextPage() { isLoading = true lastKey?.let { cursor -> databaseReference.orderByKey().startAfter(cursor).limitToFirst(PAGE_SIZE).addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val newItems = mutableListOf<Upload?>() if (snapshot.exists()) { for (dataSnapshot in snapshot.children) { val upload = dataSnapshot.getValue(Upload::class.java)!! upload.setmKey(dataSnapshot.key) newItems.add(upload) lastKey = dataSnapshot.key } } isLoading = false // 隐藏加载进度条 loadingProgressBar.visibility = View.GONE if (newItems.isNotEmpty()) { val insertStart = mUploads.size mUploads.addAll(newItems) postsAdapter.notifyItemRangeInserted(insertStart, newItems.size) } isLastPage = newItems.size < PAGE_SIZE } override fun onCancelled(error: DatabaseError) { isLoading = false loadingProgressBar.visibility = View.GONE } }) } ?: run { isLoading = false isLastPage = true } }
优化说明
- 替换
addValueEventListener为addListenerForSingleValueEvent,避免数据变动时全量回调打乱分页逻辑,若需要实时更新数据可单独监听新增条目追加到列表 - Glide缓存策略可调整为
DiskCacheStrategy.ALL,提升图片加载速度,进一步降低加载感知 - 若需要添加底部加载脚,可在Adapter中新增加载项类型,根据
isLoading状态控制显示隐藏
内容的提问来源于stack exchange,提问作者Vasant Raval
相关产品推荐
相关产品推荐

