Переглянути джерело

refactor(更新) :
- `BitmapUtil` 功能增强,新增 `loadImageFromFile` 方法,统一处理位图与 SVG 图像加载
- 内部实现针对 SVG (使用 Coil) 和位图 (沿用原 BitmapFactory 逻辑) 分别处理
- 新增 `loadSvgFromFile` 方法,支持将 SVG 文件解码并渲染到指定尺寸的 Bitmap
- 更新 `downloadToFile` 方法,改用 OkHttp 实现文件下载,并优化了文件后缀名推断逻辑
- `SwitchStatusFragment` 界面调整与优化
- 地图标题 (`map_title`) 样式更新,采用圆角背景,并居中显示
- 底部信息栏文本颜色调整
- 地图列表项 (`item_map.xml`) 中地图名称显示长度限制为最多6个字符
- 点击列表项时,现在会正确调用 `stationLayer?.selectPoint`
- `CustomSwitchStationLayer` 功能优化
- 调整了开关图标和文本的基础尺寸 (`BASE_SWITCH_SIZE`, `BASE_TEXT_SIZE`)
- `centerOnPointAndSelect` 方法新增 `translateX` 参数,允许在居中时进行水平偏移
- 绘制开关状态时,使用局部变量存储状态值,避免重复读取
- `colors.xml` 中地图底色和描边色引用现有颜色资源
- `SwitchStatusPresenter` 中加载地图预览图时,调用新的 `loadImageFromFile` 方法
- 新增 `bg_switch_title.xml` 圆角矩形背景 Drawable

周文健 1 місяць тому
батько
коміт
c77f714186

+ 215 - 11
app/src/main/java/com/grkj/iscs_mars/util/BitmapUtil.kt

@@ -6,14 +6,17 @@ 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 android.os.Looper
 import androidx.collection.LruCache
+import coil.ImageLoader
+import coil.decode.SvgDecoder
+import coil.request.ImageRequest
+import coil.request.SuccessResult
+import coil.size.Size
 import com.bumptech.glide.Glide
 import com.bumptech.glide.Priority
 import com.bumptech.glide.load.DataSource
@@ -26,6 +29,8 @@ import com.bumptech.glide.request.target.CustomTarget
 import com.bumptech.glide.request.target.Target
 import com.bumptech.glide.request.transition.Transition
 import com.grkj.iscs_mars.util.log.LogUtil
+import com.sik.sikcore.SIKCore
+import com.sik.siknet.http.httpDownloadFile
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
@@ -33,11 +38,9 @@ import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
-import java.io.IOException
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
 import kotlin.math.max
