<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在Android應用中,列表有著舉足輕重的地位,幾乎所有的應用都有列表的身影,但是對於列表的互動體驗一直是一個大問題。在效能比較好的裝置上,列表滑動幾乎看不出任何卡頓,但是放在低端機上,卡頓會比較明顯,而且列表中經常會伴隨圖片的載入,卡頓會更加嚴重,因此本章從手寫分頁載入元件入手,並對列表卡頓做出對應的優化
為什麼要分頁載入,通常列表資料儲存在伺服器端會超過100條,甚至上千條,如果伺服器端一次性返回,我們一次性接受直接載入,如果其中有圖片載入,肯定直接報OOM,應用崩潰,因此我們通常會跟伺服器端約定分頁的規則,伺服器端會按照頁碼從0開始給資料,或者在資料中返回下一頁對應的索引,當出發分頁載入時,就會拿到下一頁的頁碼請求新一頁的資料。
目前在JetPack元件中,Paging是使用比較多的一個分頁載入元件,但是Paging使用的場景有限,因為流的限制,導致只能是單一資料來源,而且資料不能斷,只能全部載入進來,因此決定手寫一個分頁載入元件,適用多種場景。
如果想要自己寫一個分頁載入庫,首先需要明白,分頁載入元件需要做什麼事?
對於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的,後續會介紹。
先把第一版的程式碼貼出來,有個完整的體系
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,所以這裡判斷如果是最後一頁,就不需要分頁載入了。
在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中檢視。
之前有小夥伴提到這個事情,希望在github上放出原始碼,所以就做了 github.com/LLLLLaaayyy…
大家可以在v1.0分支檢視原始碼,在app模組中有一個demo大家可以看具體的使用方式,分頁列表的程式碼在paging模組中
以上就是Android效能優化之RecyclerView分頁載入元件功能詳解的詳細內容,更多關於Android RecyclerView分頁載入的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45