RecyclerView 实现Item倒计时效果

前言

平时我们做倒计时有很多种方法实现,也相对比较简单。但是最近碰到一个需求,需要在RecyclerView的item中实现倒计时,一般这种场景在电商的业务上应该也会有。那么我们就不能像开始单个倒计时一样简单做了,还需要考虑到viewholder的复用、性能等问题。

效果

这里可以简单先写个Demo看看效果

功能实现

1. 倒计时功能实现

核心就是开启一个计时器,每秒都更新时间到页面上,这个计时器的实现就很多了,比如直接handler,或者kotlin能用flow去做,或者TimerTask这些也能实现。打个广告,要做精准的倒计时可以看这篇文章juejin.cn/post/714065...

我这里是做Demo演示,为了代码整洁和方便,我就用TimerTask来做。

2. 设计思路

试着想想,你用RecyclerView做倒计时,每个ViewHolder的倒计时时间都不同,难道要在每个ViewHolder中开一个TimerTask来做吗?然后对viewholder的缓存再单独做处理?

我的想法是可以所有Item共用一个倒计时

这个系统有3个重要部分组成:

(1)倒计时的实现,只用一个TimerTask来做一个心跳的效果。每次心跳去更新正在显示的Item页面的倒计时 ,比如你有100个Item,但是显示在屏幕上的只有5个,那我只需要关心这5个Item的时间变动,其他95个没必要做处理。

(2)观察者队列。我的每次心跳都要通知正在显示的Item更新页面,那是不是很明显要通过观察者模式去做。

(3)倒计时时间列表。倒计时也需要用一个列表管理起来,recyclerview的页面显示是根据数据去显示,虽然比如说100个数组只需要5、6个viewholder来复用,但是你的差异数据还是100个数据。

3. 倒计时列表

倒计时列表的实现,我这里是用一个HashMap来实现,因为方便直接获取某个实际Item的当前倒计时的时间。

kotlin 复制代码
private val cdMap: HashMap<Long, Long> = HashMap()

我的key假设用一个id来做处理,因为我们的数据结构中基本都会存在id并且id是基础数据类型。

kotlin 复制代码
data class RcdItemData(  
    var id : Long,   //  id
    var cd : Long    //  总倒计时时间
)

添加倒计时

kotlin 复制代码
fun addCountDown(id: Long, totalCd: Long, isCover: Boolean = false) {  
    if (cdMap.containsKey(id)) {  
        if (isCover) {  
            cdMap[id] = totalCd  
        }  
    } else {  
        cdMap[id] = totalCd  
    }  
}

这个isCover是一个策略,adapter数据刷新时是否更新倒计时,这里可以先不用管,可以简单看成

kotlin 复制代码
fun addCountDown(id: Long, totalCd: Long, isCover: Boolean = false) {  
    if (!cdMap.containsKey(id)) {  
        cdMap[id] = totalCd  
    }
}

清除倒计时(比如页面退出时就需要做释放操作)

kotlin 复制代码
fun clearCountDown() {  
    cdMap.clear()  
}

获取某个Item当前倒计时的时间

kotlin 复制代码
fun getCountDownById(id: Long): Long? {  
    if (cdMap.containsKey(id)) {  
        return cdMap[id]  
    }  

    return null  
}

更新时间(随心跳更新所有数据)

