|
|
@@ -7,17 +7,17 @@ import android.graphics.Matrix
|
|
|
import android.graphics.Paint
|
|
|
import android.graphics.PointF
|
|
|
import android.os.SystemClock
|
|
|
-import android.util.Pair
|
|
|
import android.view.MotionEvent
|
|
|
import androidx.core.content.ContextCompat
|
|
|
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.BitmapUtil
|
|
|
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.cos
|
|
|
+import kotlin.math.abs
|
|
|
import kotlin.math.sin
|
|
|
|
|
|
/**
|
|
|
@@ -26,34 +26,98 @@ import kotlin.math.sin
|
|
|
class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
mapView: MapView?, private var stationList: MutableList<IsolationPoint> = mutableListOf()
|
|
|
) : MapBaseLayer(mapView) {
|
|
|
+
|
|
|
@Volatile
|
|
|
var inDraw: Boolean = false
|
|
|
private set
|
|
|
|
|
|
- // 呼吸灯周期(毫秒)
|
|
|
- private val breathePeriod = 1200f
|
|
|
- private val FRAME_INTERVAL = 32L // 约 30fps
|
|
|
+ // ---- 动画参数 ----
|
|
|
+ private val breathePeriod = 1200f // ALARM 脉冲周期(ms)
|
|
|
+ private val FRAME_INTERVAL = 32L // ~30fps
|
|
|
+ private var pulsePhase: Float = 0f // 0..1 脉冲相位
|
|
|
+
|
|
|
+ // —— 动画时长配置 ——
|
|
|
+ private val longPressCenterDurationMs = 280L
|
|
|
+ private val clickZoomDurationMs = 220L
|
|
|
+ private val clickCenterDurationMs = 220L
|
|
|
+
|
|
|
+ // 点击后希望的目标缩放(你想要多大就设多少)
|
|
|
+ private val desiredSelectScale = 2.0f
|
|
|
+
|
|
|
+ // 旧的 alpha 给 ON 态呼吸用的;现在只给 ALARM 脉冲使用
|
|
|
private var alpha = 255
|
|
|
- private val refreshRunnable = Runnable {
|
|
|
- // 2. uptimeMillis 保证单调递增
|
|
|
- val now = SystemClock.uptimeMillis()
|
|
|
- // 3. 先在 Long 上做模,再转 Float 计算 phase
|
|
|
- val phase = ((now % breathePeriod).toFloat()) / breathePeriod
|
|
|
- val normalized = ((sin(phase * 2 * Math.PI) + 1) / 2).toFloat()
|
|
|
- alpha = (normalized * (255 - 50) + 50).toInt()
|
|
|
- mapView?.refresh()
|
|
|
- mapView?.post {
|
|
|
- startAnimation()
|
|
|
+
|
|
|
+ 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()
|
|
|
+ mapView?.postDelayed(this, FRAME_INTERVAL) // 🔑 用 this 引用自己
|
|
|
+ } else {
|
|
|
+ mapView?.refresh()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
private lateinit var paint: Paint
|
|
|
private var currentZoom = 0f
|
|
|
private var currentDegree = 0f
|
|
|
private var ratio: Float = 1f
|
|
|
- private var switchSize: Float = 1f
|
|
|
+
|
|
|
+ // 尺寸:注意你把 switchSize 当作「半径」来使用
|
|
|
+ private var switchSize: Float = 2f
|
|
|
private var textSize: Float = 1f
|
|
|
private var lastKnownZoom: Float = 1f
|
|
|
|
|
|
+ // 长按探测
|
|
|
+ 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 touchSlopSq: Float = touchSlop * touchSlop
|
|
|
+
|
|
|
+ private var pendingLongPress = false
|
|
|
+ private var longPressTriggered = false
|
|
|
+ private var downScreenX = 0f
|
|
|
+ private var downScreenY = 0f
|
|
|
+ private var lastScreenX = 0f
|
|
|
+ private var lastScreenY = 0f
|
|
|
+ private var currentPressed: IsolationPoint? = null
|
|
|
+
|
|
|
+ private val longPressRunnable = Runnable {
|
|
|
+ if (pendingLongPress && currentPressed != null) {
|
|
|
+ longPressTriggered = true
|
|
|
+ pendingLongPress = false // ✅ 关键:触发后不再允许被 MOVE 取消
|
|
|
+
|
|
|
+ val p = currentPressed!!
|
|
|
+ val centerMapX = p.pos.x + switchSize / 2f
|
|
|
+ val centerMapY = p.pos.y + switchSize / 2f
|
|
|
+
|
|
|
+ mapView?.animateCenterOnPoint(centerMapX, centerMapY, longPressCenterDurationMs, lastKnownZoom)
|
|
|
+ mapView?.postDelayed({
|
|
|
+ val screen = mapToScreen(centerMapX, centerMapY)
|
|
|
+ onLongPressListener?.invoke(p, screen.x, screen.y, centerMapX, centerMapY)
|
|
|
+ }, longPressCenterDurationMs)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /** 长按回调:点、屏幕坐标(已居中)、图内坐标(中心点) */
|
|
|
+ var onLongPressListener: ((
|
|
|
+ point: IsolationPoint,
|
|
|
+ screenX: Float, screenY: Float,
|
|
|
+ mapX: Float, mapY: Float
|
|
|
+ ) -> Unit)? = null
|
|
|
+
|
|
|
+
|
|
|
init {
|
|
|
initLayer()
|
|
|
}
|
|
|
@@ -72,69 +136,170 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
stationList.clear()
|
|
|
stationList.addAll(points)
|
|
|
}
|
|
|
- mapView.postInvalidate() // 触发重绘
|
|
|
+ mapView.postInvalidate()
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 若给定点位在屏幕可视区域内,则执行一次重绘;否则忽略。
|
|
|
- * 可在异步加载小图标完成后调用,避免整图层频繁刷新。
|
|
|
*/
|
|
|
fun refreshIfVisible(point: PointF, margin: Float = 0f) {
|
|
|
- // 如果当前仍在 draw() 里,直接返回,等那一帧完成即可
|
|
|
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;如项目中名称不同请自行调整
|
|
|
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
|
|
|
- ) {
|
|
|
+ if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
|
|
|
mapView.postInvalidate()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 全局比例设置(叠乘版)
|
|
|
+ * 如果不想叠乘(反复调用越来越大/小),改为基准写法:
|
|
|
+ * switchSize = setValue(BASE_SWITCH_SIZE * ratio)
|
|
|
+ * textSize = switchSize
|
|
|
+ */
|
|
|
fun setRatio(ratio: Float) {
|
|
|
this.ratio = ratio
|
|
|
- switchSize = setValue(4 * ratio)
|
|
|
- textSize = switchSize
|
|
|
+ switchSize = setValue(4 * ratio * switchSize)
|
|
|
+ textSize = switchSize * 0.8f
|
|
|
}
|
|
|
|
|
|
fun startAnimation() {
|
|
|
- // 先干掉前一次没执行的
|
|
|
mapView.removeCallbacks(refreshRunnable)
|
|
|
- // 延后 16ms 再刷新,自动合并一堆连续的调用
|
|
|
mapView.postDelayed(refreshRunnable, FRAME_INTERVAL)
|
|
|
}
|
|
|
|
|
|
fun stopAnimation() {
|
|
|
- // 先干掉前一次没执行的
|
|
|
mapView.removeCallbacks(refreshRunnable)
|
|
|
}
|
|
|
|
|
|
- override fun onTouch(event: MotionEvent) {
|
|
|
- if (event.action != MotionEvent.ACTION_UP) return
|
|
|
-
|
|
|
- // 屏幕坐标 → 图内坐标(和 draw() 的 concat 对应)
|
|
|
+ // —— 坐标互转工具 ——
|
|
|
+ private fun screenToMap(x: Float, y: Float): PointF? {
|
|
|
val inv = Matrix()
|
|
|
val m = Matrix(mapView.currentMatrix)
|
|
|
- if (!m.invert(inv)) return
|
|
|
- val pt = floatArrayOf(event.x, event.y)
|
|
|
+ if (!m.invert(inv)) return null
|
|
|
+ val pt = floatArrayOf(x, y)
|
|
|
inv.mapPoints(pt)
|
|
|
- val mapX = pt[0];
|
|
|
- val mapY = pt[1]
|
|
|
+ return PointF(pt[0], pt[1])
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ override fun onTouch(event: MotionEvent) {
|
|
|
+ when (event.actionMasked) {
|
|
|
+ MotionEvent.ACTION_DOWN -> {
|
|
|
+ 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) {
|
|
|
+ currentPressed = hit
|
|
|
+ pendingLongPress = true
|
|
|
+ mapView.parent?.requestDisallowInterceptTouchEvent(true) // ✅ 防父布局拦截
|
|
|
+ mapView.removeCallbacks(longPressRunnable)
|
|
|
+ mapView.postDelayed(longPressRunnable, longPressTimeoutMs)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ MotionEvent.ACTION_MOVE -> {
|
|
|
+ lastScreenX = event.x
|
|
|
+ lastScreenY = event.y
|
|
|
+ if (pendingLongPress && !longPressTriggered) {
|
|
|
+ val dx = event.x - downScreenX
|
|
|
+ val dy = event.y - downScreenY
|
|
|
+ if (dx*dx + dy*dy > touchSlopSq) {
|
|
|
+ pendingLongPress = false
|
|
|
+ mapView.removeCallbacks(longPressRunnable)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ MotionEvent.ACTION_UP -> {
|
|
|
+ // 取消长按
|
|
|
+ mapView.removeCallbacks(longPressRunnable)
|
|
|
+
|
|
|
+ // 如果刚才已触发长按,吞掉点击
|
|
|
+ if (longPressTriggered) {
|
|
|
+ pendingLongPress = false
|
|
|
+ longPressTriggered = false
|
|
|
+ currentPressed = null
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // 命中半径(用和绘制一致的图内半径,稍放宽)
|
|
|
+ // —— 点击:先放大,再居中,再标记(保证最终居中)——
|
|
|
+ val mapPt = screenToMap(event.x, event.y) ?: return
|
|
|
+ val hit = hitTest(mapPt.x, mapPt.y) ?: return
|
|
|
+
|
|
|
+ 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 {
|
|
|
+ // 万一拿不到中心,就直接在下一步里统一做
|
|
|
+ }
|
|
|
+
|
|
|
+ // ② 再居中到目标点(保持 targetScale),确保最后居中
|
|
|
+ mapView?.postDelayed({
|
|
|
+ mapView?.animateCenterOnPoint(targetMapX, targetMapY, 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()
|
|
|
+ }
|
|
|
+ }, clickZoomDurationMs + clickCenterDurationMs)
|
|
|
+ }
|
|
|
+
|
|
|
+ MotionEvent.ACTION_CANCEL -> {
|
|
|
+ pendingLongPress = false
|
|
|
+ longPressTriggered = false
|
|
|
+ currentPressed = null
|
|
|
+ mapView.removeCallbacks(longPressRunnable)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ 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
|
|
|
@@ -147,102 +312,161 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
hit = p
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // 命中了才操作:用 pointSerialNumber 走你现成的 selectPoint;scale 用当前缩放
|
|
|
- hit?.pointSerialNumber?.let { serial ->
|
|
|
- selectPoint(
|
|
|
- serial = serial,
|
|
|
- animated = true,
|
|
|
- durationMs = 300L,
|
|
|
- scale = lastKnownZoom // 不改缩放,只居中
|
|
|
- )
|
|
|
- }
|
|
|
+ return hit
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
override fun draw(
|
|
|
canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
|
|
|
) {
|
|
|
- if (!isVisible) return // ← 只有这一个早退允许在 save 之前
|
|
|
- if (inDraw) {
|
|
|
- return
|
|
|
- }
|
|
|
+ if (!isVisible) return
|
|
|
+ if (inDraw) return
|
|
|
inDraw = true
|
|
|
+
|
|
|
this.currentZoom = currentZoom
|
|
|
- lastKnownZoom = currentZoom // ← 记录下来
|
|
|
+ lastKnownZoom = currentZoom
|
|
|
currentDegree = 360 - currentRotateDegrees
|
|
|
+
|
|
|
try {
|
|
|
canvas.save()
|
|
|
try {
|
|
|
-// 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
|
|
|
+ // 合并地图矩阵
|
|
|
canvas.concat(currentMatrix)
|
|
|
+
|
|
|
val switchData = ModBusController.getSwitchData()
|
|
|
val tempPointList = synchronized(stationList) { stationList.toList() }
|
|
|
+
|
|
|
+ // 如果存在 ALARM,则确保动画驱动起来
|
|
|
+ if (hasAnyAlarm(tempPointList, switchData)) {
|
|
|
+ startAnimation()
|
|
|
+ }
|
|
|
+
|
|
|
tempPointList.forEach { point ->
|
|
|
- val switchStatus = switchData
|
|
|
- .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
|
|
|
- // point.pos.x/y 已经是「图内像素坐标」
|
|
|
+ val bean = switchData.find { it.idx == point.pointSerialNumber?.toInt() }
|
|
|
+ val status = parseStatus(bean)
|
|
|
+
|
|
|
val x = point.pos.x
|
|
|
val y = point.pos.y
|
|
|
- // 先画背景(它会被 currentMatrix 自动缩放)
|
|
|
- paint.alpha = 255
|
|
|
- if (switchStatus != null) {
|
|
|
- if (point.isSelected) {
|
|
|
- paint.color = Color.RED
|
|
|
- paint.strokeWidth = 2f
|
|
|
- paint.style = Paint.Style.STROKE
|
|
|
- canvas.drawRect(
|
|
|
- x - switchSize / 2 - paint.strokeWidth,
|
|
|
- y - switchSize / 2 - paint.strokeWidth,
|
|
|
- x + switchSize * 1.5f + paint.strokeWidth,
|
|
|
- y + switchSize * 1.5f + paint.strokeWidth,
|
|
|
- paint
|
|
|
- )
|
|
|
- }
|
|
|
- paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
- // 再画 icon
|
|
|
- if (switchStatus) {
|
|
|
+
|
|
|
+ // ---- 选中态:蓝色外环(想要回红框,把这段改回去即可)----
|
|
|
+ if (point.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.style = Paint.Style.FILL_AND_STROKE
|
|
|
+ when (status) {
|
|
|
+ SwitchStatus.ON -> {
|
|
|
+ stationList.firstOrNull { it.entityId == point.entityId }?.status = 1
|
|
|
paint.color = ContextCompat.getColor(
|
|
|
- MyApplication.instance?.applicationContext!!,
|
|
|
+ MyApplication.instance!!.applicationContext,
|
|
|
R.color.common_switch_enable
|
|
|
)
|
|
|
- paint.alpha = alpha
|
|
|
+ paint.alpha = 255
|
|
|
canvas.drawCircle(
|
|
|
x + switchSize / 2,
|
|
|
y + switchSize / 2,
|
|
|
- switchSize, paint
|
|
|
+ switchSize,
|
|
|
+ paint
|
|
|
)
|
|
|
- paint.alpha = 255
|
|
|
- } else {
|
|
|
+ }
|
|
|
+
|
|
|
+ SwitchStatus.OFF -> {
|
|
|
+ stationList.firstOrNull { it.entityId == point.entityId }?.status = 0
|
|
|
paint.color = ContextCompat.getColor(
|
|
|
- MyApplication.instance?.applicationContext!!,
|
|
|
+ MyApplication.instance!!.applicationContext,
|
|
|
R.color.common_switch_disable
|
|
|
)
|
|
|
+ paint.alpha = 255
|
|
|
+ canvas.drawCircle(
|
|
|
+ x + switchSize / 2,
|
|
|
+ y + switchSize / 2,
|
|
|
+ switchSize,
|
|
|
+ paint
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ SwitchStatus.ALARM -> {
|
|
|
+ stationList.firstOrNull { it.entityId == point.entityId }?.status = 2
|
|
|
+ // 脉冲:颜色红/橙跳变 + alpha 呼吸 + 半径轻微伸缩 + 外光晕
|
|
|
+ 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 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,
|
|
|
- switchSize, paint
|
|
|
+ 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
|
|
|
+ )
|
|
|
+ paint.alpha = 255
|
|
|
}
|
|
|
}
|
|
|
- // 然后画文字
|
|
|
+
|
|
|
+ // ---- 文本(和你原来一致)----
|
|
|
paint.style = Paint.Style.FILL
|
|
|
paint.strokeWidth = 1f
|
|
|
paint.color = Color.WHITE
|
|
|
- paint.textSize = textSize // 这里是「图内」的文字大小,后面会跟着缩放
|
|
|
- val textW = paint.measureText(point.pointSerialNumber ?: "")
|
|
|
- // 计算文本宽度和偏移
|
|
|
- val fontMetrics = paint.fontMetrics
|
|
|
- val textHeight = fontMetrics.bottom - fontMetrics.top
|
|
|
-
|
|
|
- // 计算绘制文本的起点(左下角)
|
|
|
+ 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 + (textHeight / 2 - fontMetrics.bottom)
|
|
|
- canvas.drawText(
|
|
|
- "${point.pointSerialNumber}",
|
|
|
- textX,
|
|
|
- textY,
|
|
|
- paint
|
|
|
- )
|
|
|
+ val textY = y + switchSize / 2 + (textH / 2 - fm.bottom)
|
|
|
+ canvas.drawText(text, textX, textY, paint)
|
|
|
}
|
|
|
} finally {
|
|
|
canvas.restore()
|
|
|
@@ -250,25 +474,17 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
} finally {
|
|
|
inDraw = false
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据序号查找点位的「图内坐标」。
|
|
|
- * @return 对应的 PointF(x,y),找不到返回 null。
|
|
|
*/
|
|
|
fun getPointPosition(serial: String): PointF? {
|
|
|
- return stationList
|
|
|
- .find { it.pointSerialNumber == serial }
|
|
|
- ?.pos
|
|
|
+ return stationList.find { it.pointSerialNumber == serial }?.pos
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 选中一个点位:标记 isSelected 并定位到视图中心。
|
|
|
- * @param serial 要选中的序号
|
|
|
- * @param animated 是否平滑动画,false 则瞬间定位
|
|
|
- * @param durationMs 动画时长(仅 animated = true 时生效)
|
|
|
- * @return 是否成功找到了并处理
|
|
|
*/
|
|
|
fun selectPoint(
|
|
|
serial: String,
|
|
|
@@ -280,18 +496,12 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
point.isSelected = true
|
|
|
ThreadUtils.runOnMainDelayed(3000) {
|
|
|
point.isSelected = false
|
|
|
- stationList.replaceAll {
|
|
|
- it.copy(isSelected = false)
|
|
|
- }
|
|
|
+ stationList.replaceAll { it.copy(isSelected = false) }
|
|
|
mapView?.postInvalidate()
|
|
|
}
|
|
|
- // 1. 更新状态(如果你要改变外观,比如高亮,记得在 draw() 里用 isSelected)
|
|
|
- stationList.replaceAll {
|
|
|
- it.copy(isSelected = (it.pointSerialNumber == serial))
|
|
|
- }
|
|
|
+ stationList.replaceAll { it.copy(isSelected = (it.pointSerialNumber == serial)) }
|
|
|
mapView?.postInvalidate()
|
|
|
|
|
|
- // 2. 定位:调用 MapView 的 API
|
|
|
mapView?.let { mv ->
|
|
|
if (animated) {
|
|
|
mv.animateCenterOnPoint(point.pos.x, point.pos.y, durationMs, scale)
|
|
|
@@ -302,6 +512,46 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
+ // ====== 状态解析 & 动画触发判定 ======
|
|
|
+
|
|
|
+ /** 预留:设备状态 */
|
|
|
+ private enum class SwitchStatus { ON, OFF, ALARM, UNKNOWN }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一解析设备状态:未来 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
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否告警(预留钩子):将来你在 SwitchBean 里加 type/statusCode 等,改这里即可。
|
|
|
+ * 当前默认:不告警。
|
|
|
+ */
|
|
|
+ private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
|
|
|
+ // 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
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====== 数据结构(保持你的原样)======
|
|
|
+
|
|
|
data class IsolationPoint(
|
|
|
val pos: PointF,
|
|
|
val entityName: String,
|
|
|
@@ -309,5 +559,6 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val entityId: Long,
|
|
|
val pointSerialNumber: String?,
|
|
|
var isSelected: Boolean,
|
|
|
+ var status: Int = 0
|
|
|
)
|
|
|
}
|