Просмотр исходного кода

refactor(更新) :
- MapView 刷新机制优化,采用离屏 Picture 提高渲染效率
- CustomSwitchStationLayer 尺寸和文本大小调整,优化点击判定范围
- SwitchStatusActivity 中地图点位层提前创建,避免在 onMapLoadSuccess 中重复创建
- SwitchStatusActivity 列表项显示电机名称和编码
- BitmapUtil 增加 loadBitmapFromFile 方法,用于从文件加载并缩放图片,支持修正方向和拉伸填充
- MapLayer 增加网格线显示功能
- 移除 RegionTileLayer

周文健 2 месяцев назад
Родитель
Сommit
366ded476d

+ 113 - 0
app/src/main/java/com/grkj/iscs_mars/util/BitmapUtil.kt

@@ -4,10 +4,13 @@ import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.ImageFormat
+import android.graphics.Matrix
 import android.graphics.Rect
 import android.graphics.YuvImage
 import android.graphics.drawable.Drawable
+import android.media.ExifInterface
 import android.os.Environment
 import androidx.collection.LruCache
 import com.bumptech.glide.Glide
@@ -26,9 +29,11 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import java.io.ByteArrayOutputStream
 import java.io.File
+import java.io.FileInputStream
 import java.io.FileOutputStream
 import java.io.IOException
 import kotlin.math.max
+import kotlin.math.roundToInt
 
 /**
  * 通用图片工具:
@@ -323,4 +328,112 @@ object BitmapUtil {
 
         return bitmap
     }
+
+    /**
+     * 从文件解码并缩放到目标画布(左上对齐,留空不裁剪)。
+     * - 等比缩放 (contain)
+     * - 结果位图尺寸固定为 outWidth x outHeight
+     * - 图像贴在左上角 (0,0),剩余区域可用背景色填充
+     *
+     * @param allowUpscale  是否允许把小图放大;false 时小图保持原尺寸贴左上,其余留空
+     * @param backgroundColor  画布背景;null 表示透明
+     */
+    fun loadBitmapFromFile(
+        file: File,
+        outWidth: Int,
+        outHeight: Int,
+        allowUpscale: Boolean = true,
+        fixOrientation: Boolean = true,
+        backgroundColor: Int? = null,
+        stretchToFill: Boolean = true,   // <- 新增:是否拉伸填满(非等比)
+    ): Bitmap? {
+        if (!file.exists() || outWidth <= 0 || outHeight <= 0) return null
+
+        fun readExifRotation(path: String): Int = try {
+            when (ExifInterface(path).getAttributeInt(
+                ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL
+            )) {
+                ExifInterface.ORIENTATION_ROTATE_90  -> 90
+                ExifInterface.ORIENTATION_ROTATE_180 -> 180
+                ExifInterface.ORIENTATION_ROTATE_270 -> 270
+                else -> 0
+            }
+        } catch (_: Throwable) { 0 }
+
+        // 1) 读原始尺寸
+        val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+        try { FileInputStream(file).use { BitmapFactory.decodeStream(it, null, bounds) } }
+        catch (_: Throwable) { return null }
+        var sw = bounds.outWidth
+        var sh = bounds.outHeight
+        if (sw <= 0 || sh <= 0) return null
+
+        // 2) 采样(粗解码,尽量贴近目标绘制尺寸,避免后续放大锯齿)
+        // 先估一个“期望绘制尺寸”(不考虑旋转带来的宽高互换,足够接近即可)
+        val (targetW, targetH) = if (stretchToFill) {
+            val tw = if (allowUpscale) outWidth else minOf(outWidth, sw)
+            val th = if (allowUpscale) outHeight else minOf(outHeight, sh)
+            tw to th
+        } else {
+            val s = minOf(outWidth / sw.toFloat(), outHeight / sh.toFloat())
+            val scale = if (!allowUpscale && s > 1f) 1f else s
+            (sw * scale).coerceAtLeast(1f).toInt() to (sh * scale).coerceAtLeast(1f).toInt()
+        }
+
+        var inSample = 1
+        while (sw / (inSample * 2) >= targetW && sh / (inSample * 2) >= targetH) {
+            inSample *= 2
+        }
+
+        val opts = BitmapFactory.Options().apply {
+            inJustDecodeBounds = false
+            inPreferredConfig = Bitmap.Config.ARGB_8888
+            inDither = true
+            inSampleSize = inSample
+        }
+        var bmp = try { FileInputStream(file).use { BitmapFactory.decodeStream(it, null, opts) } }
+        catch (_: Throwable) { null }
+        if (bmp == null) return null
+
+        // 3) EXIF 方向修正
+        if (fixOrientation) {
+            val deg = readExifRotation(file.absolutePath)
+            if (deg != 0) {
+                val m = Matrix().apply { postRotate(deg.toFloat()) }
+                Bitmap.createBitmap(bmp, 0, 0, bmp.width, bmp.height, m, true)?.let { r ->
+                    if (r !== bmp) bmp.recycle()
+                    bmp = r
+                }
+            }
+        }
+        if (bmp == null) return null
+
+        // 4) 按模式计算最终绘制尺寸(左上对齐)
+        sw = bmp.width
+        sh = bmp.height
+
+        val (drawW, drawH) = if (stretchToFill) {
+            // 非等比拉伸:独立的 x/y 缩放;allowUpscale=false 时只缩不放
+            val sx = if (allowUpscale) outWidth / sw.toFloat() else minOf(1f, outWidth / sw.toFloat())
+            val sy = if (allowUpscale) outHeight / sh.toFloat() else minOf(1f, outHeight / sh.toFloat())
+            maxOf(1, (sw * sx).toInt()) to maxOf(1, (sh * sy).toInt())
+        } else {
+            // 等比 contain:不裁剪,左上贴,留空
+            val s = minOf(outWidth / sw.toFloat(), outHeight / sh.toFloat())
+            val scale = if (!allowUpscale && s > 1f) 1f else s
+            maxOf(1, (sw * scale).toInt()) to maxOf(1, (sh * scale).toInt())
+        }
+
+        // 5) 画到固定尺寸画布(左上角为原点,(0,0)贴齐)
+        val out = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888)
+        val c = Canvas(out)
+        if (backgroundColor != null) c.drawColor(backgroundColor) else c.drawColor(Color.TRANSPARENT)
+
+        val dst = Rect(0, 0, drawW, drawH) // 左上贴齐;伸缩后的尺寸
+        c.drawBitmap(bmp, null, dst, null)
+
+        if (!bmp.isRecycled) bmp.recycle()
+        return out
+    }
+
 }

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

