瀏覽代碼

refactor(更新) :
- 新增地图列表切换功能
- 优化地图加载及点位刷新逻辑
- 优化自定义开关图层绘制及交互逻辑
- 修复其他已知问题

周文健 2 月之前
父節點
當前提交
dbc15b7c59

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/MyApplication.kt

@@ -42,7 +42,7 @@ class MyApplication : Application() {
 
         BusinessManager.initMsgEventBus()
 
-        ArcSoftUtil.checkActiveStatus(this)
+//        ArcSoftUtil.checkActiveStatus(this)
 
 
         NetApi.logout()

+ 9 - 4
app/src/main/java/com/grkj/iscs_mars/modbus/DockBean.kt

@@ -408,14 +408,18 @@ class DockBean(
                                 deviceList.filterIsInstance<SwitchBean>()
                                     .find { it.switchBoardAddr == switchBoardAddr[1] && it.idx == idx }
                                     ?.let {
-                                        it.enabled =
-                                            switchStatus[idx]
+                                        if (it.enabled != switchStatus[idx]) {
+                                            it.enabled =
+                                                switchStatus[idx]
+                                            it.changed = true
+                                        }
                                     } ?: run {
                                     deviceList.add(
                                         SwitchBean(
                                             idx,
                                             switchBoardAddr[1],
-                                            switchStatus[idx]
+                                            switchStatus[idx],
+                                            changed = true
                                         )
                                     )
                                 }
@@ -619,6 +623,7 @@ class DockBean(
     class SwitchBean(
         idx: Int,
         val switchBoardAddr: Byte,
-        var enabled: Boolean
+        var enabled: Boolean,
+        var changed: Boolean = false
     ) : DeviceBean(DEVICE_TYPE_SWITCH, idx, true)
 }

+ 6 - 1
app/src/main/java/com/grkj/iscs_mars/modbus/ModBusController.kt

@@ -1227,7 +1227,12 @@ object ModBusController {
     fun getSwitchData(): MutableList<DockBean.SwitchBean> {
         return dockList.filter { it.type == DOCK_TYPE_COLLECT }.sortedBy { it.addr }
             .flatMap { it.getSwitchList() }.mapIndexed { index, switchBean ->
-                DockBean.SwitchBean(index, switchBean.switchBoardAddr, switchBean.enabled)
+                DockBean.SwitchBean(
+                    index,
+                    switchBean.switchBoardAddr,
+                    switchBean.enabled,
+                    switchBean.changed
+                )
             }.toMutableList()
     }
 

+ 5 - 0
app/src/main/java/com/grkj/iscs_mars/model/UrlConsts.kt

@@ -362,4 +362,9 @@ object UrlConsts {
      * 检查我是否需要连接钥匙
      */
     const val GET_MY_SELF_STATE = "/iscs/hardware-api/getMySelfState"
+
+    /**
+     * 获取地图列表
+     */
+    const val GET_MAP_PAGE = "/iscs/switchmap/getIsLotoSwitchMapPage"
 }

+ 24 - 0
app/src/main/java/com/grkj/iscs_mars/model/vo/map/LotoSwitchMapPageRespVO.kt

@@ -0,0 +1,24 @@
+package com.grkj.iscs_mars.model.vo.map
+
+data class LotoSwitchMapPageRespVO(
+    val current: Int,
+    val optimizeCountSql: Boolean,
+    val orders: List<Any>,
+    val pages: Int,
+    val records: List<Record>,
+    val searchCount: Boolean,
+    val size: Int,
+    val total: Int
+) {
+    data class Record(
+        val switchMapId: Long?,
+
+        val mapId: String?,
+
+        val mapName: String?,
+
+        val switchMapName: String?,
+
+        val workstationId: Long?,
+    )
+}

+ 6 - 10
app/src/main/java/com/grkj/iscs_mars/util/MapLayerExt.kt

@@ -16,22 +16,18 @@ import com.onlylemi.mapview.library.layer.MapBaseLayer
  * - 新层默认插入到索引 0(最底层),保持:底图 → 业务层 → 叠加层 的绘制顺序。
  */
 fun MapView.replaceLayer(tag: String, newLayer: MapBaseLayer) {
-    // 为新层打 tag,方便后续查找
     newLayer.tag = tag
 
-    // 1. 查找并移除同 tag 的旧层
-    val itr = layers.iterator()
-    while (itr.hasNext()) {
-        val layer = itr.next()
-        if (layer.tag == tag) {
-            itr.remove()
-            break
-        }
+    // 1. 移除已有同 tag 的旧层
+    val old = layers.firstOrNull { it.tag == tag }
+    if (old != null) {
+        layers.remove(old)
     }
 
-    // 2. 插入最底层(index 0)。如需不同顺序可自行调整索引。
+    // 2. 插入最底层
     layers.add(0, newLayer)
 
     // 3. 请求重绘
     postInvalidate()
 }
+

+ 19 - 0
app/src/main/java/com/grkj/iscs_mars/util/NetApi.kt

@@ -32,6 +32,7 @@ import com.grkj.iscs_mars.model.vo.lock.LockTakeUpdateReqVO
 import com.grkj.iscs_mars.model.vo.machinery.IsLotoStationPageRespVO
 import com.grkj.iscs_mars.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs_mars.model.vo.machinery.MachineryPageRespVO
+import com.grkj.iscs_mars.model.vo.map.LotoSwitchMapPageRespVO
 import com.grkj.iscs_mars.model.vo.map.MapInfoRespVO
 import com.grkj.iscs_mars.model.vo.map.MapPointPageRespVO
 import com.grkj.iscs_mars.model.vo.sop.SopInfoRespVO
@@ -1566,4 +1567,22 @@ object NetApi {
             }, isGet = true, isAuth = true
         )
     }
+
+    /**
+     * 获取地图列表
+     */
+    fun getMapPage(callBack: (LotoSwitchMapPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_MAP_PAGE,
+            false,
+            mapOf("current" to 1, "size" to -1),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
 }

+ 37 - 19
app/src/main/java/com/grkj/iscs_mars/view/activity/SwitchStatusActivity.kt

@@ -3,22 +3,22 @@ package com.grkj.iscs_mars.view.activity
 import android.view.GestureDetector
 import android.view.Gravity
 import android.view.MotionEvent
+import android.widget.LinearLayout.HORIZONTAL
 import androidx.core.view.isVisible
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
-import com.drake.brv.utils.divider
+import com.drake.brv.utils.dividerSpace
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.databinding.ActivitySwitchStatusBinding
+import com.grkj.iscs_mars.databinding.ItemMapBinding
 import com.grkj.iscs_mars.databinding.ItemSwitchBinding
 import com.grkj.iscs_mars.modbus.ModBusController
 import com.grkj.iscs_mars.model.eventmsg.MsgEventConstants.MSG_EVENT_SWITCH_COLLECTION_UPDATE
+import com.grkj.iscs_mars.model.vo.map.LotoSwitchMapPageRespVO
 import com.grkj.iscs_mars.model.vo.map.MapInfoRespVO.IsMapPoint
 import com.grkj.iscs_mars.util.CommonUtils
 import com.grkj.iscs_mars.util.ToastUtils
@@ -30,16 +30,15 @@ import com.grkj.iscs_mars.view.presenter.SwitchStatusPresenter
 import com.grkj.iscs_mars.view.widget.CustomSwitchStationLayer
 import com.onlylemi.mapview.library.MapViewListener
 import com.sik.sikcore.extension.setDebouncedClickListener
-import com.sik.sikcore.extension.toJson
 import com.sik.sikcore.thread.ThreadUtils
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 
 class SwitchStatusActivity :
     BaseMvpActivity<ISwitchStatusView, SwitchStatusPresenter, ActivitySwitchStatusBinding>() {
     private var stationLayer: CustomSwitchStationLayer? = null
     private lateinit var gestureDetector: GestureDetector
     private lateinit var switchInfoDialog: SwitchInfoDialog
+    private var currentSwitchMapId = 0L
+    private var currentMapId = ""
 
     override fun initPresenter(): SwitchStatusPresenter {
         return SwitchStatusPresenter()
@@ -70,15 +69,27 @@ class SwitchStatusActivity :
             }
         }
         mBinding?.mapview?.setBackgroundColorInt(getColor(R.color.color_map_base))
-        mBinding?.rvList?.linear()?.divider {
-            orientation = DividerOrientation.HORIZONTAL
-            setColorRes(R.color.common_bg_black_30)
-        }?.setup {
+        mBinding?.rvList?.linear()?.dividerSpace(10, DividerOrientation.GRID)?.setup {
             addType<IsMapPoint>(R.layout.item_switch)
             onBind {
                 onRVListBinding()
             }
         }
+        mBinding?.mapRv?.linear(HORIZONTAL)?.dividerSpace(10, DividerOrientation.GRID)?.setup {
+            addType<LotoSwitchMapPageRespVO.Record>(R.layout.item_map)
+            onBind {
+                val item = getModel<LotoSwitchMapPageRespVO.Record>()
+                val itemBinding = getBinding<ItemMapBinding>()
+                itemBinding.mapName.text = item.switchMapName
+                itemBinding.mapName.isSelected = item.switchMapId == currentSwitchMapId
+                itemBinding.mapName.setDebouncedClickListener {
+                    getMap(item.mapId.toString())
+                    currentSwitchMapId = item.switchMapId ?: 0
+                    currentMapId = item.mapId.toString()
+                    adapter.notifyDataSetChanged()
+                }
+            }
+        }
         initMap()
     }
 
@@ -87,8 +98,7 @@ class SwitchStatusActivity :
         val item = getModel<IsMapPoint>()
         val switchData = ModBusController.getSwitchData()
         itemBinding.switchName.text = item.entityName
-        itemBinding.switchId.text = context.getString(R.string.switch_id, item.pointSerialNumber)
-        LogUtil.i("开关数据:${switchData.toJson()}")
+        itemBinding.switchId.text = context.getString(R.string.switch_id, item.pointNfc)
         val switchStatus = switchData
             .find { it.idx == item.pointSerialNumber?.toInt() }?.enabled
             ?: (item.switchStatus == "1")
@@ -109,7 +119,7 @@ class SwitchStatusActivity :
             }
         }
         itemBinding.root.setDebouncedClickListener {
-            stationLayer?.selectPoint(item.pointSerialNumber.toString())
+            stationLayer?.selectPoint(item.entityId)
         }
     }
 
