| | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 更新所有可见的倒计时 |
| | | * 更新所有可见项的倒计时 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 此项移除 |
| | |
| | | } |
| | | } |
| | | |
| | | // 移除已超时的 holder,避免无效刷新 |
| | | timeComplete.forEach { |
| | | countdownHolders.remove(it) |
| | | } |
| | |
| | | |
| | | /** |
| | | * 开始倒计时循环 |
| | | * |
| | | * 发送倒计时消息,触发每秒刷新机制。 |
| | | */ |
| | | fun startCountdown() { |
| | | countdownHandler.removeMessages(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,用于后续更新时获取数据 |
| | | // 保存 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 |
| | |
| | | } |
| | | 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) |
| | |
| | | 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 |
| | |
| | | bind.colon1.setImageResource(imgId) |
| | | bind.colon2.setImageResource(imgId) |
| | | |
| | | // 倒计时全部归零时隐藏时间数字与冒号 |
| | | val hideTime = list[0] == 0L && list[1] == 0L && list[2] == 0L |
| | | |
| | | bind.hour.isVisible = !hideTime |
| | |
| | | |
| | | /** |
| | | * 计算目标时间与当前时间的差值,返回时分秒格式 |
| | | * |
| | | * @param targetTimeStr 目标时间字符串,格式为 yyyy-MM-dd HH:mm:ss |
| | | * @return 包含 [时, 分, 秒] 的列表;若输入为空则返回 [0, 0, 0] |
| | | */ |
| | | fun calculateTimeDifference(targetTimeStr: String?): List<Long> { |
| | | targetTimeStr?.let { |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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) |
| | |
| | | } |
| | | |
| | | companion object { |
| | | /** 倒计时消息标识 */ |
| | | const val MSG_COUNTDOWN = 1001 |
| | | } |
| | | } |