@@ -28,6 +28,6 @@ fun MapView.replaceLayer(tag: String, newLayer: MapBaseLayer) {
     layers.add(0, newLayer)
 
     // 3. 请求重绘
-    postInvalidate()
+    refresh()
 }
 

+ 21 - 20
app/src/main/java/com/grkj/iscs_mars/view/activity/SwitchStatusActivity.kt

@@ -97,7 +97,7 @@ class SwitchStatusActivity :
         val itemBinding = getBinding<ItemSwitchBinding>()
         val item = getModel<MotorMapInfoRespVO.IsMotorMapPoint>()
         val switchData = ModBusController.getSwitchData()
-        itemBinding.switchName.text = item.motorName
+        itemBinding.switchName.text = "${item.motorName} - ${item.motorCode}"
         itemBinding.switchId.text = context.getString(R.string.switch_id, item.pointNfc)
         val switchStatus = switchData
             .find { it.idx == item.pointSerialNumber?.toInt() }?.enabled
@@ -174,29 +174,29 @@ class SwitchStatusActivity :
     private fun initMap() {
         mBinding?.mapview?.isScaleAndRotateTogether = false
         mBinding?.mapview?.setOnTouchListener { _, event ->
-            gestureDetector.onTouchEvent(event)
-            false
+            gestureDetector.onTouchEvent(event); false
         }
+
+        // ✅ 提前创建点位层(不要等 onMapLoadSuccess)
+        if (stationLayer == null) {
+            stationLayer = CustomSwitchStationLayer(mBinding?.mapview, mutableListOf()).apply {
+                onLongPressListener = { point, screenX, screenY, _, _ ->
+                    switchInfoDialog.setSwitchInfo(
+                        point.motorName ?: "",
+                        "${point.pointNfc}",
+                        point.status
+                    )
+                    switchInfoDialog.showPopupWindow(screenX.toInt(), screenY.toInt())
+                }
+            }
+            mBinding?.mapview?.addLayer(stationLayer)
+        }
+
         mBinding?.mapview?.setMapViewListener(object : MapViewListener {
             override fun onMapLoadSuccess() {
+                // 地图加载完成后,只做视图层面的事,别再 new layer 了
                 mBinding?.mapview?.post {
-                    if (stationLayer != null) {
-                        mBinding?.mapview?.currentRotateDegrees = 0f
-                        return@post
-                    }
-                    stationLayer = CustomSwitchStationLayer(
-                        mBinding?.mapview, presenter?.mStationList ?: mutableListOf()
-                    )
-                    stationLayer?.onLongPressListener = { point, screenX, screenY, _, _ ->
-                        switchInfoDialog.setSwitchInfo(
-                            point.motorName ?: "",
-                            "${point.pointNfc}",
-                            point.status
-                        )
-                        switchInfoDialog.showPopupWindow(screenX.toInt(), screenY.toInt())
-                    }
-                    mBinding?.mapview?.addLayer(stationLayer)
-                    stationLayer?.setRatio(presenter?.mapRatio ?: 1f)
+                    mBinding?.mapview?.currentRotateDegrees = 0f
                     mBinding?.mapview?.refresh()
                 }
             }
@@ -206,4 +206,5 @@ class SwitchStatusActivity :
             }
         })
     }
