puzhibing
2026-08-21 5fa94b3718b4a78a4fcea4c97652b0ef20b7de3a
app/src/main/java/com/xw/yunwei/ui/task/list/TaskListAdapter.kt
@@ -4,14 +4,13 @@
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.view.View
import androidx.core.view.isVisible
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.amap.api.maps.model.LatLng
import com.blankj.utilcode.util.TimeUtils
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xw.common.TimeDialog.Companion.getTime
import com.xw.common.base.TwoItemElementClickListener
import com.xw.common.utils.BindUtils.getResColor
import com.xw.common.utils.CalculatorLineDistance
@@ -20,32 +19,54 @@
import com.xw.yunwei.R
import com.xw.yunwei.databinding.ItemTaskContentBinding
import com.xw.yunwei.model.TaskModel
import com.xw.yunwei.model.taskPlaceHolder
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.math.max
/**
 * 任务列表适配器
 *
 * 负责展示任务列表项,支持以下功能:
 * - 根据任务紧急程度显示不同等级图标
 * - 对事故类待到达任务进行倒计时展示
 * - 根据当前定位计算与车辆距离,控制"到达现场"按钮可用状态
 * - 支持列表项点击、操作按钮点击及导航点击事件
 *
 * @property listener 列表项及子元素点击事件回调
 */
