1. 背景
在 Android 开发中,倒计时功能是一个常见需求,比如:
- 订单支付倒计时
- 限时活动显示
- 直播倒计时
当倒计时与 RecyclerView 结合时,容易遇到以下问题:
- 倒计时错乱:
ViewHolder复用导致倒计时重复启动或错误。 - 性能问题:每个
ViewHolder启动单独的CountDownTimer,导致资源浪费。 - 回收问题:
ViewHolder解绑时,倒计时仍然在运行,导致 UI 更新异常。
本篇文章介绍一种高效的 RecyclerView 倒计时管理方案,能够避免倒计时错乱、减少资源消耗,并确保 UI 更新正确。
2. 倒计时管理思路
2.1 设计目标
- 全局管理倒计时,避免重复创建多个
CountDownTimer。 - 支持 ViewHolder 解绑时 UI 解绑,但倒计时继续。
- 倒计时结束后自动清理,避免内存泄漏。
2.2 解决方案
- 使用全局
CountdownManager统一管理所有倒计时。 - 倒计时数据存入
Map<String, CountdownItem>,每秒更新 UI。 - 在
onBindViewHolder()绑定倒计时,立即更新 UI。 - 在
onViewRecycled()解绑 UI,避免内存泄漏。 - 当
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. 代码优化点
- 支持 RecyclerView 复用:解绑 UI 但倒计时继续,避免重复启动。
- 倒计时精准:使用
SystemClock.elapsedRealtime()计算剩余时间。 - 节省资源:无倒计时时停止循环,避免空转。
- 自动清理:倒计时结束后移除
Map,避免内存泄漏。
可能的优化
- 后台支持:使用
WorkManager实现后台倒计时。 - 更精准的 UI 更新:可减少
delay(1000L)为500ms。 - 增加暂停/恢复功能:可扩展
pause()和resume()方法。
6. 结语
这种方案高效管理 RecyclerView 中多个倒计时任务,在确保 UI 正确更新的同时,减少资源消耗,并避免倒计时错乱的问题。你可以根据需求进一步优化,比如支持后台倒计时、提高时间精度等。