在 RecyclerView 中实现高效倒计时管理

1,461 阅读3分钟

1. 背景

在 Android 开发中,倒计时功能是一个常见需求,比如:

  • 订单支付倒计时
  • 限时活动显示
  • 直播倒计时

当倒计时与 RecyclerView 结合时,容易遇到以下问题:

  1. 倒计时错乱ViewHolder 复用导致倒计时重复启动或错误。
  2. 性能问题:每个 ViewHolder 启动单独的 CountDownTimer,导致资源浪费。
  3. 回收问题ViewHolder 解绑时,倒计时仍然在运行,导致 UI 更新异常。

本篇文章介绍一种高效的 RecyclerView 倒计时管理方案,能够避免倒计时错乱、减少资源消耗,并确保 UI 更新正确


2. 倒计时管理思路

2.1 设计目标

  • 全局管理倒计时,避免重复创建多个 CountDownTimer
  • 支持 ViewHolder 解绑时 UI 解绑,但倒计时继续
  • 倒计时结束后自动清理,避免内存泄漏

2.2 解决方案

  1. 使用全局 CountdownManager 统一管理所有倒计时
  2. 倒计时数据存入 Map<String, CountdownItem> ,每秒更新 UI
  3. onBindViewHolder() 绑定倒计时,立即更新 UI
  4. onViewRecycled() 解绑 UI,避免内存泄漏
  5. Map 为空时,自动停止定时任务,节省资源

3. 倒计时管理类实现

import android.os.SystemClock
import kotlinx.coroutines.*
import kotlin.collections.LinkedHashMap

class CountdownManager {
    private val countdownMap = LinkedHashMap<String, CountdownItem>()
    private var job: Job? = null

    // 启动倒计时更新任务
    private fun startCountdownLoop() {
        if (job?.isActive == true) return
        
        job = CoroutineScope(Dispatchers.Main).launch {
            while (isActive) {
                val currentTime = SystemClock.elapsedRealtime()
                val iterator = countdownMap.iterator()
                
                while (iterator.hasNext()) {
                    val (key, item) = iterator.next()
                    val remainingTime = item.endTimeMs - currentTime
                    
                    if (remainingTime <= 0) {
                        item.callback?.onFinish()
                        iterator.remove() // 倒计时结束,移除
                    } else {
                        item.callback?.onTick(remainingTime)
                    }
                }
                
                if (countdownMap.isEmpty()) {
                    stopCountdownLoop()
                } else {
                    delay(1000L)
                }
            }
        }
    }
    
    // 停止倒计时任务
    private fun stopCountdownLoop() {
        job?.cancel()
        job = null
    }
    
    // 注册倒计时
    fun register(key: String, remainingMs: Long, callback: CountdownCallback) {
        val endTimeMs = SystemClock.elapsedRealtime() + remainingMs
        countdownMap[key] = CountdownItem(endTimeMs, callback)
        callback.onTick(remainingMs.coerceAtLeast(0))
        startCountdownLoop()
    }
    
    // 解绑倒计时(仅解绑 UI,不停止计时)
    fun unregister(key: String) {
        countdownMap[key]?.callback = null
    }
    
    // 清空所有倒计时,防止内存泄漏
    fun clear() {
        stopCountdownLoop()
        countdownMap.clear()
    }

    private data class CountdownItem(
        val endTimeMs: Long,
        var callback: CountdownCallback?
    )

    interface CountdownCallback {
        fun onTick(remainingMs: Long)
        fun onFinish()
    }
}

4. RecyclerView 适配

class MyAdapter(private val countdownManager: CountdownManager) :
    RecyclerView.Adapter<MyAdapter.ViewHolder>() {

    private val items = mutableListOf<ItemData>()

    fun setItems(newItems: List<ItemData>) {
        items.clear()
        items.addAll(newItems)
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_layout, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.bind(item, countdownManager)
    }

    override fun onViewRecycled(holder: ViewHolder) {
        super.onViewRecycled(holder)
        holder.unbind(countdownManager)
    }

    override fun getItemCount(): Int = items.size

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val countdownText: TextView = itemView.findViewById(R.id.countdownText)
        private var currentKey: String? = null

        fun bind(item: ItemData, countdownManager: CountdownManager) {
            currentKey = item.id

            countdownManager.register(item.id, item.remainingMs, object :
                CountdownManager.CountdownCallback {
                override fun onTick(remainingMs: Long) {
                    countdownText.text = "${remainingMs / 1000}s"
                }

                override fun onFinish() {
                    countdownText.text = "倒计时结束"
                }
            })
        }

        fun unbind(countdownManager: CountdownManager) {
            currentKey?.let { countdownManager.unregister(it) }
            currentKey = null
        }
    }
}

5. 代码优化点

  1. 支持 RecyclerView 复用:解绑 UI 但倒计时继续,避免重复启动。
  2. 倒计时精准:使用 SystemClock.elapsedRealtime() 计算剩余时间。
  3. 节省资源:无倒计时时停止循环,避免空转。
  4. 自动清理:倒计时结束后移除 Map,避免内存泄漏。

可能的优化

  1. 后台支持:使用 WorkManager 实现后台倒计时。
  2. 更精准的 UI 更新:可减少 delay(1000L)500ms
  3. 增加暂停/恢复功能:可扩展 pause()resume() 方法。

6. 结语

这种方案高效管理 RecyclerView 中多个倒计时任务,在确保 UI 正确更新的同时,减少资源消耗,并避免倒计时错乱的问题。你可以根据需求进一步优化,比如支持后台倒计时、提高时间精度等。