class TaskListAdapter(var listener: TwoItemElementClickListener<TaskModel>) :
    BaseQuickAdapter<TaskModel, BaseViewHolder>(R.layout.item_task_content) {
    /** 当前纬度,用于计算与任务车辆的距离 */
    var lat: Double = 0.0
    /** 当前经度,用于计算与任务车辆的距离 */
    var lng: Double = 0.0
    /**
     * 设置当前定位坐标
     *
     * @param lat 纬度,为空时默认为 0.0
     * @param lng 经度,为空时默认为 0.0
     * @param refresh 是否立即刷新列表,默认为 true
     */
    @SuppressLint("NotifyDataSetChanged")
    fun setLocation(lat: Double?, lng: Double?, refresh: Boolean = true) {
        this.lat = lat ?: 0.0
        this.lng = lng ?: 0.0
        if (refresh) {
            recyclerView.post { notifyDataSetChanged() }
            notifyDataSetChanged()
        }
    }
    // 记录需要倒计时的holder,key为holder.hashCode()
    /** 记录需要倒计时的 holder,key 为 holder.hashCode(),用于每秒刷新倒计时 UI */
    private val countdownHolders = ConcurrentHashMap<Int, ItemTaskContentBinding>()
    // 倒计时Handler
    /** 倒计时 Handler,主线程每秒发送消息更新所有可见倒计时 */
    private val countdownHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if (msg.what == MSG_COUNTDOWN) {
@@ -61,17 +82,27 @@
    }
    /**
     * 更新所有可见的倒计时
     * 更新所有可见项的倒计时 UI
     *
     * 遍历当前已注册的倒计时 holder,计算剩余时间并更新显示;
     * 若倒计时结束(时分秒均为 0),则标记为"已超时"并从注册表中移除。
     */
    private fun updateAllCountdowns() {
        if (!recyclerView.isAttachedToWindow) {
            return
        }
        val timeComplete = mutableListOf<Int>()
        countdownHolders.forEach { (code, binding) ->
            if (!binding.root.isAttachedToWindow) {
                return@forEach
            }
            // 获取当前绑定的数据
            val position = binding.root.tag as? Int ?: return@forEach
            val item = getItemOrNull(position) ?: return@forEach
            if (item.status == 0 && item.taskCategory == 2) {
            // 仅对待到达状态(status=0)的事故任务(taskCategory=2)进行倒计时
            if ((item.status == 0 || item.status == 4) && item.taskCategory == 2) {
                val list = calculateTimeDifference(item.endTime)
                updateTime(binding, list)
                if (list.all { it == 0L }) { // 全0 此项移除
@@ -81,6 +112,7 @@
            }
        }
        // 移除已超时的 holder,避免无效刷新
        timeComplete.forEach {
            countdownHolders.remove(it)
        }
@@ -88,6 +120,8 @@
    /**
     * 开始倒计时循环
     *
     * 发送倒计时消息,触发每秒刷新机制。
     */
    fun startCountdown() {
        countdownHandler.removeMessages(MSG_COUNTDOWN)
@@ -96,6 +130,8 @@
    /**
     * 停止倒计时循环
     *
     * 移除 Handler 中待处理的倒计时消息。
     */
    fun stopCountdown() {
        countdownHandler.removeMessages(MSG_COUNTDOWN)
@@ -103,10 +139,21 @@
    @SuppressLint("SetTextI18n")
    override fun convert(holder: BaseViewHolder, item: TaskModel) {
        if (!recyclerView.isAttachedToWindow) {
            return
        }
        if (holder.bindingAdapterPosition == RecyclerView.NO_POSITION) {
            return
        }
        DataBindingUtil.bind<ItemTaskContentBinding>(holder.itemView)?.let { bind ->
            // 保存position到tag,用于后续更新时获取数据
            // 保存 position 到 tag,用于后续更新时获取数据
            bind.root.tag = holder.bindingAdapterPosition
            bind.clContent.visibility = if (item == taskPlaceHolder) View.GONE else View.VISIBLE
            bind.tips.visibility = if (item == taskPlaceHolder) View.VISIBLE else View.GONE
            if (item == taskPlaceHolder) {
                return
            }
            // 根据任务紧急程度设置左侧等级图标
            val levelId = when (item.taskUrgencyLevel) {
                0 -> R.mipmap.ic_task_item_left_one
                1 -> R.mipmap.ic_task_item_left_two
@@ -116,30 +163,51 @@
            }
            levelId?.let { bind.level.setImageResource(levelId) }
            if (item.status == 0 && item.taskCategory == 2) {
            // 事故类待到达任务显示倒计时布局
            if ((item.status == 0 || item.status == 4) && item.taskCategory == 2) {
                bind.countdownLayout.isVisible = true
                val list = calculateTimeDifference(item.endTime)
                updateTime(bind, list)
                if (list.all { it == 0L }) { // 全0
                if (list.all { it == 0L }) { // 全0 表示已超时
                    bind.countdownTips.text = "已超时"
                } else { // 注册到倒计时列表
                } else { // 注册到倒计时列表,启动每秒刷新
                    bind.countdownTips.text = "倒计时结束前,务必到达现场"
                    countdownHolders[holder.hashCode()] = bind
                    startCountdown()
                }
            } else {
                bind.countdownLayout.isVisible = false
                // 如果不是倒计时项,从列表移除
                // 如果不是倒计时项,从列表移除,避免内存泄漏
                countdownHolders.remove(holder.hashCode())
            }
            // 设置任务类型文本:事故或常规任务类型名称
            if (item.isAccidentTask()) {
                bind.type.text = "事故"
            } else if (item.isRoutineTask()) {
                bind.type.text = item.getTaskType()
            }
            // 先按"与 type 同行"展示车牌号;待布局完成后检测是否被省略,
            // 若被省略则把车牌号下移到 type 的下一行整行显示。
            bind.plateNumber.text = item.plateNumber
            bind.plateNumberFull.text = item.plateNumber
            bind.plateNumber.isVisible = true
            bind.plateNumberFull.isVisible = false
            bind.plateNumber.post {
                val layout = bind.plateNumber.layout ?: return@post
                val ellipsized = (0 until layout.lineCount).any {
                    layout.getEllipsisCount(it) > 0
                }
                if (ellipsized) {
                    bind.plateNumber.visibility = View.INVISIBLE
                    bind.plateNumberFull.isVisible = true
                } else {
                    bind.plateNumber.isVisible = true
                    bind.plateNumberFull.isVisible = false
                }
            }
            // 初始化状态文本颜色与背景
            val setStatus: (Int, Int) -> Unit = { text, bg ->
                bind.status.setTextColor(text)
                bind.status.setBackgroundResource(bg)
@@ -148,59 +216,96 @@
                getResColor(com.xw.common.R.color.gray_8),
                com.xw.common.R.color.gray_3
            )
            // 0 = 待到达 1 = 待处理 2 = 已完成 3 = 已取消
            // 0 = 待到达 1 = 待处理 2 = 已完成 3 = 已取消 4 = 改派中
            when (item.status) {
                0 -> bind.status.text = "待到达"
                1 -> bind.status.text = "待处理"
                2 -> bind.status.text = "已处理"
                3 -> bind.status.text = "已取消"
                4 -> bind.status.text = "改派中"
            }
            bind.ivArrow.visibility = if (item.status == 4) View.GONE else View.VISIBLE
            bind.address.text = item.vehicleLocation
            bind.llNotice.visibility = if (item.status == 4) View.VISIBLE else View.GONE
            // 控制"操作按钮"是否可点击
            var canClick = true
            // 仅对待到达或待处理状态的事故任务显示操作按钮
            bind.tvControl.isVisible = item.status in arrayOf(0, 1) && item.taskCategory == 2
            if (item.status == 0) {
                bind.tvControl.isVisible = MyApp.findPermissionByName("到达事故现场")?.visible == "0"
                // 检查是否有"到达事故现场"权限
                bind.tvControl.isVisible =
                    MyApp.findPermissionByName("到达事故现场")?.visible == "0"
                bind.tvControl.text = "我已到达现场"
                canClick = false
                if (lat != 0.0 && lng != 0.0 && (item.vehicleLat ?: 0.0) != 0.0 && (item.vehicleLon ?: 0.0) != 0.0) {
                // 当定位与车辆位置均有效时,计算距离并判断是否在允许到达范围内
                if (lat != 0.0 && lng != 0.0 && (item.vehicleLat ?: 0.0) != 0.0 && (item.vehicleLon
                        ?: 0.0) != 0.0
                ) {
                    val dis = CalculatorLineDistance.calculate(
                        LatLng(lat, lng),
                        LatLng(item.vehicleLat!!, item.vehicleLon!!)
                    )
                    canClick = (item.canArriveRange ?: 0) > dis.toInt()
                    LogUtils.i("zmc-test-adapter","${item.plateNumber} dis = $dis canArriveRange = ${item.canArriveRange} canClick = $canClick")
                    LogUtils.i(
                        "zmc-test-adapter",
                        "${item.plateNumber} dis = $dis canArriveRange = ${item.canArriveRange} canClick = $canClick"
                    )
                }
            } else if (item.status == 1) {
                bind.tvControl.text = "处理事故"
            }
            // 根据按钮可用状态设置不同背景样式
            if (canClick) {
                bind.tvControl.setBackgroundResource(R.drawable.shape_blue_5_bg_4)
            } else {
                bind.tvControl.setBackgroundResource(R.drawable.shape_blue_1_bg_4)
            }
            bind.root.setOnClickListener { listener.onItemClick(item) }
            bind.tvControl.setOnClickListener { if (canClick) listener.oneItemElementClick(item) }
            bind.nav.setOnClickListener { listener.twoItemElementClick(item) }
            // 设置点击事件:整项点击、操作按钮点击、导航点击
            bind.clContent.setOnClickListener {
                if (item.status == 4) {
                    return@setOnClickListener
                }
                listener.onItemClick(item)
            }
            bind.tvControl.setOnClickListener {
                if (item.status == 4) {
                    return@setOnClickListener
                }
                if (canClick) listener.oneItemElementClick(item)
            }
            bind.nav.setOnClickListener {
                if (item.status == 4) {
                    return@setOnClickListener
                }
                listener.twoItemElementClick(item)
            }
        }
    }
    /**
     * 更新倒计时时间显示及样式
     *
     * @param bind 列表项视图绑定对象
     * @param list 包含时、分、秒的列表
     */
    private fun updateTime(bind: ItemTaskContentBinding, list: List<Long>) {
        bind.hour.text = if (list[0] < 10) "0${list[0]}" else "${list[0]}"
        bind.minutes.text = if (list[1] < 10) "0${list[1]}" else "${list[1]}"
        bind.second.text = if (list[2] < 10) "0${list[2]}" else "${list[2]}"
        val showRed = list[0] == 0L && list[1] == 0L
        // 当剩余时间不足3分钟时,切换为红色警示样式
        val showRed = list[0] == 0L && list[1] <= 2L
        val countdownId = if (showRed) R.mipmap.img_task_item_top_bg_red else R.mipmap.img_task_item_top_bg_blue
        val countdownId =
            if (showRed) R.mipmap.img_task_item_top_bg_red else R.mipmap.img_task_item_top_bg_blue
        bind.countdownLayout.setBackgroundResource(countdownId)
        val colorId = if (showRed) getResColor(com.xw.common.R.color.red_6) else getResColor(com.xw.common.R.color.blue_5)
        val colorId =
            if (showRed) getResColor(com.xw.common.R.color.red_6) else getResColor(com.xw.common.R.color.blue_5)
        bind.countdownTips.setTextColor(colorId)
        val bgId = if (showRed) R.drawable.shape_fa3e3e_bg_4 else R.drawable.shape_blue_5_bg_4
@@ -212,6 +317,7 @@
        bind.colon1.setImageResource(imgId)
        bind.colon2.setImageResource(imgId)
        // 倒计时全部归零时隐藏时间数字与冒号
        val hideTime = list[0] == 0L && list[1] == 0L && list[2] == 0L
        bind.hour.isVisible = !hideTime
@@ -223,6 +329,9 @@
    /**
     * 计算目标时间与当前时间的差值,返回时分秒格式
     *
     * @param targetTimeStr 目标时间字符串,格式为 yyyy-MM-dd HH:mm:ss
     * @return 包含 [时, 分, 秒] 的列表;若输入为空则返回 [0, 0, 0]
     */
    fun calculateTimeDifference(targetTimeStr: String?): List<Long> {
        targetTimeStr?.let {
@@ -234,7 +343,7 @@
            val now = LocalDateTime.now()
            // 计算时间差(毫秒)
            val duration = Duration.between(now, targetTime)
            val totalSeconds = max(duration.seconds,0L)
            val totalSeconds = max(duration.seconds, 0L)
            // 转换为时分秒
            val hours = TimeUnit.SECONDS.toHours(totalSeconds)
            val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
@@ -245,15 +354,19 @@
    }
    /**
     * View被回收时清理
     * View 被回收时清理
     *
     * 移除该 holder 在倒计时注册表中的记录,防止对已回收视图进行刷新。
     */
    override fun onViewRecycled(holder: BaseViewHolder) {
        super.onViewRecycled(holder)
        countdownHolders.remove(holder.hashCode())
        super.onViewRecycled(holder)
    }
    /**
     * Adapter从RecyclerView分离时清理所有倒计时
     * Adapter 从 RecyclerView 分离时清理所有倒计时
     *
     * 停止 Handler 消息循环并清空 holder 注册表,防止内存泄漏。
     */
    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
@@ -262,6 +375,7 @@
    }
    companion object {
        /** 倒计时消息标识 */
        const val MSG_COUNTDOWN = 1001
    }
}