+
 }

+ 85 - 93
app/src/main/java/com/grkj/iscs_mars/view/presenter/SwitchStatusPresenter.kt

@@ -9,11 +9,9 @@ import com.grkj.iscs_mars.util.BitmapUtil
 import com.grkj.iscs_mars.util.Executor
 import com.grkj.iscs_mars.util.NetApi
 import com.grkj.iscs_mars.util.log.LogUtil
-import com.grkj.iscs_mars.util.replaceLayer
 import com.grkj.iscs_mars.view.base.BasePresenter
 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
 
@@ -91,105 +89,99 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
             return
         }
 
-        // 首屏快速预览(低清),仅用于先显示个底色/缩略
-        BitmapUtil.decodePreview(
-            tempImageFile,
-            backgroundColor = context.getColor(R.color.color_map_base)
-        ).also(onPreview)
-
         // ===== 2) 读取后端坐标系尺寸 =====
         val backendW = itMapInfo.width?.toFloat() ?: return
         val backendH = itMapInfo.height?.toFloat() ?: return
+        // 首屏快速预览(低清),仅用于先显示个底色/缩略
+        LogUtil.i("后台设置数据:${backendW},${backendH}")
+        val map = BitmapUtil.loadBitmapFromFile(
+            tempImageFile,
+            outWidth = backendW.toInt(),
+            outHeight = backendH.toInt(),
+            backgroundColor = context.getColor(R.color.color_map_base)
+        )?.also(onPreview)
+
+        map?.let {
+            val points = mutableListOf<CustomSwitchStationLayer.IsolationPoint>()
+            val byId = mutableMapOf<Long, CustomSwitchStationLayer.IsolationPoint>()
+            val ratio = getInitZoom(
+                (mapViewRef()?.measuredWidth ?: it.width).toFloat(),
+                (mapViewRef()?.measuredHeight ?: it.height).toFloat(),
+                backendW,
+                backendH
+            )
+            LogUtil.i("缩放比1:${mapViewRef()?.measuredWidth},${mapViewRef()?.measuredHeight},${map.width},${map.height}")
+            LogUtil.i("缩放比2:${ratio}")
+            val offX = itMapInfo.x?.toFloatOrNull() ?: 0f
+            val offY = itMapInfo.y?.toFloatOrNull() ?: 0f
+            itMotorMapInfo?.data?.asSequence()
+                ?.filter { it.x != null && it.y != null && it.motorId != null }?.forEach { pt ->
+                    // 每个点:
+                    val gridX = (pt.x!!.toFloat() + 1.5f) * cellPx
+                    val gridY = (pt.y!!.toFloat() + 1.5f) * cellPx
+
+                    // **修正:用 ‘加’ 把子图局部坐标搬到大图坐标系**
+                    var worldX = gridX + offX
+                    var worldY = gridY + offY
+
+                    // 如果后端以左下为原点,需要翻 Y(基于“世界高度”翻)
+                    if (yAxisFlip) {
+                        worldY = backendH - worldY
+                    }
 
-
-        // ===== 3) 计算点位(全部落在“后端坐标系”里)=====
-        val offX = (itMapInfo.x ?: "0").toFloat()
-        val offY = (itMapInfo.y ?: "0").toFloat()
-
-        val points = mutableListOf<CustomSwitchStationLayer.IsolationPoint>()
-        val byId = mutableMapOf<Long, CustomSwitchStationLayer.IsolationPoint>()
-
-        itMotorMapInfo?.data
-            ?.asSequence()
-            ?.filter { it.x != null && it.y != null && it.motorId != null }
-            ?.forEach { pt ->
-                // 每个点:
-                val gridX = (pt.x!!.toFloat() + 0.5f) * cellPx
-                val gridY = (pt.y!!.toFloat() + 0.5f) * cellPx
-
-                // **修正:用 ‘加’ 把子图局部坐标搬到大图坐标系**
-                var worldX = gridX + offX
-                var worldY = gridY + offY
-
-                // 如果后端以左下为原点,需要翻 Y(基于“世界高度”翻)
-                if (yAxisFlip) {
-                    worldY = backendH - worldY
-                }
-
-                // 2) 去掉子图偏移(后端通常给“子图左上在大图中的偏移”)
-                var localX = gridX - offX
-                var localY = gridY - offY
-
-                // 3) 若后端以左下为原点,需要翻转 Y(以后端子图高度为参考)
-                if (yAxisFlip) {
-                    worldY = backendH - worldY
-                }
-
-                // 4) 直接作为最终坐标(不要乘 ratioX/ratioY)
-                val fX = localX
-                val fY = localY
-
-                val switchStatus =
-                    if (pt.switchStatus == "1") CustomSwitchStationLayer.STATUS_ON
-                    else CustomSwitchStationLayer.STATUS_OFF
-
-                val p = CustomSwitchStationLayer.IsolationPoint(
-                    pos = android.graphics.PointF(worldX, worldY),
-                    motorCode = pt.motorCode,
-                    pointName = pt.pointName,
-                    motorName = pt.motorName,
-                    motorType = pt.motorType,
-                    pointId = pt.pointId,
-                    icon = null,
-                    entityId = pt.motorId!!,
-                    pointSerialNumber = pt.pointSerialNumber,
-                    isSelected = false,
-                    pointNfc = pt.pointNfc ?: "",
-                    status = switchStatus
-                )
-                points += p
-                byId[p.entityId] = p
-
-                // 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)
+                    val switchStatus =
+                        if (pt.switchStatus == "1") CustomSwitchStationLayer.STATUS_ON
+                        else CustomSwitchStationLayer.STATUS_OFF
+
+                    val p = CustomSwitchStationLayer.IsolationPoint(
+                        pos = android.graphics.PointF(worldX, worldY),
+                        motorCode = pt.motorCode,
+                        pointName = pt.pointName,
+                        motorName = pt.motorName,
+                        motorType = pt.motorType,
+                        pointId = pt.pointId,
+                        icon = null,
+                        entityId = pt.motorId!!,
+                        pointSerialNumber = pt.pointSerialNumber,
+                        isSelected = false,
+                        pointNfc = pt.pointNfc ?: "",
+                        status = switchStatus
+                    )
+                    points += p
+                    byId[p.entityId] = p
+
+                    // 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)
+                        }
                     }
                 }
