在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模块中

Android性能优化 -- RecyclerView分页加载组件

假如有协助到我们,希望我们点个star,这个库会在后边的日子里继续更新,假如我们在事务中碰到新的需求,也能够随时留言