Android性能优化之RecyclerView分页加载组件功能详解
作者:Ghelper 发布时间:2023-06-04 16:24:14
引言
在Android应用中,列表有着举足轻重的地位,几乎所有的应用都有列表的身影,但是对于列表的交互体验一直是一个大问题。在性能比较好的设备上,列表滑动几乎看不出任何卡顿,但是放在低端机上,卡顿会比较明显,而且列表中经常会伴随图片的加载,卡顿会更加严重,因此本章从手写分页加载组件入手,并对列表卡顿做出对应的优化
1 分页加载组件
为什么要分页加载,通常列表数据存储在服务端会超过100条,甚至上千条,如果服务端一次性返回,我们一次性接受直接加载,如果其中有图片加载,肯定直接报OOM,应用崩溃,因此我们通常会跟服务端约定分页的规则,服务端会按照页码从0开始给数据,或者在数据中返回下一页对应的索引,当出发分页加载时,就会拿到下一页的页码请求新一页的数据。
目前在JetPack组件中,Paging是使用比较多的一个分页加载组件,但是Paging使用的场景有限,因为流的限制,导致只能是单一数据源,而且数据不能断,只能全部加载进来,因此决定手写一个分页加载组件,适用多种场景。
1.1 功能定制
如果想要自己写一个分页加载库,首先需要明白,分页加载组件需要做什么事?
对于RecyclerView来说,它的主要功能就是创建视图并绑定数据,因此我们先定义分页列表的基础能力,绑定视图和数据
interface IPagingList<T> {
fun bindView(context: Context,lifecycleOwner: LifecycleOwner, recyclerView: RecyclerView,adapter: PagingAdapter<T>,mode: ListMode) {}
fun bindData(model: List<BasePagingModel<T>>) {}
}
bindData:
bindData就不多说了,就是绑定数据,首先我们拿到的数据一定是一个列表数据,因为并不知道业务方需要展示的数据类型是啥样的,因此需要泛型修饰,那么BasePagingModel是干什么的呢?
open class BasePagingModel<T>(
var pageCount: String = "", //页码
var type: Int = 1, //分页类型 1 带日期 2 普通列表
var time: String = "", //如果是带日期的model,那么需要传入此值
var itemData: T? = null
)
首先BasePagingModel是分页列表中数据的基类,其中存储的元素包括pageCount,代表传进来的数据列表是哪一页,type用来区分列表数据类型,time可以代表当前数据在服务端的时间(主要场景就是列表中数据展示需要带时间,并根据某一天进行数据聚合),itemData代表业务层需要处理的数据。
bindView:
对于RecyclerView来说,创建视图、展示数据需要适配器,因此这里传入了RecyclerView还有通用的适配器PagingAdapter
abstract class PagingAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var datas: List<BasePagingModel<T>>? = null
private var maps: MutableMap<String, MutableList<BasePagingModel<T>>>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return buildBusinessHolder(parent, viewType)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (datas != null) {
bindBusinessData(holder, position, datas)
} else if (maps != null) {
bindBusinessMapData(holder, position, maps)
}
}
abstract fun getHolderWidth(context: Context):Int
override fun getItemCount(): Int {
return if (datas != null) datas!!.size else 0
}
open fun bindBusinessMapData(
holder: RecyclerView.ViewHolder,
position: Int,
maps: MutableMap<String, MutableList<BasePagingModel<T>>>?
) {
}
open fun bindBusinessData(
holder: RecyclerView.ViewHolder,
position: Int,
datas: List<BasePagingModel<T>>?
) {
}
abstract fun buildBusinessHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
fun setPagingData(datas: List<BasePagingModel<T>>) {
this.datas = datas
notifyDataSetChanged()
}
fun setPagingMapData(maps: MutableMap<String, MutableList<BasePagingModel<T>>>) {
this.maps = maps
notifyDataSetChanged()
}
}
这一章,我们先介绍使用场景比较多的单数据列表
PagingAdapter是一个抽象类,携带的数据同样是业务方需要处理的数据,是一个泛型,创建视图方法buildBusinessHolder交给业务方实现,这里我们关注两个数据相关的方法 bindBusinessData和setPagingData,当调用setPagingData方法时,将处理好的数据列表发进来,然后调用notifyDataSetChanged方法刷新列表,这个时候会调用bindBusinessData将列表中的数据绑定并展示出来。
这里我们还需要关注一个方法,这个方法业务方必须要实现,这个方法有什么作用呢?
abstract fun getHolderWidth(context: Context):Int
这个方法用于返回列表中每个ItemView的尺寸宽度,因为在分页组件中会判断当前列表可见的ItemView有多少个。这里大家可能会有疑问,RecyclerView的LayoutManager不是有对应的api吗,像
findFirstVisibleItemPosition()
findLastVisibleItemPosition()
findFirstCompletelyVisibleItemPosition()
findLastCompletelyVisibleItemPosition()
为什么不用呢?因为我们的分页组件是要兼容多种视图形式的,虽然我们今天讲到的普通列表用这个是没有问题的,但是有些视图类型是不能兼容这个api的,后续会介绍。
1.2 手写分页列表
先把第一版的代码贴出来,有个完整的体系
class PagingList<T> : IPagingList<T>, IModelProcess<T>, LifecycleEventObserver {
private var mTotalScroll = 0
private var mCallback: IPagingCallback? = null
private var currentPageIndex = ""
//模式
private var mode: ListMode = ListMode.DATE
private var adapter: PagingAdapter<T>? = null
//支持的类型 普通列表
private val dateMap: MutableMap<String, MutableList<BasePagingModel<T>>> by lazy {
mutableMapOf()
}
private val simpleList: MutableList<BasePagingModel<T>> by lazy {
mutableListOf()
}
override fun bindView(
context: Context,
lifecycleOwner: LifecycleOwner,
recyclerView: RecyclerView,
adapter: PagingAdapter<T>,
mode: ListMode
) {
this.mode = mode
this.adapter = adapter
recyclerView.adapter = adapter
recyclerView.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
addRecyclerListener(recyclerView)
lifecycleOwner.lifecycle.addObserver(this)
}
private fun addRecyclerListener(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (!recyclerView.canScrollHorizontally(1) && currentPageIndex == "-1" && mTotalScroll > 0) {
//滑动到底部
mCallback?.scrollEnd()
}
//获取可见item的个数
val visibleCount = getVisibleItemCount(recyclerView.context, recyclerView)
if (recyclerView.childCount > 0 && visibleCount >= (getListCount(mode) ?: 0)) {
if (currentPageIndex != "-1") {
//请求下一页数据
mCallback?.scrollRefresh()
}
}
} else {
//暂停刷新
mCallback?.scrolling()
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollHorizontally(1) && currentPageIndex == "-1" && mTotalScroll > 0) {
//滑动到底部
mCallback?.scrollEnd()
}
mTotalScroll += dx
//滑动超出2屏
// binding.ivBackFirst.visibility =
// if (mTotalScroll > ScreenUtils.getScreenWidth(requireContext()) * 2) View.VISIBLE else View.GONE
}
})
}
override fun bindData(model: List<BasePagingModel<T>>) {
//处理数据
dealPagingModel(model)
//adapter刷新数据
if (mode == ListMode.DATE) {
adapter?.setPagingMapData(dateMap)
} else {
adapter?.setPagingData(simpleList)
}
}
fun setScrollListener(callback: IPagingCallback) {
this.mCallback = callback
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_RESUME) {
//TODO 加载图片
// Glide.with(requireContext()).resumeRequests()
} else if (event == Lifecycle.Event.ON_PAUSE) {
//TODO 停止加载图片
} else if (event == Lifecycle.Event.ON_DESTROY) {
//TODO 页面销毁不会加载图片
}
}
/**
* 获取可见的item个数
*/
private fun getVisibleItemCount(context: Context, recyclerView: RecyclerView): Int {
var totalCount = 0
//首屏假设全部占满
totalCount +=
ScreenUtils.getScreenWidth(recyclerView.context) / adapter?.getHolderWidth(context)!!
totalCount += mTotalScroll / adapter?.getHolderWidth(context)!!
return (totalCount + 1)
}
override fun getTotalCount(): Int? {
return getListCount(mode)
}
override fun dealPagingModel(data: List<BasePagingModel<T>>) {
this.currentPageIndex = updateCurrentPageIndex(data)
if (mode == ListMode.DATE) {
data.forEach { model ->
val time = DateFormatterUtils.check(model.time)
if (dateMap.containsKey(time)) {
model.itemData?.let {
dateMap[time]?.add(model)
}
} else {
val list = mutableListOf<BasePagingModel<T>>()
list.add(model)
dateMap[time] = list
}
}
} else {
simpleList.addAll(data)
}
}
private fun updateCurrentPageIndex(data: List<BasePagingModel<T>>): String {
if (data.isNotEmpty()) {
return data[0].pageCount
}
return "-1"
}
private fun getListCount(mode: ListMode): Int? {
var count = 0
if (mode == ListMode.DATE) {
dateMap.keys.forEach { key ->
//获取key下的元素个数
count += dateMap[key]?.size ?: 0
}
} else {
count = simpleList.size
}
return count
}
}
首先,PagingList实现了IPagingList接口,我们先看实现,在bindView方法中,其实就是给RecyclerView设置了适配器,然后注册了RecyclerView的滑动监听,我们看下 * 中的主要实现。
onScrollStateChanged方法主要用于监听列表是否在滑动,当列表的状态为SCROLL_STATE_IDLE时,代表列表停止了滑动,这里做了两件事:
(1)首先判断列表是否滑动到了底部
if (!recyclerView.canScrollHorizontally(1) && currentPageIndex == "-1" && mTotalScroll > 0) {
//滑动到底部
mCallback?.scrollEnd()
}
这里需要满足三个条件:recyclerView.canScrollHorizontally(1)如果返回了false,那么代表列表不能继续滑动;还有就是会判断currentPageIndex是否是最后一页,如果等于-1那么就是最后一页,同样需要判断滑动的距离,综合来说就是【如果列表滑动到了最后一页而且不能再继续滑动了,那么就是到底了】,这里可以展示尾部的到底UI。
(2)判断是否能够触发分页加载
/**
* 获取可见的item个数
*/
private fun getVisibleItemCount(context: Context, recyclerView: RecyclerView): Int {
var totalCount = 0
//首屏假设全部占满
totalCount +=
ScreenUtils.getScreenWidth(recyclerView.context) / adapter?.getHolderWidth(context)!!
totalCount += mTotalScroll / adapter?.getHolderWidth(context)!!
return (totalCount + 1)
}
首先这里会判断展示了多少ItemView,之前提到的适配器中的getHolderWidth这里就用到了,首先我们会假设首屏全部占满了ItemView,然后根据列表滑动的距离,判断后续有多少ItemView展示出来,最终返回结果。
我们先不看下面的逻辑,因为分页加载涉及到了数据的处理,因此我们先看下bindData的实现
override fun bindData(model: List<BasePagingModel<T>>) {
//处理数据
dealPagingModel(model)
//adapter刷新数据
if (mode == ListMode.DATE) {
adapter?.setPagingMapData(dateMap)
} else {
adapter?.setPagingData(simpleList)
}
}
在调用bindData时会传入一页的数据,dealPagingModel方法用于处理数据,首先获取当前数据的页码,用于判断是否需要继续分页加载。
override fun dealPagingModel(data: List<BasePagingModel<T>>) {
this.currentPageIndex = updateCurrentPageIndex(data)
if (mode == ListMode.DATE) {
data.forEach { model ->
val time = DateFormatterUtils.check(model.time)
if (dateMap.containsKey(time)) {
model.itemData?.let {
dateMap[time]?.add(model)
}
} else {
val list = mutableListOf<BasePagingModel<T>>()
list.add(model)
dateMap[time] = list
}
}
} else {
simpleList.addAll(data)
}
}
剩下的工作用于组装数据,simpleList用于存储全部的列表数据,每次传入一页数据,都会存在这个集合中。处理完数据之后,将数据塞进adapter,用于刷新数据。
然后我们回到前面,我们在拿到了可见的ItemView的个数之后,首先会判断recyclerView展示的ItemView个数,如果等于0,那么就说明没有数据,就不需要触发分页加载。
if (recyclerView.childCount > 0 && visibleCount >= (getListCount(mode) ?: 0)) {
if (currentPageIndex != "-1") {
//请求下一页数据
mCallback?.scrollRefresh()
}
}
假设每页展示10条数据,这个时候getListCount方法返回的就是总的数据个数(10),如果visibleCount超过了List的总个数,那么就需要触发分页加载,因为之前我们提到,最后一页的index就是-1,所以这里判断如果是最后一页,就不需要分页加载了。
1.3 生命周期管理
在PagingList中,我们实现了LifecycleEventObserver接口,这里的作用是什么呢?
就是我们知道,在列表中经常会有图片的加载,那么在图片加载时如果滑动列表,那么势必会产生卡顿,因此我们在滑动的过程中不会去加载图片,而是在滑动停止时,重新加载,这个优化体验是没有问题,用户不会关注滑动时的状态。
那么这里会存在一个问题,例如我们在滑动的过程中退出到后台,这个时候列表滑动停止时加载图片,可能存在上下文找不到的场景导致应用崩溃,因此我们传入生命周期的目的在于:让列表具备感知生命周期的能力,当列表处在不可见的状态时,不能进行多余的网络请求。
2022-09-04 15:41:43.541 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:43.651 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:43.661 2763-2763/com.lay.paginglist E/MainActivity: scrollRefresh--
2022-09-04 15:41:43.668 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:43.674 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:43.877 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:43.885 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:43.950 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:44.101 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:44.175 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:44.318 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:44.467 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:44.475 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:45.188 2763-2777/com.lay.paginglist I/.lay.paginglis: WaitForGcToComplete blocked RunEmptyCheckpoint on ProfileSaver for 12.247ms
2022-09-04 15:41:47.008 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.099 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.186 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:47.322 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.403 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.404 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:47.514 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:47.606 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.650 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:47.683 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.781 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:47.889 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.950 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:47.963 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:48.156 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:48.182 2763-2763/com.lay.paginglist E/MyAdapter: bindBusinessData ---
2022-09-04 15:41:48.231 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:48.489 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:48.533 2763-2763/com.lay.paginglist E/MainActivity: scrolling--
2022-09-04 15:41:48.593 2763-2763/com.lay.paginglist E/MainActivity: scrollEnd--
我们可以看下具体的实现效果就是,当触发分页加载时,scrollRefresh会被回调,这里可以进行网络请求,拿到数据之后再次调用bindData方法,然后继续往下滑动,当滑动到最后一页时,scrollEnd被回调,具体的使用,可以在demo中查看。
2 github
之前有小伙伴提到这个事情,希望在github上放出源码,所以就做了 github.com/LLLLLaaayyy…
大家可以在v1.0分支查看源码,在app模块中有一个demo大家可以看具体的使用方式,分页列表的代码在paging模块中
来源:https://juejin.cn/post/7139465987811704863
猜你喜欢
- 起源 [1946: John von Neumann, Stan Ulam, and Nick Metropolis, all a
- 🎉工作场景中遇到这样一个需求:根据主机的 IP 地址联动更新其他模型的相关信息。需求很简单,只涉及一般的数据库联动查询以及更新操作,然而在编
- 一、介绍knife4j增强版本的Swagger 前端UI,取名knife4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍,更名也是希望把
- java沙箱环境测试支付宝支付接口?准备工作,登陆支付宝开放平台,进入沙箱环境开放平台链接:https://developers.alipa
- 前言有时线上问题我们用打日志的方式来观察错误或埋点参数,但由于这些日志如果都打出来会占用大量存储空间而且覆盖了一些有效信息,所以线上级别一般
- 一、什么是反射Java Reflaction in Action中的解释:反射是运行中的程序检查自己和软件运行环境的能力,它可以根据它发现的
- Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入
- 目录背景问题解决思路其他问题小结背景关于个人,前段时间由于业务太忙,所以一直没有来得及思考并且沉淀点东西;同时组内一个个都在业务上能有自己的
- 本文实例为大家分享了java + dom4j.jar提取xml文档内容的具体代码,供大家参考,具体内容如下资源下载页:点击下载本例程主要借助
- 实现过滤器很简单,只需要继承ZuulFilter,并实现ZuulFilter中的抽象方法。filterType():定义过滤器的类型,它有4
- C#重绘checkbox生成滑动开关,供大家参考,具体内容如下通过调用checkbox控件的paint事件,在重绘事件里判断checked属
- Java基本概念JDK包含了不少Java开发相关命令。如,javac、java、javap、javaw、javadoc。虽然现在的Java开
- 这是一个演示如何使用java执行定时任务的实例,本实例开始运行后不会自动结束,请在运行本实例后手动结束程序。package com.hong
- 因为一直用spring整合了mybatis,所以很少用到mybatis的session缓存。 习惯是本地缓存自己用map写或者引入第三方的本
- 本人刚参加工作,面试的时候遇四道笔试题,其中就有这道多线程有序读取文件的题目,初看时拿不准,感觉会,又感觉不会。于是放弃了这道题,今天闲下来
- 有哪些“纪律”是Java程序员所要遵守的?1. 为代码添加注释(Add comments to your code). – 每个人都知道这一
- 一、注解是什么Java 注解用于为 Java 代码提供元数据,看完这句话也许你还是一脸懵逼,用人话说就是注解不直接影响你的代码执行,仅提供信
- 1.概念a.是个二叉树(每个节点最多有两个子节点)b.对于这棵树中的节点的节点值左子树中的所有节点值 < 根节点 < 右子树的所
- 问题现象今天使用mybatis遇到个很奇怪的问题,我使用一个参数@param("threshold"),类型是java的
- 判断对象存活方法引用计数法:在对象中添加一个引用计数子,每当一个地方引用他时,计数器就加一,当引用失效时,计数器就减一。会有对象循环引用问题