-            }
-
-        // 6) 提交点位(保持选中可选)
-        stationLayer?.submitPoints(points)
 
-        /* --- 6) 用 RegionTileLayer 替换旧底图(内部自带 LruCache / 512 tile) --- */
-        if (needReloadMap) {
-            mapViewRef()?.let { mv ->
-                imgFile?.let {
-                    val tileLayer = RegionTileLayer(mv, it)
-                    mv.replaceLayer("bigMap", tileLayer)
-                }
-            }
+            // 6) 提交点位(保持选中可选)
+            stationLayer?.submitPoints(points)
         }
-        needReloadMap = false
+    }
+
+    /**
+     * calculate init zoom
+     *
+     * @param viewWidth
+     * @param viewHeight
+     * @param imageWidth
+     * @param imageHeight
+     * @return
+     */
+    // 等比塞入视图(不裁剪)
+    private fun getInitZoom(vw: Float, vh: Float, iw: Float, ih: Float): Float {
+        if (vw <= 0f || vh <= 0f || iw <= 0f || ih <= 0f) return 1f
+        return minOf(vw / iw, vh / ih)
     }
 
 

+ 7 - 19
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomSwitchStationLayer.kt

@@ -88,11 +88,9 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private val desiredSelectScale = 2.0f
 
     // ===== 尺寸(与原版兼容)=====