kotlin 复制代码
private fun updateCdByMap() {  
    cdMap.forEach { (t, u) ->  
    if (cdMap[t]!! > 0) {  
        cdMap[t] = u - 1  
    }   
    
    ......
}

这些代码都不难理解,就不过多解释了

4. 观察者数组实现

先创建一个观察者数组

php 复制代码
private var viewHolderObservables: ArrayList<OnItemSchedule?> = ArrayList()

然后就是最基础的添加观察者和移除观察者操作

kotlin 复制代码
fun addHolderObservable(onItemSchedule: OnItemSchedule?) {  
    viewHolderObservables.add(onItemSchedule)  
}  
  
fun removeHolderObservable(onItemSchedule: OnItemSchedule?) {  
    viewHolderObservables.remove(onItemSchedule)  
}  
  
fun releaseHolderObservable() {  
    viewHolderObservables.clear()  
}

通知观察者(通知Item倒计时1秒了,可以刷新页面了)

kotlin 复制代码
private fun notifyCdFinish() {  
    viewHolderObservables.forEach {  
        it?.onCdSchedule()  
    }  
}

5. 倒计时心跳实现

前面说了,我们让所有的Item共用一个倒计时,也是通过一个心跳去更新各自倒计时时间

csharp 复制代码
private var task: TimerTask? = null  
private var timer: Timer? = null

开始倒计时

kotlin 复制代码
fun startHeartBeat() {  
    if (task == null) {  
        timer = Timer()  
        task = object : TimerTask() {  
            override fun run() {  
                updateCdByMap()  
            }  
        }  
        timer?.schedule(task, 1000, 1000) // 每隔 1 秒钟执行一次  
    }  
}

每一秒都会调用updateCdByMap()方法去刷新时间。

scss 复制代码
private fun updateCdByMap() {  
    cdMap.forEach { (t, u) ->  
        if (cdMap[t]!! > 0) {  
            cdMap[t] = u - 1  
        }  
    }  
    
    // 更改完数据之后通知观察者  
    Handler(Looper.getMainLooper()).post {  
        notifyCdFinish()  
    }  
}

TimerTask会在子线程中进行,所以最后通知观察者的操作需要切到主线程

最后关闭倒计时(页面关闭这些时机调用)

kotlin 复制代码
fun closeHeartBeat() {  
    task?.cancel()  
    task = null  
    timer = null  
}

6. 整体功能

因为上面是拆开来解释说明,这里再把整个工具的代码合起来可能会比较好管理。

kotlin 复制代码
object RecyclerCountDownManager {  
  
    private var task: TimerTask? = null  
    private var timer: Timer? = null  

    // viewHolder观察者  
    private var viewHolderObservables: ArrayList<OnItemSchedule?> = ArrayList()  

    // 倒计时对象数组  
    private val cdMap: HashMap<Long, Long> = HashMap()  

    /**  
    * 添加viewHolder观察  
    */  
    fun addHolderObservable(onItemSchedule: OnItemSchedule?) {  
        viewHolderObservables.add(onItemSchedule)  
    }  

    fun removeHolderObservable(onItemSchedule: OnItemSchedule?) {  
        viewHolderObservables.remove(onItemSchedule)  
    }  

    fun releaseHolderObservable() {  
        viewHolderObservables.clear()  
    }  

    /**  
    * 添加倒计时对象  
    * @param totalCd 总倒计时时间  
    * @param isCover 是否覆盖  
    */  
    fun addCountDown(id: Long, totalCd: Long, isCover: Boolean = false) {  
        if (cdMap.containsKey(id)) {  
            if (isCover) {  
                cdMap[id] = totalCd  
            }  
        } else {  
            cdMap[id] = totalCd  
        }  
    }  

    /**  
    * 清除倒计时  
    */  
    fun clearCountDown() {  
        cdMap.clear()  
    }  

    /**  
    * 根据id获取倒计时  
    */  
    fun getCountDownById(id: Long): Long? {  
        if (cdMap.containsKey(id)) {  
            return cdMap[id]  
        }  

        return null  
    }  

    /**  
    * 开始心跳  
    */  
    fun startHeartBeat() {  
        if (task == null) {  
            timer = Timer()  
            task = object : TimerTask() {  
                override fun run() {  
                    updateCdByMap()  
                }  
            }  
            timer?.schedule(task, 1000, 1000) // 每隔 1 秒钟执行一次  
        }  
    }  

    /**  
    * 更新所有倒计时对象  
    */  
    private fun updateCdByMap() {  
        cdMap.forEach { (t, u) ->  
            if (cdMap[t]!! > 0) {  
                cdMap[t] = u - 1  
            }  
        }  
        // 更改完数据之后通知观察者  
        Handler(Looper.getMainLooper()).post {  
            notifyCdFinish()  
        }  
    }  

    private fun notifyCdFinish() {  
        viewHolderObservables.forEach {  
            it?.onCdSchedule()  
        }  
    }  

    /**  
    * 关闭心跳  
    */  
    fun closeHeartBeat() {  
        task?.cancel()  
        task = null  
        timer = null  
    }  

    /**  
    * 调度通知,一般由ViewHolder实现该接口  
    */  
    interface OnItemSchedule {  

        fun onCdSchedule()  

    }  

  
}

可以看到代码都整体比较简单,就不用过多说明,就是需要注意一下这个是用一个单例去实现的工具,在页面关闭之后需要手动调用closeHeartBeat()、clearCountDown()、releaseHolderObservable()去释放资源。

调用的地方,Demo的Adapter

kotlin 复制代码
class RcdAdapter(var context: Context, var list: List<RcdItemData>) :  
    RecyclerView.Adapter<RcdAdapter.RcdViewHolder>() {  
  
    init {  
        // 因为模式默认选择不覆盖,需要每次添加前先清除  
        RecyclerCountDownManager.clearCountDown()  
        list.forEach {  
            RecyclerCountDownManager.addCountDown(it.id, it.cd)  
        }  
    }  

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RcdViewHolder {  
        val text: TextView = TextView(context)  
        text.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 64)  
        text.gravity = Gravity.CENTER  
        val holder = RcdViewHolder(text)  
        RecyclerCountDownManager.addHolderObservable(holder)  
        return holder  
    }  

    override fun getItemCount(): Int {  
        return list.size  
    }  

    override fun onBindViewHolder(holder: RcdViewHolder, position: Int) {  
        holder.setData(list[position])  
    }  

    class RcdViewHolder(var view: TextView) : RecyclerView.ViewHolder(view),  
        RecyclerCountDownManager.OnItemSchedule {  

        private var mData: RcdItemData? = null  

        fun setData(data: RcdItemData) {  
            mData = data  
        }  

        override fun onCdSchedule() {  
            val cd = mData?.id?.let { RecyclerCountDownManager.getCountDownById(it) }  
            if (cd != null) {  
            // 测试展示分秒  
                view.text = "${String.format("%02d", cd / 60)}:${String.format("%02d", cd % 60)}"  
            }  
        }  

    }  

}

其他都比较基础的adapter的写法,就是viewholder要实现RecyclerCountDownManager.OnItemSchedule来充当观察者,然后拿到列表数据后调用RecyclerCountDownManager.addCountDown(it.id, it.cd)去创建倒计时列表。在onCreateViewHolder中调用RecyclerCountDownManager.addHolderObservable(holder)去添加观察者。最后在onCdSchedule()回调中做倒计时的更新

在页面销毁的时候主动释放内存

相关推荐
大白要努力!2 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟3 小时前
Android音频采集
android·音视频
小白也想学C4 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程4 小时前
初级数据结构——树
android·java·数据结构
闲暇部落6 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX8 小时前
Android 分区相关介绍
android
大白要努力!9 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee9 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood9 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-12 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记