18034727885
2026-06-23 c87de07e5c055cb774da32d6deb3be310a42d64b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package com.xw.yunwei.ui.task.list
 
import android.annotation.SuppressLint
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.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xw.common.base.TwoItemElementClickListener
import com.xw.common.utils.BindUtils.getResColor
import com.xw.common.utils.CalculatorLineDistance
import com.xw.common.utils.LogUtils
import com.xw.yunwei.MyApp
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.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) {
            notifyDataSetChanged()
        }
    }
 
    /** 记录需要倒计时的 holder,key 为 holder.hashCode(),用于每秒刷新倒计时 UI */
    private val countdownHolders = ConcurrentHashMap<Int, ItemTaskContentBinding>()
 
    /** 倒计时 Handler,主线程每秒发送消息更新所有可见倒计时 */
    private val countdownHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if (msg.what == MSG_COUNTDOWN) {
                if (countdownHolders.isEmpty()) {
                    stopCountdown()
                } else {
                    updateAllCountdowns()
                    // 每秒刷新一次
                    sendEmptyMessageDelayed(MSG_COUNTDOWN, 1000)
                }
            }
        }
    }
 
    /**
     * 更新所有可见项的倒计时 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
 
            // 仅对待到达状态(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 此项移除
                    binding.countdownTips.text = "已超时"
                    timeComplete.add(code)
                }
            }
        }
 
        // 移除已超时的 holder,避免无效刷新
        timeComplete.forEach {
            countdownHolders.remove(it)
        }
    }
 
    /**
     * 开始倒计时循环
     *
     * 发送倒计时消息,触发每秒刷新机制。
     */
    fun startCountdown() {
        countdownHandler.removeMessages(MSG_COUNTDOWN)
        countdownHandler.sendEmptyMessage(MSG_COUNTDOWN)
    }
 
    /**
     * 停止倒计时循环
     *
     * 移除 Handler 中待处理的倒计时消息。
     */
    fun stopCountdown() {
        countdownHandler.removeMessages(MSG_COUNTDOWN)
    }
 
    @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,用于后续更新时获取数据
            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
                2 -> R.mipmap.ic_task_item_left_three
                3 -> R.mipmap.ic_task_item_left_four
                else -> null
            }
            levelId?.let { bind.level.setImageResource(levelId) }
 
            // 事故类待到达任务显示倒计时布局
            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 表示已超时
                    bind.countdownTips.text = "已超时"
                } 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)
            }
            setStatus(
                getResColor(com.xw.common.R.color.gray_8),
                com.xw.common.R.color.gray_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.text = "我已到达现场"
                canClick = false
                // 当定位与车辆位置均有效时,计算距离并判断是否在允许到达范围内
                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"
                    )
                }
            } 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.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]}"
 
        // 当剩余时间不足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
        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)
        bind.countdownTips.setTextColor(colorId)
 
        val bgId = if (showRed) R.drawable.shape_fa3e3e_bg_4 else R.drawable.shape_blue_5_bg_4
        bind.hour.setBackgroundResource(bgId)
        bind.minutes.setBackgroundResource(bgId)
        bind.second.setBackgroundResource(bgId)
 
        val imgId = if (showRed) R.mipmap.ic_task_item_colon_r else R.mipmap.ic_task_item_colon_b
        bind.colon1.setImageResource(imgId)
        bind.colon2.setImageResource(imgId)
 
        // 倒计时全部归零时隐藏时间数字与冒号
        val hideTime = list[0] == 0L && list[1] == 0L && list[2] == 0L
 
        bind.hour.isVisible = !hideTime
        bind.minutes.isVisible = !hideTime
        bind.second.isVisible = !hideTime
        bind.colon1.isVisible = !hideTime
        bind.colon2.isVisible = !hideTime
    }
 
    /**
     * 计算目标时间与当前时间的差值,返回时分秒格式
     *
     * @param targetTimeStr 目标时间字符串,格式为 yyyy-MM-dd HH:mm:ss
     * @return 包含 [时, 分, 秒] 的列表;若输入为空则返回 [0, 0, 0]
     */
    fun calculateTimeDifference(targetTimeStr: String?): List<Long> {
        targetTimeStr?.let {
            // 定义输入格式
            val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            // 解析目标时间
            val targetTime = LocalDateTime.parse(targetTimeStr, formatter)
            // 获取当前时间
            val now = LocalDateTime.now()
            // 计算时间差(毫秒)
            val duration = Duration.between(now, targetTime)
            val totalSeconds = max(duration.seconds, 0L)
            // 转换为时分秒
            val hours = TimeUnit.SECONDS.toHours(totalSeconds)
            val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
            val seconds = totalSeconds % 60
            return listOf(hours, minutes, seconds)
        }
        return listOf(0L, 0L, 0L)
    }
 
    /**
     * View 被回收时清理
     *
     * 移除该 holder 在倒计时注册表中的记录,防止对已回收视图进行刷新。
     */
    override fun onViewRecycled(holder: BaseViewHolder) {
        countdownHolders.remove(holder.hashCode())
        super.onViewRecycled(holder)
    }
 
    /**
     * Adapter 从 RecyclerView 分离时清理所有倒计时
     *
     * 停止 Handler 消息循环并清空 holder 注册表,防止内存泄漏。
     */
    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        stopCountdown()
        countdownHolders.clear()
    }
 
    companion object {
        /** 倒计时消息标识 */
        const val MSG_COUNTDOWN = 1001
    }
}