-    private val BASE_SWITCH_SIZE = 16f
-    private val BASE_TEXT_SIZE = 12f
+    private val BASE_SWITCH_SIZE = 15f
+    private val BASE_TEXT_SIZE = 11f
 
-    // 仍然保留 setRatio 语义
-    private var ratio: Float = 1f
     private var switchSize: Float = BASE_SWITCH_SIZE   // 仅作为屏幕像素基准(dp->px后)
     private var textSize: Float = BASE_TEXT_SIZE
 
@@ -140,7 +138,6 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
     init {
         paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE }
-        setRatio(1f) // 初始化
         startAnimation()
     }
 
@@ -207,15 +204,6 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         else throttleInvalidate()
     }
 
-    /** 比例(基准)。兼容原有 API:影响圆与文字的“基准尺寸”,不影响坐标 */
-    fun setRatio(ratio: Float) {
-        this.ratio = ratio.coerceAtLeast(0.1f)
-        switchSize = BASE_SWITCH_SIZE * this.ratio     // 屏幕像素基准
-        textSize = BASE_TEXT_SIZE * this.ratio         // (文本的 min/max 可用它当基准去推)
-        throttleInvalidate()
-    }
-
-
     /** 按 entityId 选中并定位(保留原 API) */
     fun selectPoint(
         entityId: Long?,
@@ -343,8 +331,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
             // 由于 canvas 已被 currentMatrix 放大了 zoom 倍,
             // 画到“地图坐标”层面的尺寸需要 /zoom 才能抵消缩放,获得恒定屏幕像素。
-            val mapR = screenR / currentZoom
-            val mapStroke = screenStroke / currentZoom
+            val mapR = screenR
+            val mapStroke = screenStroke
 
             for (p in points) {
                 val c = centerOf(p)
@@ -386,7 +374,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     val maxTextWidthScreen = (screenR * 2f) - 6f  // 屏幕像素
                     // 先在“屏幕像素语义”下求字号
                     val screenTextSize = fitTextSizeScreen(paint, text, maxTextWidthScreen, screenTextMin, screenTextMax)
-                    val mapTextSize = screenTextSize / currentZoom
+                    val mapTextSize = screenTextSize
 
                     paint.textSize = mapTextSize
                     val textW = paint.measureText(text)
@@ -454,7 +442,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
     private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
         val screenR = switchSize * 1.2f
-        val hitR = screenR / currentZoom        // 转回地图坐标
+        val hitR = screenR        // 转回地图坐标
         val hitR2 = hitR * hitR
 
         val snapshot = synchronized(dataLock) { stationList.toList() }
@@ -472,7 +460,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
 
     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
+        return abs(x - other.x) <= eps && kotlin.math.abs(y - other.y) <= eps
     }
 
     /** 优先做局部刷新;MapView 若无 invalidateRect 则回退整层刷新 */

+ 0 - 22
app/src/main/java/com/grkj/iscs_mars/view/widget/RegionTileLayer.kt

@@ -1,22 +0,0 @@
-package com.grkj.iscs_mars.view.widget
-
-import android.graphics.*
-import android.view.MotionEvent
-import com.onlylemi.mapview.library.MapView
-import com.onlylemi.mapview.library.layer.MapBaseLayer
-import java.io.File
-
-class RegionTileLayer(val mapView: MapView?, val mapFile: File) : MapBaseLayer(mapView) {
-    override fun onTouch(event: MotionEvent) {
-
-    }
-
-    override fun draw(
-        canvas: Canvas,
-        currentMatrix: Matrix,
-        currentZoom: Float,
-        currentRotateDegrees: Float
-    ) {
-
-    }
-}

+ 62 - 11
app/src/main/java/com/onlylemi/mapview/library/MapView.java

@@ -38,8 +38,6 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     private float currentZoom = 1.0f;
     private float saveZoom = 1.0f;           // 默认初始保存zoom为1
     private float currentRotateDegrees = 0.0f;
-    private float saveRotateDegrees = 0.0f;
-
     private float minZoom = 0.5f;
     private float maxZoom = 3.0f;
     private boolean isScaleAndRotateTogether = false;
@@ -57,10 +55,13 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
 
     public static final int TOUCH_STATE_NO = 0;
     public static final int TOUCH_STATE_SCROLL = 1;
-    public static final int TOUCH_STATE_SCALE = 2;
-    public static final int TOUCH_STATE_ROTATE = 3;
     public static final int TOUCH_STATE_TWO_POINTED = 4;
 
+    // ★ 用于“先合成再整体变换”的离屏录制
+    private final Matrix IDENTITY = new Matrix(); // 恒等矩阵
+    private Picture framePicture;                 // 每帧录制的世界坐标内容
+    private int frameW = 0, frameH = 0;          // 录制尺寸(通常等于地图宽高)
+
     public MapView(Context context) {
         this(context, null);
     }
@@ -117,6 +118,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                 post(() -> {
                     if (mapLayer == null) {
                         mapLayer = new MapLayer(MapView.this);
+                        mapLayer.tag = "bigMap";
                         layers.add(0, mapLayer); // 确保底层在最底
                     } else if (!layers.contains(mapLayer)) {
                         layers.add(0, mapLayer);
@@ -126,6 +128,11 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                         mapViewListener.onMapLoadSuccess();
                     }
                     isMapLoadFinish = true;
+
+                    // ★ 地图尺寸变了,下一帧重建离屏
+                    framePicture = null;
+                    frameW = frameH = 0;
+
                     refresh();
                 });
             } else {
@@ -146,17 +153,57 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         return Bitmap.createScaledBitmap(src, w, h, true);
     }
 