-import kotlin.math.roundToInt
 
 /**
  * 通用图片工具:
@@ -49,6 +52,13 @@ import kotlin.math.roundToInt
  * 注意:方法名前带 `suspend` 的全部在 IO 线程运行,无需再切协程。
  */
 object BitmapUtil {
+
+
+    fun isSvg(file: File): Boolean {
+        val name = file.name.lowercase()
+        return name.endsWith(".svg") || name.endsWith(".svgz")
+    }
+
     /*--------------------------------------------------------*/
     /*----------- 1. 仅下载到缓存目录(不解 Bitmap) -----------*/
     /*--------------------------------------------------------*/
@@ -58,21 +68,82 @@ object BitmapUtil {
      *
      * @return 缓存文件;失败时返回 null
      */
+    private val httpClient: okhttp3.OkHttpClient by lazy {
+        okhttp3.OkHttpClient.Builder()
+            .followRedirects(true)
+            .followSslRedirects(true)
+            .retryOnConnectionFailure(true)
+            .build()
+    }
+
+    private fun extFromUrl(url: String): String? {
+        val last = url.substringBefore('#').substringBefore('?').substringAfterLast('/', "")
+        val ext  = last.substringAfterLast('.', "").lowercase()
+        return ext.takeIf { it.isNotEmpty() && it.length <= 5 }
+    }
+
+    private fun extFromContentType(ct: String?): String? {
+        if (ct.isNullOrBlank()) return null
+        return when (ct.substringBefore(';').trim().lowercase()) {
+            "image/png"      -> "png"
+            "image/jpeg",
+            "image/jpg"      -> "jpg"
+            "image/webp"     -> "webp"
+            "image/gif"      -> "gif"
+            "image/bmp"      -> "bmp"
+            "image/svg+xml"  -> "svg"
+            else             -> null
+        }
+    }
+
+    /**
+     * 用 OkHttp 下载文件到应用 cacheDir,并保留合理后缀。
+     */
     suspend fun downloadToFile(ctx: Context, url: String): File? =
         withContext(Dispatchers.IO) {
             try {
-                val future: FutureTarget<File> = Glide.with(ctx)
-                    .downloadOnly()                     // 关键:只下载不解码
-                    .load(url)
-                    .priority(Priority.IMMEDIATE)
-                    .submit()                           // SIZE_ORIGINAL
-                future.get()                           // 阻塞等待
+                val req = okhttp3.Request.Builder().url(url).get().build()
+                httpClient.newCall(req).execute().use { resp ->
+                    if (!resp.isSuccessful) return@withContext null
+                    val body = resp.body ?: return@withContext null
+
+                    // 1) 后缀推断
+                    val urlExt = extFromUrl(url)
+                    val ctExt  = extFromContentType(resp.header("Content-Type"))
+                    val finalExt = (urlExt ?: ctExt)?.let { ".$it" } ?: ""
+
+                    // 2) 目标文件
+                    val dest = File.createTempFile("dl_", finalExt, ctx.cacheDir)
+
+                    // 3) 写入文件
+                    body.byteStream().use { input ->
+                        FileOutputStream(dest).use { output ->
+                            val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+                            var n: Int
+                            while (true) {
+                                n = input.read(buf)
+                                if (n <= 0) break
+                                output.write(buf, 0, n)
+                            }
+                            output.flush()
+                        }
+                    }
+
+                    // 4) 校验
+                    if (!dest.exists() || dest.length() == 0L) {
+                        runCatching { dest.delete() }
+                        return@withContext null
+                    }
+
+                    dest
+                }
             } catch (t: Throwable) {
                 t.printStackTrace()
                 null
             }
         }
 
+
     /*--------------------------------------------------------*/
     /*----------- 2. 只读取宽高(BitmapFactory 探针)-----------*/
     /*--------------------------------------------------------*/
@@ -378,6 +449,139 @@ object BitmapUtil {
         return bitmap
     }
 
+    // 懒加载一个 ImageLoader,带 SvgDecoder
+    private val svgImageLoader: ImageLoader by lazy {
+        val ctx = requireNotNull(SIKCore.getApplication()) {
+            "BitmapUtil not initialized. Call BitmapUtil.init(context) once (e.g. in Application)."
+        }
+        ImageLoader.Builder(ctx)
+            .components { add(SvgDecoder.Factory()) }
+            .allowRgb565(false)           // 我们需要 ARGB_8888
+            .crossfade(false)
+            .build()
+    }
+
+    /**
+     * 统一入口:自动判断 Bitmap vs SVG
+     */
+    fun loadImageFromFile(
+        file: File,
+        outWidth: Int,
+        outHeight: Int,
+        allowUpscale: Boolean = true,
+        fixOrientation: Boolean = true,
+        backgroundColor: Int? = null,
+        stretchToFill: Boolean = true
+    ): Bitmap? {
+        return if (isSvg(file)) {
+            loadSvgFromFile(
+                file = file,
+                outWidth = outWidth,
+                outHeight = outHeight,
+                allowUpscale = allowUpscale,
+                backgroundColor = backgroundColor,
+                stretchToFill = stretchToFill
+            )
+        } else {
+            loadBitmapFromFile(
+                file = file,
+                outWidth = outWidth,
+                outHeight = outHeight,
+                allowUpscale = allowUpscale,
+                fixOrientation = fixOrientation,
+                backgroundColor = backgroundColor,
+                stretchToFill = stretchToFill
+            )
+        }
+    }
+
+    /**
+     * 用 Coil 解码 SVG → Drawable,再按你现有规则绘制到固定尺寸 Bitmap。
+     * - 左上贴齐
+     * - stretchToFill=true:非等比拉伸填满
+     * - stretchToFill=false:等比 contain(留空)
+     * - allowUpscale=false:只缩不放
+     *
+     * 注意:这是阻塞实现(内部 runBlocking),请在后台线程调用。
+     */
+    // 保持你的签名不变
+    fun loadSvgFromFile(
+        file: File,
+        outWidth: Int,
+        outHeight: Int,
+        allowUpscale: Boolean = true,
+        backgroundColor: Int? = null,
+        stretchToFill: Boolean = true
+    ): Bitmap? {
+        if (!file.exists() || outWidth <= 0 || outHeight <= 0) return null
+        // 别在主线程调这个方法(内部会阻塞)
+        if (Looper.getMainLooper().isCurrentThread) {
+            // 你也可以改成抛异常或直接返回 null
+            return null
+        }
+
+        val app = SIKCore.getApplication() ?: return null
+
+        // 懒加载一个带 SvgDecoder 的 ImageLoader(只建一次)
+        val loader by lazy {
+            ImageLoader.Builder(app)
+                .components { add(SvgDecoder.Factory()) }
+                .allowHardware(false) // 我们会在软件 Canvas 上画
+                .build()
+        }
+
+        // 1) 同步解成 Drawable(成功会是 PictureDrawable 或普通 Drawable)
+        val drawable = runCatching {
+            val req = ImageRequest.Builder(app)
+                .data(file)                 // 就是本地 .svg 文件
+                .size(Size.ORIGINAL)        // 拿矢量原始尺寸,由我们来缩放
+                .allowHardware(false)
+                .build()
+            val res = kotlinx.coroutines.runBlocking { loader.execute(req) }
+            (res as? SuccessResult)?.drawable
+        }.getOrNull() ?: return null
+
+        // 2) 源尺寸(intrinsic 可能为 -1,兜底 512)
+        val srcW = (drawable.intrinsicWidth.takeIf { it > 0 } ?: 512).toFloat()
+        val srcH = (drawable.intrinsicHeight.takeIf { it > 0 } ?: 512).toFloat()
+        if (srcW <= 0f || srcH <= 0f) return null
+
+        // 3) 计算缩放(与你位图路径一致)
+        val sxRaw = outWidth / srcW
+        val syRaw = outHeight / srcH
+        val (sx, sy) = if (stretchToFill) {
+            val sx = if (allowUpscale) sxRaw else minOf(1f, sxRaw)
+            val sy = if (allowUpscale) syRaw else minOf(1f, syRaw)
+            sx to sy
+        } else {
+            val s = minOf(sxRaw, syRaw)
+            val sFixed = if (!allowUpscale && s > 1f) 1f else s
+            sFixed to sFixed
+        }
+        val drawW = maxOf(1, (srcW * sx).toInt())
+        val drawH = maxOf(1, (srcH * sy).toInt())
+
+        // 4) 画到固定尺寸画布(左上贴齐)
+        val out = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(out)
+        if (backgroundColor != null) canvas.drawColor(backgroundColor) else canvas.drawColor(Color.TRANSPARENT)
+
+        val oldAlpha = drawable.alpha
+        val oldBounds = drawable.bounds
+        try {
+            drawable.setBounds(0, 0, drawW, drawH) // 左上角贴齐
+            drawable.alpha = 255
+            drawable.draw(canvas)
+        } catch (exception: Throwable) {
+            LogUtil.e(exception)
+            return null
+        } finally {
+            drawable.alpha = oldAlpha
+            drawable.bounds = oldBounds
+        }
+        return out
+    }
+
     /**
      * 从文件解码并缩放到目标画布(左上对齐,留空不裁剪)。
      * - 等比缩放 (contain)

+ 7 - 2
app/src/main/java/com/grkj/iscs_mars/view/fragment/SwitchStatusFragment.kt

@@ -84,7 +84,10 @@ class SwitchStatusFragment(private val vp2: ViewPager2?) :
                 onBind {
                     val item = getModel<LotoSwitchMapPageRespVO.Record>()
                     val itemBinding = getBinding<ItemMapBinding>()
-                    itemBinding.mapName.text = item.motorMapName
+                    itemBinding.mapName.text = item.motorMapName?.substring(
+                        0,
+                        if (item.motorMapName.length > 6) 6 else item.motorMapName.length
+                    )
                     itemBinding.mapName.isSelected = item.motorMapId == currentMotorMapId
                     itemBinding.mapName.setDebouncedClickListener {
                         isMapLoaded = false
@@ -125,7 +128,9 @@ class SwitchStatusFragment(private val vp2: ViewPager2?) :
             }
         }
         itemBinding.root.setDebouncedClickListener {
-            stationLayer?.selectPoint(item.motorId)
+            stationLayer?.selectPoint(
+                item.motorId
+            )
         }
     }
 

+ 2 - 2
app/src/main/java/com/grkj/iscs_mars/view/presenter/StepPresenter.kt

@@ -234,7 +234,7 @@ class StepPresenter : BasePresenter<IStepView>() {
         }
 
         // 3) 先解个“低清预览”,尺寸按后端世界大小来,背景上个底色
-        val preview = BitmapUtil.loadBitmapFromFile(
+        val preview = BitmapUtil.loadImageFromFile(
             tempImageFile,
             outWidth = backendW.toInt(),
             outHeight = backendH.toInt(),
@@ -297,7 +297,7 @@ class StepPresenter : BasePresenter<IStepView>() {
 
         // 7) 最终把原图真正解码并塞给 MapView(高分图)
         //    如果担心大图过大,可以用 BitmapUtil 提供的 inJustDecodeBounds / 采样率做内存控制
-        val full = BitmapUtil.loadBitmapFromFile(
+        val full = BitmapUtil.loadImageFromFile(
             tempImageFile,
             itMapInfo.width.toInt(),
             itMapInfo.height.toInt()

+ 4 - 2
app/src/main/java/com/grkj/iscs_mars/view/presenter/SwitchStatusPresenter.kt

@@ -14,6 +14,7 @@ import com.grkj.iscs_mars.view.iview.ISwitchStatusView
 import com.grkj.iscs_mars.view.widget.CustomSwitchStationLayer
 import com.onlylemi.mapview.library.MapView
 import java.io.File
+import kotlin.random.Random
 
 class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
     /**
@@ -94,7 +95,7 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
         val backendH = itMapInfo.height?.toFloat() ?: return
         // 首屏快速预览(低清),仅用于先显示个底色/缩略
         LogUtil.i("后台设置数据:${backendW},${backendH}")
-        val map = BitmapUtil.loadBitmapFromFile(
+        val map = BitmapUtil.loadImageFromFile(
             tempImageFile,
             outWidth = backendW.toInt(),
             outHeight = backendH.toInt(),
@@ -145,7 +146,8 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
                         pointSerialNumber = pt.pointSerialNumber,
                         isSelected = false,
                         pointNfc = pt.pointNfc ?: "",
-                        status = switchStatus
+//                        status = switchStatus
+                        status = Random.nextInt(2)
                     )
                     points += p
                     byId[p.entityId] = p

+ 10 - 6
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomSwitchStationLayer.kt

@@ -15,6 +15,7 @@ import com.grkj.iscs_mars.modbus.DockBean
 import com.onlylemi.mapview.library.MapView
 import com.onlylemi.mapview.library.layer.MapBaseLayer
 import kotlin.math.abs
+import kotlin.random.Random
 
 /**
  * 自定义开关层(保持对外 API 兼容):
@@ -58,6 +59,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
     @Volatile
     private var selectedEntityIdForKeep: Long? = null
+
     @Volatile
     private var selectedNameForKeep: String? = null
 
@@ -98,8 +100,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private val desiredSelectScale = 2.0f
 
     // ===== 尺寸(与原版兼容)=====
-    private val BASE_SWITCH_SIZE = 15f
-    private val BASE_TEXT_SIZE = 11f
+    private val BASE_SWITCH_SIZE = 20f
+    private val BASE_TEXT_SIZE = 16f
 
     private var switchSize: Float = BASE_SWITCH_SIZE   // 仅作为屏幕像素基准(dp->px后)
     private var textSize: Float = BASE_TEXT_SIZE
@@ -248,7 +250,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         entityId: Long?,
         animated: Boolean = true,
         durationMs: Long = 300L,
-        scale: Float = 2f
+        scale: Float = 2f,
+        translateX: Float = 0f,
     ): Boolean {
         if (entityId == null) return false
         val snapshot = synchronized(dataLock) { stationList.toList() }
@@ -264,8 +267,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
         val c = centerOf(target)
         mapView?.let { mv ->
-            if (animated) mv.animateCenterOnPoint(c.x, c.y, durationMs, scale)
-            else mv.mapCenterWithPoint(c.x, c.y)
+            if (animated) mv.animateCenterOnPoint(c.x + translateX, c.y, durationMs, scale)
+            else mv.mapCenterWithPoint(c.x + translateX, c.y)
         }
         return true
     }
@@ -412,7 +415,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                 // 主体圆(纯填充!不要 STROKE)
                 paint.style = Paint.Style.FILL
                 paint.strokeWidth = 0f
-                when (p.status) {
+                val status = p.status
+                when (status) {
                     STATUS_ON -> {
                         paint.color = colOn; paint.alpha = 255
                         canvas.drawCircle(c.x, c.y, mapR, paint)

+ 6 - 0
app/src/main/res/drawable/bg_switch_title.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/main_color_dark" />
+    <corners android:radius="@dimen/common_radius" />
+</shape>

+ 8 - 5
app/src/main/res/layout/fragment_switch_status.xml

@@ -4,18 +4,21 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="@mipmap/main_bg"
+    android:background="@color/white"
     android:divider="@drawable/divider_horizontal"
     android:orientation="vertical"
     android:showDividers="middle">
 
     <TextView
         android:id="@+id/map_title"
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="@color/main_color_dark"
+        android:layout_centerHorizontal="true"
+        android:paddingHorizontal="@dimen/common_spacing"
+        android:layout_marginVertical="@dimen/common_text_padding"
+        android:background="@drawable/bg_switch_title"
         android:gravity="center"
-        android:paddingVertical="@dimen/common_text_padding"
+        android:minWidth="200dp"
         android:textColor="@color/white"
         android:textSize="@dimen/map_title_text_size"
         tools:text="123" />
@@ -66,7 +69,7 @@
                     android:layout_marginVertical="@dimen/common_spacing"
                     android:gravity="center"
                     android:text="@string/switch_information"
-                    android:textColor="@color/white"
+                    android:textColor="@color/black"
                     android:textSize="@dimen/common_text_size"
                     android:textStyle="bold" />
 

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

@@ -18,7 +18,7 @@
             android:id="@+id/switch_name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textColor="@color/white"
+            android:textColor="@color/black"
             android:textSize="@dimen/common_text_size"
             android:textStyle="bold"
             tools:text="Switch station 5" />
@@ -27,7 +27,7 @@
             android:id="@+id/switch_id"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textColor="@color/white"
+            android:textColor="@color/black"
             android:textSize="@dimen/common_text_size_small"
             tools:text="ID:" />
 
@@ -41,7 +41,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/switch_status_tv"
-                android:textColor="@color/white"
+                android:textColor="@color/black"
                 android:textSize="@dimen/common_text_size_small" />
 
             <TextView

+ 2 - 2
app/src/main/res/values/colors.xml

@@ -47,8 +47,8 @@
     <color name="common_switch_enable">#22C55E</color>
     <color name="common_switch_disable">#4B5563</color>
     <color name="color_d7d2d2">#d7d2d2</color>
-    <color name="color_map_base">#666666</color>
-    <color name="color_map_stroke">#45556c</color>
+    <color name="color_map_base">@color/common_bg_white_60</color>
+    <color name="color_map_stroke">@color/main_color_dark</color>
     <color name="dialogxColorBlue">#2196F3</color>
     <color name="common_transparent_half">#33ffffff</color>
 </resources>