Android性能優(yōu)化之RecyclerView分頁加載組件功能詳解
引言
在Android應(yīng)用中,列表有著舉足輕重的地位,幾乎所有的應(yīng)用都有列表的身影,但是對于列表的交互體驗一直是一個大問題。在性能比較好的設(shè)備上,列表滑動幾乎看不出任何卡頓,但是放在低端機上,卡頓會比較明顯,而且列表中經(jīng)常會伴隨圖片的加載,卡頓會更加嚴(yán)重,因此本章從手寫分頁加載組件入手,并對列表卡頓做出對應(yīng)的優(yōu)化
1 分頁加載組件
為什么要分頁加載,通常列表數(shù)據(jù)存儲在服務(wù)端會超過100條,甚至上千條,如果服務(wù)端一次性返回,我們一次性接受直接加載,如果其中有圖片加載,肯定直接報OOM,應(yīng)用崩潰,因此我們通常會跟服務(wù)端約定分頁的規(guī)則,服務(wù)端會按照頁碼從0開始給數(shù)據(jù),或者在數(shù)據(jù)中返回下一頁對應(yīng)的索引,當(dāng)出發(fā)分頁加載時,就會拿到下一頁的頁碼請求新一頁的數(shù)據(jù)。
目前在JetPack組件中,Paging是使用比較多的一個分頁加載組件,但是Paging使用的場景有限,因為流的限制,導(dǎo)致只能是單一數(shù)據(jù)源,而且數(shù)據(jù)不能斷,只能全部加載進來,因此決定手寫一個分頁加載組件,適用多種場景。
1.1 功能定制
如果想要自己寫一個分頁加載庫,首先需要明白,分頁加載組件需要做什么事?
對于RecyclerView來說,它的主要功能就是創(chuàng)建視圖并綁定數(shù)據(jù),因此我們先定義分頁列表的基礎(chǔ)能力,綁定視圖和數(shù)據(jù)
interface IPagingList<T> { fun bindView(context: Context,lifecycleOwner: LifecycleOwner, recyclerView: RecyclerView,adapter: PagingAdapter<T>,mode: ListMode) {} fun bindData(model: List<BasePagingModel<T>>) {} }
bindData:
bindData就不多說了,就是綁定數(shù)據(jù),首先我們拿到的數(shù)據(jù)一定是一個列表數(shù)據(jù),因為并不知道業(yè)務(wù)方需要展示的數(shù)據(jù)類型是啥樣的,因此需要泛型修飾,那么BasePagingModel是干什么的呢?
open class BasePagingModel<T>( var pageCount: String = "", //頁碼 var type: Int = 1, //分頁類型 1 帶日期 2 普通列表 var time: String = "", //如果是帶日期的model,那么需要傳入此值 var itemData: T? = null )
首先BasePagingModel是分頁列表中數(shù)據(jù)的基類,其中存儲的元素包括pageCount,代表傳進來的數(shù)據(jù)列表是哪一頁,type用來區(qū)分列表數(shù)據(jù)類型,time可以代表當(dāng)前數(shù)據(jù)在服務(wù)端的時間(主要場景就是列表中數(shù)據(jù)展示需要帶時間,并根據(jù)某一天進行數(shù)據(jù)聚合),itemData代表業(yè)務(wù)層需要處理的數(shù)據(jù)。
bindView:
對于RecyclerView來說,創(chuàng)建視圖、展示數(shù)據(jù)需要適配器,因此這里傳入了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() } }
這一章,我們先介紹使用場景比較多的單數(shù)據(jù)列表
PagingAdapter是一個抽象類,攜帶的數(shù)據(jù)同樣是業(yè)務(wù)方需要處理的數(shù)據(jù),是一個泛型,創(chuàng)建視圖方法buildBusinessHolder交給業(yè)務(wù)方實現(xiàn),這里我們關(guān)注兩個數(shù)據(jù)相關(guān)的方法 bindBusinessData和setPagingData,當(dāng)調(diào)用setPagingData方法時,將處理好的數(shù)據(jù)列表發(fā)進來,然后調(diào)用notifyDataSetChanged方法刷新列表,這個時候會調(diào)用bindBusinessData將列表中的數(shù)據(jù)綁定并展示出來。
這里我們還需要關(guān)注一個方法,這個方法業(yè)務(wù)方必須要實現(xiàn),這個方法有什么作用呢?
abstract fun getHolderWidth(context: Context):Int
這個方法用于返回列表中每個ItemView的尺寸寬度,因為在分頁組件中會判斷當(dāng)前列表可見的ItemView有多少個。這里大家可能會有疑問,RecyclerView的LayoutManager不是有對應(yīng)的api嗎,像
findFirstVisibleItemPosition() findLastVisibleItemPosition() findFirstCompletelyVisibleItemPosition() findLastCompletelyVisibleItemPosition()
為什么不用呢?因為我們的分頁組件是要兼容多種視圖形式的,雖然我們今天講到的普通列表用這個是沒有問題的,但是有些視圖類型是不能兼容這個api的,后續(xù)會介紹。
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的個數(shù) val visibleCount = getVisibleItemCount(recyclerView.context, recyclerView) if (recyclerView.childCount > 0 && visibleCount >= (getListCount(mode) ?: 0)) { if (currentPageIndex != "-1") { //請求下一頁數(shù)據(jù) 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>>) { //處理數(shù)據(jù) dealPagingModel(model) //adapter刷新數(shù)據(jù) 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個數(shù) */ private fun getVisibleItemCount(context: Context, recyclerView: RecyclerView): Int { var totalCount = 0 //首屏假設(shè)全部占滿 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下的元素個數(shù) count += dateMap[key]?.size ?: 0 } } else { count = simpleList.size } return count } }
首先,PagingList實現(xiàn)了IPagingList接口,我們先看實現(xiàn),在bindView方法中,其實就是給RecyclerView設(shè)置了適配器,然后注冊了RecyclerView的滑動監(jiān)聽,我們看下監(jiān)聽器中的主要實現(xiàn)。
onScrollStateChanged方法主要用于監(jiān)聽列表是否在滑動,當(dāng)列表的狀態(tài)為SCROLL_STATE_IDLE時,代表列表停止了滑動,這里做了兩件事:
(1)首先判斷列表是否滑動到了底部
if (!recyclerView.canScrollHorizontally(1) && currentPageIndex == "-1" && mTotalScroll > 0) { //滑動到底部 mCallback?.scrollEnd() }
這里需要滿足三個條件:recyclerView.canScrollHorizontally(1)如果返回了false,那么代表列表不能繼續(xù)滑動;還有就是會判斷currentPageIndex是否是最后一頁,如果等于-1那么就是最后一頁,同樣需要判斷滑動的距離,綜合來說就是【如果列表滑動到了最后一頁而且不能再繼續(xù)滑動了,那么就是到底了】,這里可以展示尾部的到底UI。
(2)判斷是否能夠觸發(fā)分頁加載
/** * 獲取可見的item個數(shù) */ private fun getVisibleItemCount(context: Context, recyclerView: RecyclerView): Int { var totalCount = 0 //首屏假設(shè)全部占滿 totalCount += ScreenUtils.getScreenWidth(recyclerView.context) / adapter?.getHolderWidth(context)!! totalCount += mTotalScroll / adapter?.getHolderWidth(context)!! return (totalCount + 1) }
首先這里會判斷展示了多少ItemView,之前提到的適配器中的getHolderWidth這里就用到了,首先我們會假設(shè)首屏全部占滿了ItemView,然后根據(jù)列表滑動的距離,判斷后續(xù)有多少ItemView展示出來,最終返回結(jié)果。
我們先不看下面的邏輯,因為分頁加載涉及到了數(shù)據(jù)的處理,因此我們先看下bindData的實現(xiàn)
override fun bindData(model: List<BasePagingModel<T>>) { //處理數(shù)據(jù) dealPagingModel(model) //adapter刷新數(shù)據(jù) if (mode == ListMode.DATE) { adapter?.setPagingMapData(dateMap) } else { adapter?.setPagingData(simpleList) } }
在調(diào)用bindData時會傳入一頁的數(shù)據(jù),dealPagingModel方法用于處理數(shù)據(jù),首先獲取當(dāng)前數(shù)據(jù)的頁碼,用于判斷是否需要繼續(xù)分頁加載。
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) } }
剩下的工作用于組裝數(shù)據(jù),simpleList用于存儲全部的列表數(shù)據(jù),每次傳入一頁數(shù)據(jù),都會存在這個集合中。處理完數(shù)據(jù)之后,將數(shù)據(jù)塞進adapter,用于刷新數(shù)據(jù)。
然后我們回到前面,我們在拿到了可見的ItemView的個數(shù)之后,首先會判斷recyclerView展示的ItemView個數(shù),如果等于0,那么就說明沒有數(shù)據(jù),就不需要觸發(fā)分頁加載。
if (recyclerView.childCount > 0 && visibleCount >= (getListCount(mode) ?: 0)) { if (currentPageIndex != "-1") { //請求下一頁數(shù)據(jù) mCallback?.scrollRefresh() } }
假設(shè)每頁展示10條數(shù)據(jù),這個時候getListCount方法返回的就是總的數(shù)據(jù)個數(shù)(10),如果visibleCount超過了List的總個數(shù),那么就需要觸發(fā)分頁加載,因為之前我們提到,最后一頁的index就是-1,所以這里判斷如果是最后一頁,就不需要分頁加載了。
1.3 生命周期管理
在PagingList中,我們實現(xiàn)了LifecycleEventObserver接口,這里的作用是什么呢?
就是我們知道,在列表中經(jīng)常會有圖片的加載,那么在圖片加載時如果滑動列表,那么勢必會產(chǎn)生卡頓,因此我們在滑動的過程中不會去加載圖片,而是在滑動停止時,重新加載,這個優(yōu)化體驗是沒有問題,用戶不會關(guān)注滑動時的狀態(tài)。
那么這里會存在一個問題,例如我們在滑動的過程中退出到后臺,這個時候列表滑動停止時加載圖片,可能存在上下文找不到的場景導(dǎo)致應(yīng)用崩潰,因此我們傳入生命周期的目的在于:讓列表具備感知生命周期的能力,當(dāng)列表處在不可見的狀態(tài)時,不能進行多余的網(wǎng)絡(luò)請求。
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--
我們可以看下具體的實現(xiàn)效果就是,當(dāng)觸發(fā)分頁加載時,scrollRefresh會被回調(diào),這里可以進行網(wǎng)絡(luò)請求,拿到數(shù)據(jù)之后再次調(diào)用bindData方法,然后繼續(xù)往下滑動,當(dāng)滑動到最后一頁時,scrollEnd被回調(diào),具體的使用,可以在demo中查看。
2 github
之前有小伙伴提到這個事情,希望在github上放出源碼,所以就做了 github.com/LLLLLaaayyy…
大家可以在v1.0分支查看源碼,在app模塊中有一個demo大家可以看具體的使用方式,分頁列表的代碼在paging模塊中
以上就是Android性能優(yōu)化之RecyclerView分頁加載組件功能詳解的詳細(xì)內(nèi)容,更多關(guān)于Android RecyclerView分頁加載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android抓取CSDN首頁極客頭條內(nèi)容完整實例
這篇文章主要介紹了Android抓取CSDN首頁極客頭條內(nèi)容完整實例,具有一定借鑒價值,需要的朋友可以參考下2018-01-01詳解Android中的ActivityThread和APP啟動過程
ActivityThread就是我們常說的主線程或UI線程,ActivityThread的main方法是整個APP的入口,本篇深入學(xué)習(xí)下ActivityThread,順便了解下APP和Activity的啟動過程。2021-06-06Android開發(fā)之MediaPlayer多媒體(音頻,視頻)播放工具類
這篇文章主要介紹了Android開發(fā)之MediaPlayer多媒體(音頻,視頻)播放工具類,涉及Android針對音頻文件的讀取、播放、暫停、繼續(xù)等操作實現(xiàn)技巧,需要的朋友可以參考下2017-12-12Android不規(guī)則封閉區(qū)域填充色彩的實例代碼
這篇文章主要介紹了Android不規(guī)則封閉區(qū)域填充色彩的實例代碼, 具有很好的參考價值,希望對大家有所幫助,一起跟隨小編過來看看吧2018-05-05Android 游戲引擎libgdx 資源加載進度百分比顯示案例分析
因為案例比較簡單,所以簡單用AndroidApplication -> Game -> Stage 搭建框架感興趣的朋友可以參考下2013-01-01Android車載空調(diào)系統(tǒng)(HVAC)開發(fā)方法分析
HVAC?全稱:供暖通風(fēng)與空氣調(diào)節(jié)(Heating?Ventilation?and?Air?Conditioning),用戶可以通過他來控制整個汽車的空調(diào)系統(tǒng),是汽車中非常重要的一個功能,汽車的空調(diào)HMI雖然并不復(fù)雜,但是大多都是用符號來表示功能,必須理解空調(diào)的各個符號表示的含義2023-12-12