+    // ★★★ 关键修改:先把所有 layer 画进“世界坐标系”的离屏 Picture,再按 currentMatrix 一次性变换绘制到屏幕
     public void refresh() {
         if (surface == null || !isMapLoadFinish) return;
+
+        // 1) 确定世界坐标系尺寸(优先用地图尺寸)
+        int worldW = Math.max(1, (int) getMapWidth());
+        int worldH = Math.max(1, (int) getMapHeight());
+        if (worldW <= 1 || worldH <= 1) {
+            // 没有地图时退化回旧路径(直接按 currentMatrix 画)
+            Canvas fallback = lockCanvas();
+            if (fallback != null) {
+                try {
+                    fallback.drawColor(backgroundColor);
+                    for (MapBaseLayer layer : layers) {
+                        if (layer.isVisible) {
+                            layer.draw(fallback, currentMatrix, currentZoom, currentRotateDegrees);
+                        }
+                    }
+                } finally {
+                    unlockCanvasAndPost(fallback);
+                }
+            }
+            return;
+        }
+
+        // 2) 录制一帧:世界坐标系下(不带任何矩阵变换)
+        if (framePicture == null || frameW != worldW || frameH != worldH) {
+            framePicture = new Picture();
+            frameW = worldW;
+            frameH = worldH;
+        }
+        Canvas rec = framePicture.beginRecording(frameW, frameH);
+        // 背景:交给主画布处理(保持透明),如果你想要统一底色,也可以这里 rec.drawColor(backgroundColor);
+        rec.drawColor(Color.TRANSPARENT);
+        for (MapBaseLayer layer : layers) {
+            if (layer.isVisible) {
+                // 关键:传入恒等矩阵,让各层在“世界坐标系”里绘制
+                layer.draw(rec, IDENTITY, currentZoom, currentRotateDegrees);
+            }
+        }
+        framePicture.endRecording();
+
+        // 3) 把录制好的内容一次性按 currentMatrix 贴到屏幕
         Canvas canvas = lockCanvas();
         if (canvas != null) {
             try {
                 canvas.drawColor(backgroundColor);
-                for (MapBaseLayer layer : layers) {
-                    if (layer.isVisible) {
-                        layer.draw(canvas, currentMatrix, currentZoom, currentRotateDegrees);
-                    }
-                }
+                canvas.save();
+                canvas.concat(currentMatrix);  // 整体缩放/平移/旋转应用在这里
+                framePicture.draw(canvas);     // (0,0) → (worldW, worldH)
+                canvas.restore();
             } finally {
                 unlockCanvasAndPost(canvas);
             }
@@ -181,7 +228,6 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                 if (event.getPointerCount() == 2) {
                     saveMatrix.set(currentMatrix);
                     saveZoom = currentZoom;
-                    saveRotateDegrees = currentRotateDegrees;
                     mid.set(midPoint(event));
                     oldDist = distance(event, mid);
                     oldDegree = rotation(event, mid);
@@ -248,6 +294,8 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         }
         if (layer != null && !layers.contains(layer)) {
             layers.add(layer);
+            // ★ 内容改变,下一帧重录
+            framePicture = null;
             refresh();
         }
     }
@@ -259,6 +307,8 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
             return;
         }
         if (layer != null && layers.remove(layer)) {
+            // ★ 内容改变,下一帧重录
+            framePicture = null;
             refresh();
         }
     }