@@ -118,16 +128,26 @@ class SwitchStatusActivity :
         BusinessManager.mEventBus.observe(this) {
             when (it.code) {
                 MSG_EVENT_SWITCH_COLLECTION_UPDATE -> {
+                    getMap(currentMapId)
                     mBinding?.rvList?.adapter?.notifyDataSetChanged()
                 }
             }
         }
-        presenter?.getMapData {
-            getMap(it)
+        presenter?.getMapPage {
+            if (it?.records?.isNotEmpty() == true) {
+                currentSwitchMapId = it.records[0].switchMapId ?: 0
+                currentMapId = it.records[0].mapId.toString()
+                LogUtil.i("地图数据:${it.records}")
+                mBinding?.mapRv?.models = it.records
+                getMap(it.records[0].mapId.toString())
+            }
         }
     }
 
     private fun getMap(mapId: String) {
+        if (mapId.isEmpty()) {
+            return
+        }
         presenter?.getMapInfo(mapId.toLong()) { itMapInfo ->
             mBinding?.rvList?.models = itMapInfo?.pointList
             ThreadUtils.runOnIO {
@@ -167,15 +187,13 @@ class SwitchStatusActivity :
                     stationLayer?.onLongPressListener = { point, screenX, screenY, _, _ ->
                         switchInfoDialog.setSwitchInfo(
                             point.entityName,
-                            "${point.pointSerialNumber}",
+                            "${point.pointNfc}",
                             point.status
                         )
                         switchInfoDialog.showPopupWindow(screenX.toInt(), screenY.toInt())
                     }
                     mBinding?.mapview?.addLayer(stationLayer)
                     stationLayer?.setRatio(presenter?.mapRatio ?: 1f)
-                    stationLayer?.stopAnimation()
-                    stationLayer?.startAnimation()
                     mBinding?.mapview?.refresh()
                 }
             }

+ 2 - 2
app/src/main/java/com/grkj/iscs_mars/view/dialog/SwitchInfoDialog.kt

@@ -26,9 +26,9 @@ class SwitchInfoDialog(context: Context) : BasePopupWindow(context) {
         binding = DialogSwitchInfoBinding.bind(contentView)
     }
 
-    fun setSwitchInfo(switchName: String, switchId: String, switchStatus: Int) {
+    fun setSwitchInfo(switchName: String, pointNfc: String, switchStatus: Int) {
         binding.switchName.text = switchName
-        binding.switchId.text = context.getString(R.string.switch_id,switchId)
+        binding.switchId.text = context.getString(R.string.switch_id,pointNfc)
         when (switchStatus) {
             0 -> {
                 binding.switchStatus.setBackgroundResource(R.drawable.bg_switch_off)

+ 133 - 66
app/src/main/java/com/grkj/iscs_mars/view/presenter/SwitchStatusPresenter.kt

@@ -10,6 +10,7 @@ import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.extentions.serialNo
 import com.grkj.iscs_mars.model.SimData
 import com.grkj.iscs_mars.model.vo.BaseVO
+import com.grkj.iscs_mars.model.vo.map.LotoSwitchMapPageRespVO
 import com.grkj.iscs_mars.model.vo.map.MapInfoRespVO
 import com.grkj.iscs_mars.util.BitmapUtil
 import com.grkj.iscs_mars.util.Executor
@@ -21,8 +22,18 @@ import com.grkj.iscs_mars.view.iview.ISwitchStatusView
 import com.grkj.iscs_mars.view.widget.CustomSwitchStationLayer
 import com.grkj.iscs_mars.view.widget.RegionTileLayer
 import com.onlylemi.mapview.library.MapView
+import java.io.File
 
 class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
+    /**
+     * 最后的地图连接地址
+     */
+    private var lastMapUrl: String = ""
+    private var needReloadMap: Boolean = false
+    private var lastActualW: Int = 0
+    private var lastActualH: Int = 0
+    private var imgFile: File? = null
+
     /**
      * 地图缩放
      */
@@ -37,13 +48,6 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
      * 地图id
      */
     fun getMapInfo(mapId: Long, callBack: (MapInfoRespVO?) -> Unit) {
-//        callBack(
-//            Gson().fromJson<BaseVO<MapInfoRespVO>>(
-//                SimData.mapData,
-//                object : TypeToken<BaseVO<MapInfoRespVO>>() {}.type
-//            ).data
-//        )
-//        return
         NetApi.getMapInfo(mapId) {
             Executor.runOnMain {
                 callBack(it)
@@ -52,91 +56,154 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
     }
 
     /**
-     * 处理地图大图 + 点位
+     * 处理地图大图 + 点位(后端点位为“格子左上角”,前端以“中心锚点”绘制)
      *
      * @param context      上下文
-     * @param itMapInfo    后端返回的地图信息
-     * @param mapViewRef   MapView 的弱引用 —— 仅用来塞 RegionTileLayer(避免循环依赖,你也可直接传 MapView
-     * @param stationLayer 点位层(用于写入 IsolationPoint 列表)
+     * @param itMapInfo    后端返回的地图信息(含大图宽高/子图偏移/点位等)
+     * @param mapViewRef   MapView 的提供函数(避免循环依赖,需要时再取
+     * @param stationLayer 点位层(用于写入 IsolationPoint 列表 & 局部刷新
      * @param onPreview    首屏低清预览回调(Bitmap)
+     * @param cellPx       后端“格子”的像素大小(与后端约定,默认 50f)
+     * @param yAxisFlip    后端若以左下为 (0,0) 则需要翻转 Y;通常是左上为 (0,0) → false
      */
     suspend fun mapDataHandle(
         context: Context,
         itMapInfo: MapInfoRespVO?,
-        mapViewRef: () -> MapView?,                     // <-- 自己决定怎么拿 MapView
+        mapViewRef: () -> MapView?,
         stationLayer: CustomSwitchStationLayer?,
+        cellPx: Float = 50f,
+        yAxisFlip: Boolean = false,
         onPreview: (Bitmap) -> Unit
     ) {
-        val imageUrl = itMapInfo?.imageUrl ?: return          // 没图 URL 直接溜了
+        val imageUrl = itMapInfo?.imageUrl ?: run {
+            LogUtil.e("Map: empty imageUrl"); return
+        }
+        if (lastMapUrl != imageUrl) {
+            lastMapUrl = imageUrl
+            needReloadMap = true
+        }
+        if (needReloadMap) {
+            /* --- 1) 下载原图文件(不解码) --- */
+            val tempImageFile = BitmapUtil.downloadToFile(context, imageUrl) ?: run {
+                LogUtil.e("Map download failed → $imageUrl")
+                return
+            }
+            imgFile = tempImageFile
+            imgFile?.let {
+                /* --- 2) 首屏快速预览(低清) --- */
+                BitmapUtil.decodePreview(
+                    it,
+                    backgroundColor = context.getColor(R.color.color_map_base)
+                ).also(onPreview)
+                val size = BitmapUtil.readImageSize(it)
+                lastActualW = size.first
+                lastActualH = size.second
+            }
+        }
 
-        /* --- 1. 下载文件,不解码 --- */
-        val imgFile = BitmapUtil.downloadToFile(context, imageUrl) ?: run {
-            LogUtil.e("Map download failed → $imageUrl"); return
+        /* --- 3) 读取原图实际尺寸,计算缩放比(XY 分离) --- */
+        val backendW = itMapInfo.width?.toFloat()
+        val backendH = itMapInfo.height?.toFloat()
+        if (backendW == null || backendH == null || backendW <= 0f || backendH <= 0f) {
+            LogUtil.e("Map: backend width/height invalid → $backendW x $backendH")
+            return
         }
+        val ratioX = lastActualW / backendW
+        val ratioY = lastActualH / backendH
+        // 若你在别处仍使用 mapRatio,保留各自独立更安全
+        mapRatio = ratioX
 
-        /* --- 2. 快速首屏预览 --- */
-        BitmapUtil.decodePreview(imgFile, backgroundColor = context.getColor(R.color.color_map_base)).also(onPreview)     // 预览 Bitmap 交给 UI
+        /* --- 4) 计算点位:后端给的是格左上角 → 我们改为“格中心”坐标,再同步到实际位图坐标 --- */
+        val offX = (itMapInfo.x ?: "0").toFloat()
+        val offY = (itMapInfo.y ?: "0").toFloat()
 
-        /* --- 3. 拿原图尺寸,算缩放 --- */
-        val (actualW, actualH) = BitmapUtil.readImageSize(imgFile)
-        val backendW = itMapInfo.width!!.toFloat()
-        val backendH = itMapInfo.height!!.toFloat()
-        val ratioX = actualW / backendW
-        val ratioY = actualH / backendH
-        mapRatio = ratioX
+        // 用局部 list 构建,避免并发修改
+        val points = mutableListOf<CustomSwitchStationLayer.IsolationPoint>()
+
+        // 方便异步 icon 回填,避免 “mStationList.last()” 竞态:用 id → point 的索引
+        val byId = mutableMapOf<Long, CustomSwitchStationLayer.IsolationPoint>()
 
-        /* --- 4. 生成 IsolationPoint 数据 --- */
-        mStationList.clear()
-        val cellPx = 50f
-        val offX = itMapInfo.x!!.toFloat()
-        val offY = itMapInfo.y!!.toFloat()
         itMapInfo.pointList
-            ?.filter { it.x != null && it.y != null }
+            ?.asSequence()
+            ?.filter { it.x != null && it.y != null && it.entityId != null }
             ?.forEach { pt ->
-                val gX = cellPx.times(pt.x!!.toFloat())           // 格子 → 全局像素
-                val gY = cellPx.times(pt.y!!.toFloat())
-                val lX = gX - offX                // 减去子图偏移
-                val lY = gY - offY
-                val fX = lX * ratioX              // 按实际 Bitmap 比例缩放
-                val fY = lY * ratioY
-
-                mStationList += CustomSwitchStationLayer.IsolationPoint(
-                    PointF(fX, fY),
-                    pt.entityName.orEmpty(),
-                    null,             // 小图标后面异步填充
-                    pt.entityId!!.toLong(),
-                    pt.pointSerialNumber,
-                    false
+                // 1) 后端格坐标 → 全局像素(取格中心:+0.5f)
+                val gridX = (pt.x!!.toFloat() + 0.5f) * cellPx
+                val gridY = (pt.y!!.toFloat() + 0.5f) * cellPx
+
+                // 2) 去掉子图偏移(后端通常给“子图左上在大图中的偏移”)
+                var localX = gridX - offX
+                var localY = gridY - offY
+
+                // 3) 如果后端以左下为原点,需要翻转 Y(可按需启用)
+                if (yAxisFlip) {
+                    // 以子图高度为基准翻转;这里使用 backendH 的子区高度,若有“子图高”字段可替换
+                    localY = (backendH - localY)
+                }
+
+                // 4) 缩放到真实位图坐标(注意 X / Y 独立比例)
+                val fX = localX * ratioX
+                val fY = localY * ratioY
+
+                val switchStatus = if (pt.switchStatus == "1")
+                    CustomSwitchStationLayer.STATUS_ON
+                else
+                    CustomSwitchStationLayer.STATUS_OFF
+
+                val p = CustomSwitchStationLayer.IsolationPoint(
+                    pos = PointF(fX, fY),               // 约定:这里保存“中心点”坐标
+                    entityName = pt.entityName.orEmpty(),
+                    icon = null,                        // icon 异步回填
+                    entityId = pt.entityId!!,
+                    pointSerialNumber = pt.pointSerialNumber,
+                    isSelected = false,
+                    pointNfc = pt.pointNfc ?: "",
+                    status = switchStatus
                 )
+                points += p
+                byId[p.entityId] = p
 
-                /* ---- 小图标异步加载,可选 ----*/
-                BitmapUtil.loadBitmapSmall(
-                    ctx      = context,
-                    url      = pt.pointIcon,
-                    reqW     = 64,
-                    reqH     = 64
-                ) { bmp ->
-                    mStationList.last().icon = bmp
-                    stationLayer?.refreshIfVisible(PointF(fX, fY))
+                // 5) 异步加载小图标(拿到后刷新该点附近,避免整层重绘开销)
+                val url = pt.pointIcon
+                if (!url.isNullOrBlank()) {
+                    BitmapUtil.loadBitmapSmall(
+                        ctx = context,
+                        url = url,
+                        reqW = 64,   // 可按需调
+                        reqH = 64
+                    ) { bmp ->
+                        val target = byId[p.entityId] ?: return@loadBitmapSmall
+                        target.icon = bmp
+                        // pos 为中心锚点,绘制时以中心绘制;只需局部刷新
+                        stationLayer?.refreshIfVisible(target.pos)
+                    }
                 }
             }
 
-        /* --- 5. 把计算好的列表塞给点位层 --- */
-        stationLayer?.submitPoints(mStationList)               // 你自己的 API
+        /* --- 5) 把计算好的列表交给点位层 --- */
+        stationLayer?.submitPoints(points)
 
-        /* --- 6. 用 RegionTileLayer 替换旧底图 --- */
-        mapViewRef()?.let { mv ->
-            val tileLayer = RegionTileLayer(mv, imgFile)       // 512px tile, 内部 LruCache
-            mv.replaceLayer("bigMap", tileLayer)               // MapView 扩展函数
+        /* --- 6) 用 RegionTileLayer 替换旧底图(内部自带 LruCache / 512 tile) --- */
+        if (needReloadMap) {
+            mapViewRef()?.let { mv ->
+                imgFile?.let {
+                    val tileLayer = RegionTileLayer(mv, it)
+                    mv.replaceLayer("bigMap", tileLayer)
+                }
+            }
         }
+        needReloadMap = false
     }
 
-    fun getMapData(done: (String) -> Unit) {
-        NetApi.getIsLotoStationPage {
-            done(
-                it?.records?.find { it.lotoSerialNumber == MyApplication.instance?.applicationContext!!.serialNo() }?.mapId
-                    ?: "4"
-            )
+
+    /**
+     * 获取地图列表
+     */
+    fun getMapPage(callBack: (LotoSwitchMapPageRespVO?) -> Unit) {
+        NetApi.getMapPage {
+            Executor.runOnMain {
+                callBack(it)
+            }
         }
     }
 }

+ 0 - 1
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomStationLayer.kt

@@ -78,7 +78,6 @@ class CustomStationLayer @JvmOverloads constructor(
             try {
                 // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
                 canvas.concat(currentMatrix)
-                val switchData = ModBusController.getSwitchData()
                 val tempPointList = pointList.toList()
                 val inv = Matrix().apply { currentMatrix.invert(this) }
                 val screen = android.graphics.RectF(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat())

+ 423 - 381
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomSwitchStationLayer.kt

@@ -13,80 +13,134 @@ import com.grkj.iscs_mars.MyApplication
 import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.modbus.DockBean
 import com.grkj.iscs_mars.modbus.ModBusController
+import com.grkj.iscs_mars.util.log.LogUtil
 import com.onlylemi.mapview.library.MapView
 import com.onlylemi.mapview.library.layer.MapBaseLayer
 import com.sik.sikcore.SIKCore
-import com.sik.sikcore.thread.ThreadUtils
 import kotlin.math.abs
 import kotlin.math.sin
 
 /**
- * 自定义开关层级
+ * 自定义开关层级(按点位缓存状态版本,变了才刷新)
  */
 class CustomSwitchStationLayer @JvmOverloads constructor(
-    mapView: MapView?, private var stationList: MutableList<IsolationPoint> = mutableListOf()
+    mapView: MapView?,
+    private var stationList: MutableList<IsolationPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
 
+    // ===== 数据结构 =====
+    data class IsolationPoint(
+        var pos: PointF,                // 可能是中心 or 左上角
+        var entityName: String,
+        var icon: Bitmap?,
+        val entityId: Long,
+        var pointSerialNumber: String?, // 仅用于和 switch.idx 对表
+        var isSelected: Boolean,
+        var pointNfc: String?,
+        var status: Int = STATUS_UNKNOWN // 见常量
+    )
+
+    companion object {
+        const val STATUS_OFF = 0
+        const val STATUS_ON = 1
+        const val STATUS_ALARM = 2
+        const val STATUS_UNKNOWN = 3
+    }
+
+    // ===== 配置开关:你的 pos 是否已经是中心点? =====
+    private val POS_IS_CENTER = true // 若上游 pos 就是中心,设 true;若是左上角,改 false
+
+    // ===== 同步&选择 =====
+    private val dataLock = Any()
+
     @Volatile
-    var inDraw: Boolean = false
-        private set
+    private var selectedEntityIdForKeep: Long? = null
 
-    // ---- 动画参数 ----
-    private val breathePeriod = 1200f       // ALARM 脉冲周期(ms)
-    private val FRAME_INTERVAL = 32L        // ~30fps
-    private var pulsePhase: Float = 0f      // 0..1 脉冲相位
+    @Volatile
+    private var selectedNameForKeep: String? = null
 
-    // —— 动画时长配置 ——
-    private val longPressCenterDurationMs = 280L
-    private val clickZoomDurationMs = 220L
-    private val clickCenterDurationMs = 220L
+    // ===== switch 数据快照(仅存原始列表,不在 draw 里查表)=====
+    @Volatile
+    private var hasAlarmFlag: Boolean = false
 
-    // 点击后希望的目标缩放(你想要多大就设多少)
-    private val desiredSelectScale = 2.0f
+    // token 防过期回调
+    @Volatile
+    private var selectToken: Long = 0L
+    private inline fun withLiveToken(token: Long, block: () -> Unit) {
+        if (token == selectToken) block()
+    }
+
+    // ===== 动画参数 =====
+    @Volatile
+    var inDraw: Boolean = false
+        private set
 
-    // 旧的 alpha 给 ON 态呼吸用的;现在只给 ALARM 脉冲使用
-    private var alpha = 255
+    private val breathePeriod = 1200f
+    private var pulsePhase: Float = 0f
+
+    private val FRAME_INTERVAL_MS = 32L
+    private var nextInvalidateScheduled = false
+    private val invalidateRunnable = Runnable {
+        nextInvalidateScheduled = false
+        mapView?.refresh()
+    }
 
     private val refreshRunnable: Runnable = object : Runnable {
         override fun run() {
             val now = SystemClock.uptimeMillis()
-            val phase = ((now % breathePeriod).toFloat()) / breathePeriod
-            pulsePhase = phase
-            alpha = (((sin(phase * 2 * Math.PI) + 1) / 2f) * (255 - 50) + 50).toInt()
-
-            val points = synchronized(stationList) { stationList.toList() }
-            val switches = runCatching { ModBusController.getSwitchData() }.getOrNull()
-
-            if (hasAnyAlarm(points, switches)) {
-                mapView?.refresh()
-            } else {
-                mapView?.refresh()
-            }
-            mapView?.postDelayed(this, FRAME_INTERVAL) // 🔑 用 this 引用自己
+            pulsePhase = ((now % breathePeriod).toFloat()) / breathePeriod
+            throttleInvalidate()
+            mapView?.postDelayed(this, FRAME_INTERVAL_MS)
         }
     }
 
-    private lateinit var paint: Paint
-    private var currentZoom = 0f
-    private var currentDegree = 0f
+    private val longPressCenterDurationMs = 280L
+    private val clickZoomDurationMs = 220L
+    private val clickCenterDurationMs = 220L
+    private val desiredSelectScale = 2.0f
+
+    // 尺寸
     private var ratio: Float = 1f
+    private val BASE_SWITCH_SIZE = 18f
+    private val BASE_TEXT_SIZE = 14f
+    private var switchSize: Float = BASE_SWITCH_SIZE
+    private var textSize: Float = BASE_TEXT_SIZE
 
-    // 尺寸:注意你把 switchSize 当作「半径」来使用
-    private var switchSize: Float = 2f
-    private var textSize: Float = 1f
-    private var lastKnownZoom: Float = 1f
+    // 绘制状态
+    private lateinit var paint: Paint
+    private var currentZoom = 1f
+    private var currentDegree = 0f
+    private val TEXT_VISIBLE_ZOOM_THRESHOLD = 1f
+
+    // 颜色
+    private val appCtx get() = MyApplication.instance!!.applicationContext
+    private val colOn by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_enable) }
+    private val colOff by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_disable) }
+    private val colRed by lazy {
+        runCatching {
+            ContextCompat.getColor(
+                appCtx,
+                R.color.red_500
+            )
+        }.getOrDefault(Color.parseColor("#EF4444"))
+    }
+    private val colOrange by lazy {
+        runCatching {
+            ContextCompat.getColor(
+                appCtx,
+                R.color.orange_500
+            )
+        }.getOrDefault(Color.parseColor("#F97316"))
+    }
+    private val colSelectRing = Color.argb(180, 66, 133, 244)
 
     // 长按探测
     private val longPressTimeoutMs: Long =
         android.view.ViewConfiguration.getLongPressTimeout().toLong()
-
-    // 建议更宽容一些(x1.5):
-    private val touchSlop: Float =
-        android.view.ViewConfiguration.get(
-            mapView?.context ?: SIKCore.getApplication()
-        ).scaledTouchSlop * 1.5f
+    private val touchSlop: Float = android.view.ViewConfiguration.get(
+        mapView?.context ?: SIKCore.getApplication()
+    ).scaledTouchSlop * 1.5f
     private val touchSlopSq: Float = touchSlop * touchSlop
-
     private var pendingLongPress = false
     private var longPressTriggered = false
     private var downScreenX = 0f
@@ -98,149 +152,235 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private val longPressRunnable = Runnable {
         if (pendingLongPress && currentPressed != null) {
             longPressTriggered = true
-            pendingLongPress = false  // ✅ 关键:触发后不再允许被 MOVE 取消
-
+            pendingLongPress = false
             val p = currentPressed!!
-            val centerMapX = p.pos.x + switchSize / 2f
-            val centerMapY = p.pos.y + switchSize / 2f
-
+            val c = centerOf(p)
             mapView?.postDelayed({
-                val screen = mapToScreen(centerMapX, centerMapY)
-                onLongPressListener?.invoke(
-                    p,
-                    screen.x,
-                    screen.y - switchSize / 2 - 4,
-                    centerMapX,
-                    centerMapY
-                )
+                val screen = mapToScreen(c.x, c.y)
+                onLongPressListener?.invoke(p, screen.x, screen.y - switchSize / 2 - 4, c.x, c.y)
             }, longPressCenterDurationMs)
         }
     }
 
-
-    /** 长按回调:点、屏幕坐标(已居中)、图内坐标(中心点) */
-    var onLongPressListener: ((
-        point: IsolationPoint,
-        screenX: Float, screenY: Float,
-        mapX: Float, mapY: Float
-    ) -> Unit)? = null
-
+    var onLongPressListener: ((point: IsolationPoint, screenX: Float, screenY: Float, mapX: Float, mapY: Float) -> Unit)? =
+        null
 
     init {
-        initLayer()
+        paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE }
+        setRatio(1f)
+        startAnimation()
     }
 
-    private fun initLayer() {
-        paint = Paint()
-        paint.isAntiAlias = true
-        paint.style = Paint.Style.FILL_AND_STROKE
-    }
+    // ================= 外部接口 =================
+
+    /** 点位列表:深拷贝 pos;可选保留当前选中(按 entityId→name) */
+    // ==== 精准 Diff 的 submitPoints ====
+    fun submitPoints(points: List<IsolationPoint>, keepSelection: Boolean = true) {
+        // 快照避免外部列表后续被改动
+        val incoming = if (points === stationList) points.map { deepCopyItem(it) } else points
+
+        val keepEid = if (keepSelection) selectedEntityIdForKeep else null
+        val keepName = if (keepSelection) selectedNameForKeep else null
+
+        // 记录变化以做局部刷新
+        val changedCenters = ArrayList<PointF>(8)
+        val changedRadiusPx = 72 // 以图标 ~64px 给点冗余
+
+        synchronized(dataLock) {
+            // 1) 建索引(现有 & 新数据)
+            val oldById = HashMap<Long, IsolationPoint>(stationList.size)
+            stationList.forEach { oldById[it.entityId] = it }
+
+            val seenIds = HashSet<Long>(incoming.size)
+            val nextList = ArrayList<IsolationPoint>(incoming.size)
+
+            // 2) 逐项处理:更新/新增
+            for (src in incoming) {
+                val id = src.entityId
+                seenIds.add(id)
+                val dst = oldById[id]
+
+                if (dst == null) {
+                    // 新增:深拷贝一次,避免后续被外部改动影响
+                    val add = deepCopyItem(src).apply {
+                        isSelected = when {
+                            !keepSelection -> false
+                            keepEid != null -> entityId == keepEid
+                            keepName != null -> entityName == keepName
+                            else -> false
+                        }
+                    }
+                    nextList.add(add)
+                    changedCenters.add(add.pos)
+                } else {
+                    // 更新:字段级 copy,尽量复用对象引用,避免动画/缓存抖动
+                    var dirty = false
 
-    /**
-     * Presenter 计算好所有点后调用此方法;线程安全,可任何线程调用。
-     */
-    fun submitPoints(points: List<IsolationPoint>) {
-        synchronized(stationList) {
+                    if (!dst.pos.approxEq(src.pos)) {
+                        dst.pos = PointF(src.pos.x, src.pos.y) // 新对象,避免外部持有同一引用
+                        dirty = true
+                    }
+                    if (dst.status != src.status) {
+                        dst.status = src.status; dirty = true
+                    }
+                    if (dst.entityName != src.entityName) {
+                        dst.entityName = src.entityName; dirty = true
+                    }
+                    if (dst.pointNfc != src.pointNfc) {
+                        dst.pointNfc = src.pointNfc; dirty = true
+                    }
+                    if (dst.pointSerialNumber != src.pointSerialNumber) {
+                        dst.pointSerialNumber = src.pointSerialNumber; dirty = true
+                    }
+                    // icon:如果外部异步回填,这里一般不动;若你也带了新 icon:
+                    if (src.icon != null && dst.icon !== src.icon) {
+                        dst.icon = src.icon; dirty = true
+                    }
+
+                    // 选中保持 or 重置
+                    dst.isSelected = when {
+                        !keepSelection -> false
+                        keepEid != null -> dst.entityId == keepEid
+                        keepName != null -> dst.entityName == keepName
+                        else -> false
+                    }
+
+                    if (dirty) changedCenters.add(dst.pos)
+                    nextList.add(dst) // 复用原对象引用
+                }
+            }
+
+            // 3) 删除:老数据里有、新数据里没有的
+            if (seenIds.size != oldById.size) {
+                // 直接按 nextList(新顺序)重建 stationList 更简单干净
+                // 被删除的对象引用不再放入 nextList 即可
+            }
+
+            // 4) 最小代价重建列表(保持新顺序,但复用对象)
             stationList.clear()
-            stationList.addAll(points)
+            stationList.addAll(nextList)
+        }
+
+        // 5) 局部刷新(尽可能小范围)
+        if (changedCenters.isNotEmpty()) {
+            refreshRegionsOrThrottle(changedCenters, changedRadiusPx)
+        } else {
+            // 没有变化也触发一次轻微节流刷新,避免偶发不同步
+            throttleInvalidate()
         }
-        mapView.postInvalidate()
     }
 
-    /**
-     * 若给定点位在屏幕可视区域内,则执行一次重绘;否则忽略。
-     */
-    fun refreshIfVisible(point: PointF, margin: Float = 0f) {
-        if (inDraw) return
-        val w = mapView.width
-        val h = mapView.height
-        if (w == 0 || h == 0) return
+// ==== 辅助方法 ====
 
-        val pts = floatArrayOf(point.x, point.y)
-        mapView.currentMatrix.mapPoints(pts)
-        val x = pts[0]
-        val y = pts[1]
-        if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
-            mapView.postInvalidate()
-        }
+    // PointF 近似比较,避免浮点误差导致的伪变更
+    private fun PointF.approxEq(other: PointF, eps: Float = 0.1f): Boolean {
+        return kotlin.math.abs(x - other.x) <= eps && kotlin.math.abs(y - other.y) <= eps
     }
 
     /**
-     * 全局比例设置(叠乘版)
-     * 如果不想叠乘(反复调用越来越大/小),改为基准写法:
-     *   switchSize = setValue(BASE_SWITCH_SIZE * ratio)
-     *   textSize = switchSize
+     * 优先做局部刷新;如果你的 MapView 没有暴露 invalidateRect,就回退到节流整层刷新。
+     * 这里假设 mapView/invalidateRect 可用;否则把反射去掉,直接 throttleInvalidate()
      */
-    fun setRatio(ratio: Float) {
-        this.ratio = ratio
-        switchSize = setValue(4 * ratio * switchSize)
-        textSize = switchSize * 0.8f
-    }
+    private fun refreshRegionsOrThrottle(centers: List<PointF>, radiusPx: Int) {
+        val mv = mapView ?: return throttleInvalidate()
+
+        var didPartial = false
+        runCatching {
+            val m = mv.javaClass.getMethod(
+                "invalidateRect", Int::class.java, Int::class.java, Int::class.java, Int::class.java
+            )
+            for (c in centers) {
+                val l = (c.x - radiusPx).toInt()
+                val t = (c.y - radiusPx).toInt()
+                val r = (c.x + radiusPx).toInt()
+                val b = (c.y + radiusPx).toInt()
+                m.invoke(mv, l, t, r, b)
+            }
+            didPartial = true
+        }.onFailure {
+            // ignore
+        }
 
-    fun startAnimation() {
-        mapView.removeCallbacks(refreshRunnable)
-        mapView.postDelayed(refreshRunnable, FRAME_INTERVAL)
+        if (!didPartial) throttleInvalidate()
     }
 
-    fun stopAnimation() {
-        mapView.removeCallbacks(refreshRunnable)
-    }
 
-    // —— 坐标互转工具 ——
-    private fun screenToMap(x: Float, y: Float): PointF? {
-        val inv = Matrix()
-        val m = Matrix(mapView.currentMatrix)
-        if (!m.invert(inv)) return null
-        val pt = floatArrayOf(x, y)
-        inv.mapPoints(pt)
-        return PointF(pt[0], pt[1])
+    /** 比例(基准) */
+    fun setRatio(ratio: Float) {
+        this.ratio = ratio
+        switchSize = BASE_SWITCH_SIZE * ratio
+        textSize = BASE_TEXT_SIZE * ratio
+        throttleInvalidate()
     }
 
-    private fun mapToScreen(mx: Float, my: Float): PointF {
-        val m = Matrix(mapView.currentMatrix)
-        val pt = floatArrayOf(mx, my)
-        m.mapPoints(pt)
-        return PointF(pt[0], pt[1])
+    /** 按唯一 name 选中并定位(若 name 唯一) */
+    fun selectPoint(
+        entityId: Long?,
+        animated: Boolean = true,
+        durationMs: Long = 300L,
+        scale: Float = 2f
+    ): Boolean {
+        if (entityId == null) return false
+        val snapshot = synchronized(dataLock) { stationList.toList() }
+        val target = snapshot.firstOrNull { it.entityId == entityId } ?: return false
+        val token = ++selectToken
+
+        val eid = target.entityId
+        val name = target.entityName
+        synchronized(dataLock) {
+            stationList.forEach {
+                it.isSelected = it.entityId == entityId
+            }
+        }
+        selectedEntityIdForKeep = eid
+        selectedNameForKeep = name
+        throttleInvalidate()
+
+        val c = centerOf(target)
+        mapView?.let { mv ->
+            if (animated) mv.animateCenterOnPoint(c.x, c.y, durationMs, scale)
+            else mv.mapCenterWithPoint(c.x, c.y)
+        }
+        return true
     }
 
-    /** 取“当前屏幕中心”所对应的图内坐标,用来实现只缩放不平移 */
-    private fun currentMapCenter(): PointF? {
+    /** 局部刷新(点在屏幕上时) */
+    fun refreshIfVisible(point: PointF, margin: Float = 0f) {
+        if (inDraw) return
         val w = mapView.width
         val h = mapView.height
-        if (w == 0 || h == 0) return null
-        return screenToMap(w * 0.5f, h * 0.5f)
+        if (w == 0 || h == 0) return
+        val pts = floatArrayOf(point.x, point.y)
+        mapView.currentMatrix.mapPoints(pts)
+        val x = pts[0];
+        val y = pts[1]
+        if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
+            throttleInvalidate()
+        }
     }
 
-
+    // ================= 触摸 =================
     override fun onTouch(event: MotionEvent) {
         when (event.actionMasked) {
             MotionEvent.ACTION_DOWN -> {
-                downScreenX = event.x
-                downScreenY = event.y
-                lastScreenX = event.x
-                lastScreenY = event.y
-
+                downScreenX = event.x; downScreenY = event.y
+                lastScreenX = event.x; lastScreenY = event.y
                 longPressTriggered = false
                 pendingLongPress = false
                 currentPressed = null
-
-                val mapPt = screenToMap(event.x, event.y)
-                if (mapPt != null) {
-                    val hit = hitTest(mapPt.x, mapPt.y)
-                    if (hit != null) {
+                screenToMap(event.x, event.y)?.let { mp ->
+                    hitTest(mp.x, mp.y)?.let { hit ->
                         currentPressed = hit
                         pendingLongPress = true
-                        mapView.parent?.requestDisallowInterceptTouchEvent(true) // ✅ 防父布局拦截
+                        mapView.parent?.requestDisallowInterceptTouchEvent(true)
                         mapView.removeCallbacks(longPressRunnable)
                         mapView.postDelayed(longPressRunnable, longPressTimeoutMs)
                     }
-
                 }
             }
 
             MotionEvent.ACTION_MOVE -> {
-                lastScreenX = event.x
-                lastScreenY = event.y
+                lastScreenX = event.x; lastScreenY = event.y
                 if (pendingLongPress && !longPressTriggered) {
                     val dx = event.x - downScreenX
                     val dy = event.y - downScreenY
@@ -252,10 +392,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
             }
 
             MotionEvent.ACTION_UP -> {
-                // 取消长按
                 mapView.removeCallbacks(longPressRunnable)
-
-                // 如果刚才已触发长按,吞掉点击
                 if (longPressTriggered) {
                     pendingLongPress = false
                     longPressTriggered = false
@@ -263,44 +400,39 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     return
                 }
 
-                // —— 点击:先放大,再居中,再标记(保证最终居中)——
-                val mapPt = screenToMap(event.x, event.y) ?: return
-                val hit = hitTest(mapPt.x, mapPt.y) ?: return
+                val mp = screenToMap(event.x, event.y) ?: return
+                val hit = hitTest(mp.x, mp.y) ?: return
+                val token = ++selectToken
 
                 val targetScale = desiredSelectScale
-                val targetMapX = hit.pos.x + switchSize / 2f
-                val targetMapY = hit.pos.y + switchSize / 2f
-
-                // ① 只缩放:以当前屏幕中心为 pivot,把 scale 动到 targetScale
-                val curCenter = currentMapCenter()
-                if (curCenter != null) {
-                    mapView?.animateCenterOnPoint(
-                        curCenter.x,
-                        curCenter.y,
-                        clickZoomDurationMs,
-                        targetScale
-                    )
-                } else {
-                    // 万一拿不到中心,就直接在下一步里统一做
-                }
+                val targetCenter = centerOf(hit)
+                val eid = hit.entityId
+                val name = hit.entityName
 
-                // ② 再居中到目标点(保持 targetScale),确保最后居中
+                // ① 先拉缩放(以当前中心为 pivot)
+                currentMapCenter()?.let { cur ->
+                    mapView?.animateCenterOnPoint(cur.x, cur.y, clickZoomDurationMs, targetScale)
+                }
+                // ② 再居中到目标(保持 targetScale)
                 mapView?.postDelayed({
-                    mapView?.animateCenterOnPoint(
-                        targetMapX,
-                        targetMapY,
-                        clickCenterDurationMs,
-                        targetScale
-                    )
+                    withLiveToken(token) {
+                        mapView?.animateCenterOnPoint(
+                            targetCenter.x,
+                            targetCenter.y,
+                            clickCenterDurationMs,
+                            targetScale
+                        )
+                    }
                 }, clickZoomDurationMs)
-
-                // ③ 动画完成后再 selectPoint(此时位置稳定)
+                // ③ 完成后只选中目标
                 mapView?.postDelayed({
-                    hit.pointSerialNumber?.let { serial ->
-                        // 这里 selectPoint 内部再调用 animate 的 scale/center 就会乱,所以用“保持现状”:
-                        // 直接标记 + 刷新,不再让 selectPoint 改缩放/中心(你可以加个参数控制)
-                        stationList.replaceAll { it.copy(isSelected = (it.pointSerialNumber == serial)) }
-                        mapView?.postInvalidate()
+                    withLiveToken(token) {
+                        synchronized(dataLock) {
+                            stationList.replaceAll { it.copy(isSelected = (it.entityId == eid) || (it.entityName == name)) }
+                        }
+                        selectedEntityIdForKeep = eid
+                        selectedNameForKeep = name
+                        throttleInvalidate()
                     }
                 }, clickZoomDurationMs + clickCenterDurationMs)
             }
@@ -314,176 +446,94 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         }
     }
 
-
-    private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
-        val hitR = switchSize * 1.2f
-        val hitR2 = hitR * hitR
-        val snapshot = synchronized(stationList) { stationList.toList() }
-        var hit: IsolationPoint? = null
-        var bestD2 = Float.MAX_VALUE
-        for (p in snapshot) {
-            val dx = mapX - p.pos.x
-            val dy = mapY - p.pos.y
-            val d2 = dx * dx + dy * dy
-            if (d2 <= hitR2 && d2 < bestD2) {
-                bestD2 = d2
-                hit = p
-            }
-        }
-        return hit
-    }
-
-
+    // ================= 绘制 =================
     override fun draw(
-        canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
+        canvas: Canvas,
+        currentMatrix: Matrix,
+        currentZoom: Float,
+        currentRotateDegrees: Float
     ) {
         if (!isVisible) return
         if (inDraw) return
         inDraw = true
 
         this.currentZoom = currentZoom
-        lastKnownZoom = currentZoom
-        currentDegree = 360 - currentRotateDegrees
+        this.currentDegree = 360 - currentRotateDegrees
 
         try {
             canvas.save()
             try {
-                // 合并地图矩阵
                 canvas.concat(currentMatrix)
 
-                val switchData = ModBusController.getSwitchData()
-                val tempPointList = synchronized(stationList) { stationList.toList() }
+                val points = synchronized(dataLock) { stationList.toList() }
+                val switches = runCatching { ModBusController.getSwitchData() }.getOrNull()
+                for (p in points) {
+                    val c = centerOf(p)
 
-                // 如果存在 ALARM,则确保动画驱动起来
-                if (hasAnyAlarm(tempPointList, switchData)) {
-                    startAnimation()
-                }
-
-                tempPointList.forEach { point ->
-                    val bean = switchData.find { it.idx == point.pointSerialNumber?.toInt() }
-                    val status = parseStatus(bean)
-
-                    val x = point.pos.x
-                    val y = point.pos.y
-
-                    // ---- 选中态:蓝色外环(想要回红框,把这段改回去即可)----
-                    if (point.isSelected) {
+                    // 选中外环
+                    if (p.isSelected) {
                         paint.style = Paint.Style.STROKE
                         paint.strokeWidth = 4f
-                        paint.color = Color.argb(180, 66, 133, 244) // #4285F4 带透明
-                        canvas.drawCircle(
-                            x + switchSize / 2,
-                            y + switchSize / 2,
-                            switchSize * 1.5f,
-                            paint
-                        )
+                        paint.color = colSelectRing
+                        canvas.drawCircle(c.x, c.y, switchSize * 1.5f, paint)
                     }
 
-                    // ---- 本体圆:按状态绘制 ----
+                    // 主体:直接用 p.status(由 setSwitchData 决定)
                     paint.style = Paint.Style.FILL_AND_STROKE
+                    var status =
+                        p.pointSerialNumber?.let { parseStatus(switches?.find { it.idx.toString() == p.pointSerialNumber }) }
+                            ?: p.status
+                    if (p.entityName=="E-45"){
+                        status = STATUS_ALARM
+                    }
+//                    val status = Random.nextInt(3)
                     when (status) {
-                        SwitchStatus.ON -> {
-                            stationList.firstOrNull { it.entityId == point.entityId }?.status = 1
-                            paint.color = ContextCompat.getColor(
-                                MyApplication.instance!!.applicationContext,
-                                R.color.common_switch_enable
-                            )
-                            paint.alpha = 255
-                            canvas.drawCircle(
-                                x + switchSize / 2,
-                                y + switchSize / 2,
-                                switchSize,
-                                paint
-                            )
+                        STATUS_ON -> {
+                            paint.color = colOn; paint.alpha = 255
+                            canvas.drawCircle(c.x, c.y, switchSize, paint)
                         }
 
-                        SwitchStatus.OFF -> {
-                            stationList.firstOrNull { it.entityId == point.entityId }?.status = 0
-                            paint.color = ContextCompat.getColor(
-                                MyApplication.instance!!.applicationContext,
-                                R.color.common_switch_disable
-                            )
-                            paint.alpha = 255
-                            canvas.drawCircle(
-                                x + switchSize / 2,
-                                y + switchSize / 2,
-                                switchSize,
-                                paint
-                            )
+                        STATUS_OFF -> {
+                            paint.color = colOff; paint.alpha = 255
+                            canvas.drawCircle(c.x, c.y, switchSize, paint)
                         }
 
-                        SwitchStatus.ALARM -> {
-                            stationList.firstOrNull { it.entityId == point.entityId }?.status = 2
-                            // 脉冲:颜色红/橙跳变 + alpha 呼吸 + 半径轻微伸缩 + 外光晕
+                        STATUS_ALARM -> {
                             val t = pulsePhase
-                            val useRed = t < 0.5f
-                            val color = if (useRed) {
-                                // 如果你没有这些资源色,可以换成 Color.RED / 自定义颜色
-                                runCatching {
-                                    ContextCompat.getColor(
-                                        MyApplication.instance!!.applicationContext,
-                                        R.color.red_500
-                                    )
-                                }.getOrDefault(Color.parseColor("#EF4444"))
-                            } else {
-                                runCatching {
-                                    ContextCompat.getColor(
-                                        MyApplication.instance!!.applicationContext,
-                                        R.color.orange_500
-                                    )
-                                }.getOrDefault(Color.parseColor("#F97316"))
-                            }
-
-                            val alphaPulse =
-                                (160 + 95 * abs(sin(2f * Math.PI.toFloat() * t))).toInt()
-                            paint.color = color
-                            paint.alpha = alphaPulse
-
+                            val color = if (t < 0.5f) colRed else colOrange
+                            val a = (160 + 95 * abs(sin(2f * Math.PI.toFloat() * t))).toInt()
+                            paint.color = color; paint.alpha = a
                             val r = switchSize * (1.0f + 0.10f * sin(2f * Math.PI.toFloat() * t))
-                            canvas.drawCircle(
-                                x + switchSize / 2,
-                                y + switchSize / 2,
-                                r,
-                                paint
-                            )
-
-                            paint.alpha = (alphaPulse * 0.35f).toInt()
-                            canvas.drawCircle(
-                                x + switchSize / 2,
-                                y + switchSize / 2,
-                                r * 1.25f,
-                                paint
-                            )
-
+                            canvas.drawCircle(c.x, c.y, r, paint)
+                            paint.alpha = (a * 0.35f).toInt()
+                            canvas.drawCircle(c.x, c.y, r * 1.25f, paint)
                             paint.alpha = 255
                         }
 
-                        SwitchStatus.UNKNOWN -> {
-                            // 未知:深灰
-                            paint.color = Color.DKGRAY
-                            paint.alpha = 200
-                            canvas.drawCircle(
-                                x + switchSize / 2,
-                                y + switchSize / 2,
-                                switchSize,
-                                paint
-                            )
+                        else -> { // STATUS_UNKNOWN
+                            paint.color = Color.DKGRAY; paint.alpha = 200
+                            canvas.drawCircle(c.x, c.y, switchSize, paint)
                             paint.alpha = 255
                         }
                     }
 
-                    // ---- 文本(和你原来一致)----
-                    paint.style = Paint.Style.FILL
-                    paint.strokeWidth = 1f
-                    paint.color = Color.WHITE
-                    paint.textSize = textSize
-                    val text = point.pointSerialNumber ?: ""
-                    val textW = paint.measureText(text)
-                    val fm = paint.fontMetrics
-                    val textH = fm.bottom - fm.top
-                    val textX = x + (switchSize - textW) / 2
-                    val textY = y + switchSize / 2 + (textH / 2 - fm.bottom)
-                    canvas.drawText(text, textX, textY, paint)
+                    // 文本
+                    if (currentZoom >= TEXT_VISIBLE_ZOOM_THRESHOLD) {
+                        paint.style = Paint.Style.FILL
+                        paint.strokeWidth = 1f
+                        paint.color = Color.WHITE
+                        paint.textSize = textSize
+                        val text = p.entityName
+                        val textW = paint.measureText(text)
+                        val fm = paint.fontMetrics
+                        val textH = fm.bottom - fm.top
+                        canvas.drawText(
+                            text,
+                            c.x - textW / 2f,
+                            c.y + (textH / 2 - fm.bottom),
+                            paint
+                        )
+                    }
                 }
             } finally {
                 canvas.restore()
@@ -493,89 +543,81 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         }
     }
 
-    /**
-     * 根据序号查找点位的「图内坐标」。
-     */
-    fun getPointPosition(serial: String): PointF? {
-        return stationList.find { it.pointSerialNumber == serial }?.pos
+    // ================= 工具 =================
+    private fun centerOf(p: IsolationPoint): PointF {
+        return if (POS_IS_CENTER) PointF(p.pos.x, p.pos.y)
+        else PointF(p.pos.x + switchSize / 2f, p.pos.y + switchSize / 2f)
     }
 
-    /**
-     * 选中一个点位:标记 isSelected 并定位到视图中心。
-     */
-    fun selectPoint(
-        serial: String,
-        animated: Boolean = true,
-        durationMs: Long = 300L,
-        scale: Float = 2f
-    ): Boolean {
-        val point = stationList.find { it.pointSerialNumber == serial } ?: return false
-        point.isSelected = true
-        ThreadUtils.runOnMainDelayed(3000) {
-            point.isSelected = false
-            stationList.replaceAll { it.copy(isSelected = false) }
-            mapView?.postInvalidate()
-        }
-        stationList.replaceAll { it.copy(isSelected = (it.pointSerialNumber == serial)) }
-        mapView?.postInvalidate()
+    private fun screenToMap(x: Float, y: Float): PointF? {
+        val inv = Matrix()
+        val m = Matrix(mapView.currentMatrix)
+        if (!m.invert(inv)) return null
+        val pt = floatArrayOf(x, y)
+        inv.mapPoints(pt)
+        return PointF(pt[0], pt[1])
+    }
 
-        mapView?.let { mv ->
-            if (animated) {
-                mv.animateCenterOnPoint(point.pos.x, point.pos.y, durationMs, scale)
-            } else {
-                mv.mapCenterWithPoint(point.pos.x, point.pos.y)
-            }
-        }
-        return true
+    private fun mapToScreen(mx: Float, my: Float): PointF {
+        val m = Matrix(mapView.currentMatrix)
+        val pt = floatArrayOf(mx, my)
+        m.mapPoints(pt)
+        return PointF(pt[0], pt[1])
     }
 
-    // ====== 状态解析 & 动画触发判定 ======
+    private fun currentMapCenter(): PointF? {
+        val w = mapView.width
+        val h = mapView.height
+        if (w == 0 || h == 0) return null
+        return screenToMap(w * 0.5f, h * 0.5f)
+    }
 
-    /** 预留:设备状态 */
-    private enum class SwitchStatus { ON, OFF, ALARM, UNKNOWN }
+    private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
+        val hitR = switchSize * 1.2f
+        val hitR2 = hitR * hitR
+        val snapshot = synchronized(dataLock) { stationList.toList() }
+        var hit: IsolationPoint? = null
+        var bestD2 = Float.MAX_VALUE
+        for (p in snapshot) {
+            val c = centerOf(p)
+            val dx = mapX - c.x
+            val dy = mapY - c.y
+            val d2 = dx * dx + dy * dy
+            if (d2 <= hitR2 && d2 < bestD2) {
+                bestD2 = d2
+                hit = p
+            }
+        }
+        return hit
+    }
 
-    /**
-     * 统一解析设备状态:未来 SwitchBean 加字段时,只改这里。
-     * 现在:优先看 isAlarm(bean),否则用 enabled 判定;bean==null 记为 UNKNOWN。
-     */
-    private fun parseStatus(bean: DockBean.SwitchBean?): SwitchStatus {
-        if (bean == null) return SwitchStatus.UNKNOWN
-        if (isAlarm(bean)) return SwitchStatus.ALARM
-        return if (bean.enabled) SwitchStatus.ON else SwitchStatus.OFF
+    private fun parseStatus(bean: DockBean.SwitchBean?): Int {
+        if (bean == null) return STATUS_UNKNOWN
+        if (isAlarm(bean)) return STATUS_ALARM
+        return if (bean.enabled) STATUS_ON else STATUS_OFF
     }
 
-    /**
-     * 是否告警(预留钩子):将来你在 SwitchBean 里加 type/statusCode 等,改这里即可。
-     * 当前默认:不告警。
-     */
+    /** 告警判断:按你业务改 */
     private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
-        // TODO: 未来形如:
-        // return bean?.type == 2 || bean?.statusCode == ALARM
+        // TODO: return bean?.type == 2 || bean?.statusCode == ALARM
         return false
     }
 
-    /** 是否存在任意 ALARM 点位,用于决定是否跑动画帧 */
-    private fun hasAnyAlarm(
-        points: List<IsolationPoint>,
-        switches: List<DockBean.SwitchBean>?
-    ): Boolean {
-        if (switches == null) return false
-        for (p in points) {
-            val b = switches.find { it.idx == p.pointSerialNumber?.toInt() }
-            if (isAlarm(b)) return true
-        }
-        return false
+    private fun throttleInvalidate() {
+        if (nextInvalidateScheduled) return
+        nextInvalidateScheduled = true
+        mapView?.postDelayed(invalidateRunnable, FRAME_INTERVAL_MS)
     }
 
-    // ====== 数据结构(保持你的原样)======
+    private fun startAnimation() {
+        mapView.removeCallbacks(refreshRunnable)
+        mapView.postDelayed(refreshRunnable, FRAME_INTERVAL_MS)
+    }
 
-    data class IsolationPoint(
-        val pos: PointF,
-        val entityName: String,
-        var icon: Bitmap?,
-        val entityId: Long,
-        val pointSerialNumber: String?,
-        var isSelected: Boolean,
-        var status: Int = 0
-    )
+    private fun stopAnimation() {
+        mapView.removeCallbacks(refreshRunnable)
+    }
+
+    private fun deepCopyPoint(p: PointF) = PointF(p.x, p.y)
+    private fun deepCopyItem(it: IsolationPoint) = it.copy(pos = deepCopyPoint(it.pos))
 }

+ 64 - 59
app/src/main/java/com/onlylemi/mapview/library/MapView.java

@@ -1,5 +1,6 @@
 package com.onlylemi.mapview.library;
 
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -8,11 +9,10 @@ import android.graphics.Matrix;
 import android.graphics.Picture;
 import android.graphics.PointF;
 import android.graphics.SurfaceTexture;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.TextureView;
-import android.animation.ValueAnimator;
-import android.graphics.Matrix;
 import android.view.animation.AccelerateDecelerateInterpolator;
 
 import com.grkj.iscs_mars.util.log.LogUtil;
@@ -21,18 +21,16 @@ import com.onlylemi.mapview.library.layer.MapLayer;
 import com.onlylemi.mapview.library.utils.MapMath;
 import com.onlylemi.mapview.library.utils.MapUtils;
 
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 
-/**
- * Java版:TextureView替代SurfaceView的MapView,实现独立画布
- * 修复:支持在任意缩放级别下既能放大也能缩小。
- */
 public class MapView extends TextureView implements TextureView.SurfaceTextureListener {
 
     private SurfaceTexture surface;
     private boolean isMapLoadFinish = false;
-    private final List<MapBaseLayer> layers = new ArrayList<>();
+
+    /** 用 COWList 避免 refresh() 遍历时被并发修改 */
+    private final CopyOnWriteArrayList<MapBaseLayer> layers = new CopyOnWriteArrayList<>();
     private MapLayer mapLayer;
 
     private final Matrix saveMatrix = new Matrix();
@@ -78,7 +76,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
 
     public void setBackgroundColorInt(int color) {
         this.backgroundColor = color;
-        refresh(); // 设置后刷新
+        refresh();
     }
 
     private void init() {
@@ -94,8 +92,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     }
 
     @Override
-    public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) {
-    }
+    public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) { }
 
     @Override
     public boolean onSurfaceTextureDestroyed(SurfaceTexture st) {
@@ -104,8 +101,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     }
 
     @Override
-    public void onSurfaceTextureUpdated(SurfaceTexture st) {
-    }
+    public void onSurfaceTextureUpdated(SurfaceTexture st) { }
 
     public void loadMap(Bitmap bitmap) {
         Bitmap safe = ensureSafeBitmapSize(bitmap);
@@ -115,30 +111,27 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
 
     public void loadMap(final Picture picture) {
         isMapLoadFinish = false;
-        new Thread(new Runnable() {
-            @Override
-            public void run() {
-                if (picture != null) {
+        new Thread(() -> {
+            if (picture != null) {
+                // 在后台线程准备好 picture,真正操作 UI / layers 放到主线程
+                post(() -> {
                     if (mapLayer == null) {
                         mapLayer = new MapLayer(MapView.this);
-                        layers.add(mapLayer);
+                        layers.add(0, mapLayer); // 确保底层在最底
+                    } else if (!layers.contains(mapLayer)) {
+                        layers.add(0, mapLayer);
                     }
                     mapLayer.setImage(picture);
                     if (mapViewListener != null) {
                         mapViewListener.onMapLoadSuccess();
                     }
                     isMapLoadFinish = true;
-                    post(new Runnable() {
-                        @Override
-                        public void run() {
-                            refresh();
-                        }
-                    });
-                } else {
-                    if (mapViewListener != null) {
-                        mapViewListener.onMapLoadFail();
-                    }
-                }
+                    refresh();
+                });
+            } else {
+                post(() -> {
+                    if (mapViewListener != null) mapViewListener.onMapLoadFail();
+                });
             }
         }).start();
     }
@@ -157,13 +150,16 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         if (surface == null || !isMapLoadFinish) return;
         Canvas canvas = lockCanvas();
         if (canvas != null) {
-            canvas.drawColor(backgroundColor);
-            for (MapBaseLayer layer : layers) {
-                if (layer.isVisible) {
-                    layer.draw(canvas, currentMatrix, currentZoom, currentRotateDegrees);
+            try {
+                canvas.drawColor(backgroundColor);
+                for (MapBaseLayer layer : layers) {
+                    if (layer.isVisible) {
+                        layer.draw(canvas, currentMatrix, currentZoom, currentRotateDegrees);
+                    }
                 }
+            } finally {
+                unlockCanvasAndPost(canvas);
             }
-            unlockCanvasAndPost(canvas);
         }
     }
 
@@ -206,7 +202,6 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                     currentMatrix.postTranslate(event.getX() - startTouch.x,
                             event.getY() - startTouch.y);
                 } else if (currentTouchState == TOUCH_STATE_TWO_POINTED) {
-                    // 始终按缩放处理,避免旋转误判让缩小失效
                     float newDist = distance(event, mid);
                     float scale = newDist / oldDist;
                     if (scale * saveZoom < minZoom) {
@@ -245,8 +240,37 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         return isMapLoadFinish;
     }
 
-    public void addLayer(MapBaseLayer layer) {
-        layers.add(layer);
+    /** 在主线程添加图层;避免并发改动引发 CME */
+    public void addLayer(final MapBaseLayer layer) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            post(() -> addLayer(layer));
+            return;
+        }
+        if (layer != null && !layers.contains(layer)) {
+            layers.add(layer);
+            refresh();
+        }
+    }
+
+    /** 在主线程移除图层 */
+    public void removeLayer(final MapBaseLayer layer) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            post(() -> removeLayer(layer));
+            return;
+        }
+        if (layer != null && layers.remove(layer)) {
+            refresh();
+        }
+    }
+
+    /** 清空所有图层(主线程) */
+    public void clearLayers() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            post(this::clearLayers);
+            return;
+        }
+        layers.clear();
+        refresh();
     }
 
     public List<MapBaseLayer> getLayers() {
@@ -353,66 +377,47 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         return mapLayer != null ? mapLayer.getImage().getHeight() : 0f;
     }
 
-    /**
-     * 瞬间把地图上的 (x, y) 点移动到屏幕中心。
-     */
+    /** 瞬间把地图上的 (x, y) 点移动到屏幕中心。 */
     public void centerOnPoint(float x, float y) {
         mapCenterWithPoint(x, y);
     }
 
-    /**
-     * 瞬间把地图上的 (x, y) 点移动到屏幕中心,并切换到指定缩放级别。
-     * 注意:zoom 要在 minZoom~maxZoom 范围内。
-     */
+    /** 瞬间中心并缩放到 zoom(zoom 需在 min~max 之间) */
     public void centerAndZoom(float x, float y, float zoom) {
         setCurrentZoom(zoom);
         mapCenterWithPoint(x, y);
     }
 
     /**
-     * 平滑动画地把地图上的 (x, y) 点移动到屏幕中心。
-     *
-     * @param x        地图坐标 x
-     * @param y        地图坐标 y
-     * @param duration 动画时长(ms),建议 200~500
-     * @param scale    缩放等级
+     * 平滑动画地把地图上的 (x, y) 点移动到屏幕中心,并缩放到 scale。
      */
     public void animateCenterOnPoint(final float x, final float y, long duration, float scale) {
-        // 0. 容错
         if (duration <= 0) duration = 300;
         if (currentZoom <= 0f) currentZoom = 1f;
 
-        // 1. 记录初始状态
         final float startZoom = currentZoom;
         final float targetZoom = scale;
         final Matrix startMatrix = new Matrix(currentMatrix);
 
-        // 2. 动画驱动:每帧先缩放,再根据当帧矩阵计算把点居中需要的平移
         ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
         animator.setDuration(duration);
         animator.setInterpolator(new AccelerateDecelerateInterpolator());
         animator.addUpdateListener(animation -> {
             float frac = (float) animation.getAnimatedValue();
-            // 本帧的缩放值(线性或可替换为更合适的插值)
             float currentScale = startZoom + (targetZoom - startZoom) * frac;
 
-            // 用起始矩阵为基准重建当帧矩阵
             currentMatrix.set(startMatrix);
 
-            // 2.1 围绕“世界坐标 (x,y)”进行缩放
             float scaleFactor = currentScale / startZoom;
             currentMatrix.postScale(scaleFactor, scaleFactor, x, y);
 
-            // 2.2 计算此时 (x,y) 在屏幕坐标的位置
             float[] pts = {x, y};
             currentMatrix.mapPoints(pts);
 
-            // 2.3 计算把该点移到屏幕中心所需的平移,并直接应用
             float dx = getWidth()  / 2f - pts[0];
             float dy = getHeight() / 2f - pts[1];
             currentMatrix.postTranslate(dx, dy);
 
-            // 3. 更新状态并重绘
             currentZoom = currentScale;
             refresh();
         });

+ 5 - 0
app/src/main/res/color/top_tab_text_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"  android:color="#FFFFFFFF"/>
+    <item android:color="#A8B3CF"/>
+</selector>

+ 15 - 0
app/src/main/res/drawable/top_tab_bg_selector.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/main_color"/>
+            <corners android:radius="8dp"/>
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/transparent"/>
+            <corners android:radius="8dp"/>
+        </shape>
+    </item>
+</selector>

+ 9 - 4
app/src/main/res/layout/activity_switch_status.xml

@@ -26,17 +26,16 @@
             android:layout_height="match_parent"
             android:layout_gravity="right">
 
-
             <LinearLayout
                 android:id="@+id/point_list"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_marginLeft="12dp"
                 android:background="@drawable/common_layout_bg"
+                android:minWidth="200dp"
                 android:orientation="vertical"
                 android:padding="@dimen/common_spacing"
-                android:minWidth="200dp"
-                android:visibility="gone"
+                android:visibility="visible"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="@id/cb_show"
@@ -52,12 +51,18 @@
                     app:btn_bg="@drawable/common_btn_blue_bg"
                     app:btn_name="@string/back" />
 
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/map_rv"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/common_spacing"/>
+
                 <TextView
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_gravity="center_horizontal"
-                    android:gravity="center"
                     android:layout_marginVertical="@dimen/common_spacing"
+                    android:gravity="center"
                     android:text="@string/switch_information"
                     android:textColor="@color/white"
                     android:textSize="@dimen/common_text_size"

+ 20 - 0
app/src/main/res/layout/item_map.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:id="@+id/map_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/top_tab_bg_selector"
+            android:gravity="center"
+            android:paddingHorizontal="@dimen/common_spacing"
+            android:paddingVertical="@dimen/common_spacing_small"
+            android:textColor="@color/top_tab_text_selector"
+            android:textSize="@dimen/common_text_size" />
+    </FrameLayout>
+
+</layout>

+ 3 - 1
app/src/main/res/layout/item_switch.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <LinearLayout
         android:id="@+id/root"
@@ -10,6 +11,7 @@
         android:divider="@drawable/divider_vertical_small"
         android:orientation="vertical"
         android:padding="@dimen/common_spacing_small"
+        app:rippleColor="?attr/colorPrimary"
         android:showDividers="middle">
 
         <TextView