Android性能优化之RecyclerView分页加载组件功能怎么实现

您所在的位置:网站首页 android分页加载数据 Android性能优化之RecyclerView分页加载组件功能怎么实现

Android性能优化之RecyclerView分页加载组件功能怎么实现

2023-05-23 03:21| 来源: 网络整理| 查看: 265

Android性能优化之RecyclerView分页加载组件功能怎么实现 发布时间:2022-09-05 11:42:10 来源:亿速云 阅读:147 作者:iii 栏目:开发技术

这篇文章主要讲解了“Android性能优化之RecyclerView分页加载组件功能怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Android性能优化之RecyclerView分页加载组件功能怎么实现”吧!

引言

在Android应用中,列表有着举足轻重的地位,几乎所有的应用都有列表的身影,但是对于列表的交互体验一直是一个大问题。在性能比较好的设备上,列表滑动几乎看不出任何卡顿,但是放在低端机上,卡顿会比较明显,而且列表中经常会伴随图片的加载,卡顿会更加严重,因此本章从手写分页加载组件入手,并对列表卡顿做出对应的优化

1 分页加载组件

为什么要分页加载,通常列表数据存储在服务端会超过100条,甚至上千条,如果服务端一次性返回,我们一次性接受直接加载,如果其中有图片加载,肯定直接报OOM,应用崩溃,因此我们通常会跟服务端约定分页的规则,服务端会按照页码从0开始给数据,或者在数据中返回下一页对应的索引,当出发分页加载时,就会拿到下一页的页码请求新一页的数据。

目前在JetPack组件中,Paging是使用比较多的一个分页加载组件,但是Paging使用的场景有限,因为流的限制,导致只能是单一数据源,而且数据不能断,只能全部加载进来,因此决定手写一个分页加载组件,适用多种场景。

1.1 功能定制

如果想要自己写一个分页加载库,首先需要明白,分页加载组件需要做什么事?

对于RecyclerView来说,它的主要功能就是创建视图并绑定数据,因此我们先定义分页列表的基础能力,绑定视图和数据

interface IPagingList {     fun bindView(context: Context,lifecycleOwner: LifecycleOwner, recyclerView: RecyclerView,adapter: PagingAdapter,mode: ListMode) {}     fun bindData(model: List) {} }

bindData:

bindData就不多说了,就是绑定数据,首先我们拿到的数据一定是一个列表数据,因为并不知道业务方需要展示的数据类型是啥样的,因此需要泛型修饰,那么BasePagingModel是干什么的呢?

open class BasePagingModel(     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 : RecyclerView.Adapter() {     private var datas: List? = null     private var maps: MutableMap? = 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?     ) {     }     open fun bindBusinessData(         holder: RecyclerView.ViewHolder,         position: Int,         datas: List?     ) {     }     abstract fun buildBusinessHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder     fun setPagingData(datas: List) {         this.datas = datas         notifyDataSetChanged()     }     fun setPagingMapData(maps: MutableMap) {         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 : IPagingList, IModelProcess, LifecycleEventObserver {     private var mTotalScroll = 0     private var mCallback: IPagingCallback? = null     private var currentPageIndex = ""     //模式     private var mode: ListMode = ListMode.DATE     private var adapter: PagingAdapter? = null     //支持的类型 普通列表     private val dateMap: MutableMap by lazy {         mutableMapOf()     }     private val simpleList: MutableList by lazy {         mutableListOf()     }     override fun bindView(         context: Context,         lifecycleOwner: LifecycleOwner,         recyclerView: RecyclerView,         adapter: PagingAdapter,         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) {         //处理数据         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) {         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()                     list.add(model)                     dateMap[time] = list                 }             }         } else {             simpleList.addAll(data)         }     }     private fun updateCurrentPageIndex(data: List): 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) {     //处理数据     dealPagingModel(model)     //adapter刷新数据     if (mode == ListMode.DATE) {         adapter?.setPagingMapData(dateMap)     } else {         adapter?.setPagingData(simpleList)     } }

在调用bindData时会传入一页的数据,dealPagingModel方法用于处理数据,首先获取当前数据的页码,用于判断是否需要继续分页加载。

override fun dealPagingModel(data: List) {     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()                 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.247ms2022-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

大家可以在v1.0分支查看源码,在app模块中有一个demo大家可以看具体的使用方式,分页列表的代码在paging模块中

Android性能优化之RecyclerView分页加载组件功能怎么实现

感谢各位的阅读,以上就是“Android性能优化之RecyclerView分页加载组件功能怎么实现”的内容了,经过本文的学习后,相信大家对Android性能优化之RecyclerView分页加载组件功能怎么实现这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

推荐阅读: RecyclerView加载不同view实现效果--IT蓝豹 RecyclerView的刷新分页

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:[email protected]进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

android recyclerview 上一篇新闻:Element Plus组件Form表单Table表格二次封装怎么实现 下一篇新闻:怎么用elementUI+Springboot实现导出excel功能 猜你喜欢 怎么通过网站API接口查询ICP域名备案信息 怎么真正掌握Web前端技术 Web开发工具有哪些 Context-React如何跨组件访问数据 好程序员web前端教程分享常见基础面试题之性能优化 好程序员web前端分享H5高级工程师学习思路 bootstrap-表单控件——按钮 Jitamin在CentOS下面的安装部署过程 (一) js对象怎么根据时间进行排序 【MAC OS Sierra】PGP邮件加密教程 Id class 变量 的赋值规范 大驼峰和小驼峰 代码的格式和注释的类型


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3