@@ -270,6 +320,8 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
             return;
         }
         layers.clear();
+        // ★ 内容改变,下一帧重录
+        framePicture = null;
         refresh();
     }
 
@@ -344,7 +396,6 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     }
 
     public void setMinZoom(float min) {
-        LogUtil.INSTANCE.i("最小缩放比:" + min);
         this.minZoom = min;
     }
 

+ 115 - 1
app/src/main/java/com/onlylemi/mapview/library/layer/MapLayer.java

@@ -1,7 +1,11 @@
 package com.onlylemi.mapview.library.layer;
 
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
 import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PathEffect;
 import android.graphics.Picture;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -21,9 +25,71 @@ public class MapLayer extends MapBaseLayer {
     private Picture image;
     private boolean hasMeasured;
 
+    // ====== Grid config ======
+    private boolean showGrid = false;
+    private float gridStepX = 10f;       // 水平间隔(像素,基于图片/地图平面坐标)
+    private float gridStepY = 10f;       // 垂直间隔
+    private float gridOffsetX = 0f;      // X 偏移(像素)
+    private float gridOffsetY = 0f;      // Y 偏移(像素)
+    private int gridColor = Color.argb(100, 255, 0, 0);
+    private int gridSecColor = Color.argb(100, 0, 0, 255);
+    private float gridStroke = 1f;
+    private boolean gridDashed = true;
+    private final Paint gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
     public MapLayer(MapView mapView) {
         super(mapView);
         level = MAP_LEVEL;
+
+        gridPaint.setStyle(Paint.Style.STROKE);
+        gridPaint.setColor(gridColor);
+        gridPaint.setStrokeWidth(gridStroke);
+//        applyDash();
+    }
+// ====== Grid API ======
+
+    /**
+     * 显示/隐藏网格线
+     */
+    public void enableGrid(boolean enable) {
+        this.showGrid = enable;
+        mapView.refresh();
+    }
+
+    /**
+     * 设置网格间隔(像素),<=0 表示不画该方向
+     */
+    public void setGrid(float stepX, float stepY) {
+        this.gridStepX = stepX;
+        this.gridStepY = stepY;
+        mapView.refresh();
+    }
+
+    /**
+     * 设置网格样式
+     */
+    public void setGridStyle(int color, float stroke, boolean dashed) {
+        this.gridColor = color;
+        this.gridStroke = Math.max(0.5f, stroke);
+        this.gridDashed = dashed;
+        gridPaint.setColor(gridColor);
+        gridPaint.setStrokeWidth(this.gridStroke);
+        applyDash();
+        mapView.refresh();
+    }
+
+    /**
+     * 设置网格偏移(像素),可对齐到某个参考原点
+     */
+    public void setGridOffset(float offsetX, float offsetY) {
+        this.gridOffsetX = offsetX;
+        this.gridOffsetY = offsetY;
+        mapView.refresh();
+    }
+
+    private void applyDash() {
+        PathEffect effect = gridDashed ? new DashPathEffect(new float[]{8f, 8f}, 0f) : null;
+        gridPaint.setPathEffect(effect);
     }
 
     public void setImage(Picture image) {
@@ -77,7 +143,7 @@ public class MapLayer extends MapBaseLayer {
         float heightRatio = viewHeight / imageHeight;
 
         Log.i(TAG, "widthRatio:" + widthRatio);
-        Log.i(TAG, "widthRatio:" + heightRatio);
+        Log.i(TAG, "heightRatio:" + heightRatio);
 
         if (widthRatio * imageHeight <= viewHeight) {
             return widthRatio;
@@ -99,10 +165,58 @@ public class MapLayer extends MapBaseLayer {
         canvas.setMatrix(currentMatrix);
         if (image != null) {
             canvas.drawPicture(image);
+            if (showGrid) {
+                drawGrid(canvas);
+            }
         }
         canvas.restore();
     }
 
+    private void drawGrid(Canvas canvas) {
+        final float w = mapView.getMapWidth();
+        final float h = mapView.getMapHeight();
+
+        // 画竖线
+        if (gridStepX > 0f) {
+            // 从第一个 >=0 的网格线开始
+            float startX = gridOffsetX;
+            if (startX < 0f) {
+                // 向右推进到非负
+                float n = (float) Math.ceil((-startX) / gridStepX);
+                startX += n * gridStepX;
+            }
+            for (float x = startX; x <= w; x += gridStepX) {
+                if (x / gridStepX % 10 == 0) {
+                    gridPaint.setColor(gridColor);
+                    gridPaint.setStrokeWidth(gridStroke * 3);
+                } else {
+                    gridPaint.setColor(gridSecColor);
+                    gridPaint.setStrokeWidth(gridStroke);
+                }
+                canvas.drawLine(x, 0f, x, h, gridPaint);
+            }
+        }
+
+        // 画横线
+        if (gridStepY > 0f) {
+            float startY = gridOffsetY;
+            if (startY < 0f) {
+                float n = (float) Math.ceil((-startY) / gridStepY);
+                startY += n * gridStepY;
+            }
+            for (float y = startY; y <= h; y += gridStepY) {
+                if (y / gridStepY % 10 == 0) {
+                    gridPaint.setColor(gridColor);
+                    gridPaint.setStrokeWidth(gridStroke * 3);
+                } else {
+                    gridPaint.setColor(gridSecColor);
+                    gridPaint.setStrokeWidth(gridStroke);
+                }
+                canvas.drawLine(0f, y, w, y, gridPaint);
+            }
+        }
+    }
+
     public Picture getImage() {
         return image;
     }