当前位置:首页 > 谈天说地

Android性能优化之RecyclerView分页加载组件功能详解

34资源网2022-09-06320

引言

在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分页加载组件功能详解的详细内容,更多关于android recyclerview分页加载的资料请关注萬仟网其它相关文章!

看完文章,还可以扫描下面的二维码下载快手极速版领4元红包

快手极速版二维码

快手极速版新人见面礼

除了扫码领红包之外,大家还可以在快手极速版做签到,看视频,做任务,参与抽奖,邀请好友赚钱)。

邀请两个好友奖最高196元,如下图所示:

快手极速版邀请好友奖励

扫描二维码推送至手机访问。

版权声明:本文由34楼发布,如需转载请注明出处。

本文链接:https://www.34l.com/post/21727.html

分享给朋友:

相关文章

想去丽江旅游,可是又怕得肺炎,只能手机上看看了

想去丽江旅游,可是又怕得肺炎,只能手机上看看了

最近天天刷抖音刷到丽江的景区,其中看到最多的就是丽江古城,好多导游都会介绍半天时间,那个丽江古城的街道看的我都快“流口水”了,真的太美了,下面给大家看下我用手机截图的一张丽江古城网红打卡点:…

好太太抽油烟机推荐,目前行业领先产品

好太太抽油烟机推荐,目前行业领先产品

好太太抽油烟机是目前行业当中比较领先的一个品牌,这个牌子的油烟机一直采用的是国际的先进生产技术,而且技术都是达到国际的标准了。另外好太太油烟机还有不同的研发中心。当收集完消费者的意见以后,好太太油烟机的研发中心就开始进行研发,生产出合乎消费…

自动回复的经典句子,这11句值得收藏

自动回复的经典句子,这11句值得收藏

1、什么风把您吹来了,是timi赢了么。…

华盛顿:自己不能胜任的事情,切莫轻易答应别人

华盛顿:自己不能胜任的事情,切莫轻易答应别人

自己不能胜任的事情,切莫轻易答应别人,一旦答应了别人,就必须实践自己的诺言。——华盛顿 这则名言告诉我们什么道理?我们应该怎么做?…

天上的孩子电影好看吗?看豆瓣网友如何评价的吧

天上的孩子电影好看吗?看豆瓣网友如何评价的吧

由胡玫监制许磊导演编剧的电影《天上的孩子》。电影主要讲述了来自贵州的夫妻老何、玲霞5岁的独子查出绝症,不久于人世。为了让儿子的名字刻在纪念碑上,夫妻俩决定捐献儿子的器官却困难重重。…

引流文案微信推广(微商引流推广文案模板)

引流文案微信推广(微商引流推广文案模板)

大家好啊!今天又跟大家分享小技巧啦~往下看↓↓↓ 首先说一下什么样的文案是引流型的?实际上,一句话是将公共域流量定向到您的私有域流量池。其目的是先引流然后慢慢进行信任激活变现。 在标题方面,通常有以下几种类型,今天为大家详细描述一下。 一…