Forráskód Böngészése

refactor(重构): 登录流程与地图渲染引擎

- **登录与开关状态页面重构**
  - 将 `SwitchStatusActivity` 重构为 `SwitchStatusFragment`,并与新的 `LoginFragment` 整合到 `LoginActivity` 的 `ViewPager2` 中,实现一体化展示。
  - `SwitchStatusFragment` 在不可见时(如切换到登录页)会自动暂停地图渲染,以优化性能。
  - `SwitchStatusFragment` 现在会尝试根据设备的序列号默认加载对应的地图。

- **MapView 渲染性能与机制重构**
  - 引入独立渲染线程和离屏双缓冲(Off-screen Double Buffering)机制,大幅提升渲染性能和流畅度。
  - 通过 `Choreographer` 实现与 V-Sync 的同步渲染,使动画效果更平滑。
  - 优化绘制流程:各图层在世界坐标系中绘制到离屏缓冲,视图矩阵仅在最终合成时应用一次。
  - 所有地图图层(`CustomStationLayer`, `CustomSwitchStationLayer`, `CustomMarkLayer`)均已适配新的渲染管线,调用 `refreshWorld()` 以更新内容。
  - 新增 `PausableLayer` 接口,允许图层根据 `MapView` 的可见性(`onHostVisibilityChanged`)暂停或恢复动画。

- **API 及数据模型调整**
  - 更新开关状态上报接口 `updateSwitchList`:请求体 `SwitchListReqVO` 增加 `lotoSerialNumber` 字段。
  - 状态上报后,通过 `MSG_EVENT_SWITCH_COLLECTION_UPDATE_RESULT` 事件广播操作结果,而非简单刷新。
  - `MotorMapInfoRespVO` 数据模型中的 `switchStatus` 字段重命名为 `status` 以保持统一。

- **其他优化与调整**
  - 从 `HomeActivity` 的菜单中移除 "开关状态" 的独立入口。
  - 新增 `DisplayUtils` 工具类,用于统一获取屏幕旋转角度,并简化了 `ArcSoftUtil` 中相机初始化的相关逻辑。
周文健 1 hónapja
szülő
commit
e1e9eb66de
30 módosított fájl, 603 hozzáadás és 275 törlés
  1. 1 1
      app/src/main/AndroidManifest.xml
  2. 9 1
      app/src/main/java/com/grkj/iscs_mars/BusinessManager.kt
  3. 2 0
      app/src/main/java/com/grkj/iscs_mars/model/eventmsg/MsgEventConstants.kt
  4. 1 0
      app/src/main/java/com/grkj/iscs_mars/model/vo/hardware/SwitchListReqVO.kt
  5. 1 2
      app/src/main/java/com/grkj/iscs_mars/model/vo/map/LotoSwitchMapPageRespVO.kt
  6. 1 1
      app/src/main/java/com/grkj/iscs_mars/model/vo/map/MotorMapInfoRespVO.kt
  7. 3 7
      app/src/main/java/com/grkj/iscs_mars/util/ArcSoftUtil.kt
  8. 35 0
      app/src/main/java/com/grkj/iscs_mars/util/DisplayUtils.kt
  9. 3 3
      app/src/main/java/com/grkj/iscs_mars/util/NetApi.kt
  10. 4 18
      app/src/main/java/com/grkj/iscs_mars/view/activity/HomeActivity.kt
  11. 7 76
      app/src/main/java/com/grkj/iscs_mars/view/activity/LoginActivity.kt
  12. 15 0
      app/src/main/java/com/grkj/iscs_mars/view/adapter/TwoFragmentAdapter.kt
  13. 0 1
      app/src/main/java/com/grkj/iscs_mars/view/dialog/FaceCaptureDialog.kt
  14. 1 7
      app/src/main/java/com/grkj/iscs_mars/view/dialog/LoginDialog.kt
  15. 112 0
      app/src/main/java/com/grkj/iscs_mars/view/fragment/LoginFragment.kt
  16. 72 44
      app/src/main/java/com/grkj/iscs_mars/view/fragment/SwitchStatusFragment.kt
  17. 1 1
      app/src/main/java/com/grkj/iscs_mars/view/fragment/WorkshopFragment.kt
  18. 8 0
      app/src/main/java/com/grkj/iscs_mars/view/presenter/LoginPresenter.kt
  19. 1 1
      app/src/main/java/com/grkj/iscs_mars/view/presenter/SwitchStatusPresenter.kt
  20. 1 1
      app/src/main/java/com/grkj/iscs_mars/view/widget/CustomMarkLayer.kt
  21. 2 2
      app/src/main/java/com/grkj/iscs_mars/view/widget/CustomStationLayer.kt
  22. 16 10
      app/src/main/java/com/grkj/iscs_mars/view/widget/CustomSwitchStationLayer.kt
  23. 224 59
      app/src/main/java/com/onlylemi/mapview/library/MapView.java
  24. 5 27
      app/src/main/res/layout/activity_login.xml
  25. 50 0
      app/src/main/res/layout/fragment_login.xml
  26. 24 13
      app/src/main/res/layout/fragment_switch_status.xml
  27. 1 0
      app/src/main/res/values-en/strings.xml
  28. 1 0
      app/src/main/res/values-zh/strings.xml
  29. 1 0
      app/src/main/res/values/dimens.xml
  30. 1 0
      app/src/main/res/values/strings.xml

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -52,7 +52,7 @@
             android:exported="false"
             android:windowSoftInputMode="adjustPan|adjustResize" />
         <activity
-            android:name=".view.activity.SwitchStatusActivity"
+            android:name=".view.fragment.SwitchStatusFragment"
             android:exported="false"
             android:windowSoftInputMode="adjustPan|adjustResize" />
         <activity

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

@@ -230,8 +230,10 @@ object BusinessManager {
 
                 MSG_EVENT_SWITCH_COLLECTION_UPDATE -> {
                     ThreadUtils.runOnIO {
+                        val lotoSerialNumber = SIKCore.getApplication().serialNo()
                         val switchListReqVOS = ModBusController.getSwitchData().map {
                             SwitchListReqVO(
+                                lotoSerialNumber,
                                 it.idx.toString(),
                                 if (it.enabled) "1" else "0",
                                 TimeUtils.nowString(TimeUtils.DEFAULT_DATE_HOUR_MIN_SEC_FORMAT)
@@ -239,6 +241,12 @@ object BusinessManager {
                         }
                         NetApi.updateSwitchList(switchListReqVOS) {
                             LogUtil.d("开关更新完成")
+                            sendEventMsg(
+                                MsgEvent(
+                                    MsgEventConstants.MSG_EVENT_SWITCH_COLLECTION_UPDATE_RESULT,
+                                    it
+                                )
+                            )
                         }
                     }
                 }
@@ -829,7 +837,7 @@ object BusinessManager {
                 val lockMap = withContext(Dispatchers.Default) {
                     ModBusController.getLocks(
                         needLockCount,
-                        locksPage?.records?.filter { it.hardwareId != null && it.hardwareId== SPUtils.getCabinetId() }
+                        locksPage?.records?.filter { it.hardwareId != null && it.hardwareId == SPUtils.getCabinetId() }
                             ?.mapNotNull { it.lockNfc } ?: mutableListOf(),
                         slotsPage?.records?.filter {
                             it.slotType == slotTypeList.find { d -> d.dictLabel == "锁" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue

+ 2 - 0
app/src/main/java/com/grkj/iscs_mars/model/eventmsg/MsgEventConstants.kt

@@ -24,4 +24,6 @@ object MsgEventConstants {
 
     // ------------------------------ 开关量采集更新 1-006-000 ------------------------------
     const val MSG_EVENT_SWITCH_COLLECTION_UPDATE = 1_006_000 //开关量采集更新
+
+    const val MSG_EVENT_SWITCH_COLLECTION_UPDATE_RESULT = 1_006_001 //开关量采集更新结果
 }

+ 1 - 0
app/src/main/java/com/grkj/iscs_mars/model/vo/hardware/SwitchListReqVO.kt

@@ -4,6 +4,7 @@ package com.grkj.iscs_mars.model.vo.hardware
  * 开关状态请求实体
  */
 data class SwitchListReqVO(
+    val lotoSerialNumber: String,
     val pointSerialNumber: String,
     val switchStatus: String?,
     val switchLastUpdateTime: String?

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

@@ -12,9 +12,8 @@ data class LotoSwitchMapPageRespVO(
 ) {
     data class Record(
         val motorMapId: Long?,
-
         val lotoId: String?,
-
         val motorMapName: String?,
+        val lotoSerialNumber: String?
     )
 }

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/model/vo/map/MotorMapInfoRespVO.kt

@@ -33,6 +33,6 @@ data class MotorMapInfoRespVO(
 
         val pointSerialNumber: String?,
 
-        val switchStatus: String?,
+        val status: String?,
     )
 }

+ 3 - 7
app/src/main/java/com/grkj/iscs_mars/util/ArcSoftUtil.kt

@@ -27,6 +27,7 @@ import com.grkj.iscs_mars.view.activity.test.face.arcsoft.CameraHelper
 import com.grkj.iscs_mars.view.activity.test.face.arcsoft.CameraListener
 import com.grkj.iscs_mars.view.activity.test.face.arcsoft.NV21ToBitmap
 import com.grkj.iscs_mars.view.widget.FaceOverlayView
+import com.sik.sikcore.SIKCore
 import com.sik.sikcore.extension.toJson
 import com.sik.sikcore.file.FileStorageUtils
 import java.io.File
@@ -168,15 +169,11 @@ object ArcSoftUtil {
 
     fun initCamera(
         context: Context,
-        windowManager: WindowManager,
         preview: View,
         faceOverlayView: FaceOverlayView?,
         needCheckCenter: Boolean = false,
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
-        val metrics = DisplayMetrics()
-        windowManager.defaultDisplay.getMetrics(metrics)
-
         val cameraListener: CameraListener = object : CameraListener {
             override fun onCameraOpened(
                 camera: Camera,
@@ -272,7 +269,7 @@ object ArcSoftUtil {
         }
         cameraHelper = CameraHelper.Builder()
             .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(windowManager.defaultDisplay.rotation)
+            .rotation(DisplayUtils.getRotation(SIKCore.getApplication()))
             .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT)
             .isMirror(false)
             .previewOn(preview)
@@ -284,12 +281,11 @@ object ArcSoftUtil {
 
     fun start(
         context: Context,
-        windowManager: WindowManager,
         preview: View,
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
         initEngine(context)
-        initCamera(context, windowManager, preview, null, false, callBack)
+        initCamera(context, preview, null, false, callBack)
     }
 
     fun stop() {

+ 35 - 0
app/src/main/java/com/grkj/iscs_mars/util/DisplayUtils.kt

@@ -0,0 +1,35 @@
+package com.grkj.iscs_mars.util
+
+import android.app.Activity
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.Display
+import android.view.Surface
+import android.view.WindowManager
+
+object DisplayUtils {
+
+    /**
+     * 获取默认屏幕的 rotation(0, 90, 180, 270)
+     */
+    fun getRotation(context: Context): Int {
+        return when {
+            // Android 11+ 有 Activity.display
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && context is Activity -> {
+                context.display?.rotation ?: Surface.ROTATION_0
+            }
+            // Android 4.0+ ~ Android 10 推荐用 WindowManager
+            Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> {
+                val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+                @Suppress("DEPRECATION")
+                wm.defaultDisplay.rotation
+            }
+            else -> {
+                // 兜底:DisplayManager 取 Display.DEFAULT_DISPLAY
+                val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+                dm.getDisplay(Display.DEFAULT_DISPLAY)?.rotation ?: Surface.ROTATION_0
+            }
+        }
+    }
+}

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

@@ -1368,7 +1368,7 @@ object NetApi {
     /**
      * 批量更新开关状态
      */
-    fun updateSwitchList(switchList: List<SwitchListReqVO>, callBack: (Boolean) -> Unit) {
+    fun updateSwitchList(switchList: List<SwitchListReqVO>, callBack: (Int) -> Unit) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.UPDATE_SWITCH_LIST,
             false,
@@ -1377,9 +1377,9 @@ object NetApi {
             ),
             { res, _, _ ->
                 res?.let {
-                    callBack.invoke(true)
+                    callBack.invoke(getBaseVO<Int>(it)?.data ?: 0)
                 } ?: run {
-                    callBack.invoke(false)
+                    callBack.invoke(0)
                 }
             }, isGet = false, isAuth = false
         )

+ 4 - 18
app/src/main/java/com/grkj/iscs_mars/view/activity/HomeActivity.kt

@@ -35,6 +35,7 @@ import com.grkj.iscs_mars.view.fragment.DockTestFragment
 import com.grkj.iscs_mars.view.fragment.ExceptionReportFragment
 import com.grkj.iscs_mars.view.fragment.JobManagementFragment
 import com.grkj.iscs_mars.view.fragment.SettingFragment
+import com.grkj.iscs_mars.view.fragment.SwitchStatusFragment
 import com.grkj.iscs_mars.view.fragment.SystemSettingFragment
 import com.grkj.iscs_mars.view.iview.IHomeView
 import com.grkj.iscs_mars.view.presenter.HomePresenter
@@ -113,12 +114,6 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
                 DeviceStatusFragment()
             )
         )
-        mMenuList.add(
-            Menu(
-                getString(R.string.switch_status),
-                R.mipmap.icon_menu_switch
-            )
-        )
         mMenuList.add(
             Menu(
                 getString(R.string.exception_report),
@@ -150,18 +145,9 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
                     holder.setText(R.id.tv_name, data.title)
                     holder.getView<ImageView>(R.id.iv_icon).setImageResource(data.icon!!)
                     holder.setOnClickListener(R.id.root) {
-                        if (data.title == getString(R.string.switch_status)) {
-                            startActivity(
-                                Intent(
-                                    this@HomeActivity,
-                                    SwitchStatusActivity::class.java
-                                )
-                            )
-                        } else {
-                            mBinding?.itemSetting?.root?.setBackgroundColor(0)
-                            mBinding?.vp?.currentItem = position
-                            notifyDataSetChanged()
-                        }
+                        mBinding?.itemSetting?.root?.setBackgroundColor(0)
+                        mBinding?.vp?.currentItem = position
+                        notifyDataSetChanged()
                     }
                     holder.setBackgroundColor(
                         R.id.root,

+ 7 - 76
app/src/main/java/com/grkj/iscs_mars/view/activity/LoginActivity.kt

@@ -4,41 +4,24 @@ import android.content.Intent
 import android.graphics.Bitmap
 import android.view.InputDevice
 import android.view.KeyEvent
-import android.widget.ImageView
-import com.arcsoft.face.ActiveFileInfo
-import com.arcsoft.face.FaceEngine
-import com.arcsoft.face.model.ActiveDeviceInfo
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.MyApplication.Companion.cronJobManager
-import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.databinding.ActivityLoginBinding
-import com.grkj.iscs_mars.extentions.serialNo
 import com.grkj.iscs_mars.extentions.toByteArrays
 import com.grkj.iscs_mars.extentions.toHexStrings
 import com.grkj.iscs_mars.modbus.ModBusController
 import com.grkj.iscs_mars.model.vo.user.UserInfoRespVO
-import com.grkj.iscs_mars.util.AppUtils
-import com.grkj.iscs_mars.util.ArcSoftUtil
 import com.grkj.iscs_mars.util.FingerprintUtil
-import com.grkj.iscs_mars.util.KeyboardUtils
 import com.grkj.iscs_mars.util.log.LogUtil
+import com.grkj.iscs_mars.view.adapter.TwoFragmentAdapter
 import com.grkj.iscs_mars.view.base.BaseMvpActivity
-import com.grkj.iscs_mars.view.dialog.LoginDialog
-import com.grkj.iscs_mars.view.dialog.UrlConfigDialog
 import com.grkj.iscs_mars.view.iview.ILoginView
 import com.grkj.iscs_mars.view.presenter.LoginPresenter
 import com.sik.sikcore.SIKCore
-import com.sik.sikcore.extension.setDebouncedClickListener
-import com.sik.sikcore.file.FileStorageUtils
-import com.sik.sikcore.file.FileUtils
-import com.sik.sikcore.shell.ShellUtils
 import com.tencent.mmkv.MMKV
-import com.zhy.adapter.recyclerview.CommonAdapter
-import com.zhy.adapter.recyclerview.base.ViewHolder
 
 class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginBinding>() {
 
-    private var cardLoginDialog: LoginDialog? = null
     private var cardNo = ""
 
     override val viewBinding: ActivityLoginBinding
@@ -47,69 +30,16 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
     override fun initView() {
         MMKV.initialize(SIKCore.getApplication())
         cronJobManager.bindService()
-        mBinding?.tvVersion?.post {
-            mBinding?.tvVersion?.text = "v${AppUtils.getPkgVerName(this)}-${serialNo()}"
-        }
-        mBinding?.mainTitle?.setDebouncedClickListener {
-            val activeDeviceInfo = ActiveDeviceInfo()
-            FaceEngine.getActiveDeviceInfo(this, activeDeviceInfo)
-            ShellUtils.execCmd("echo ${activeDeviceInfo.deviceInfo} > /sdcard/iscs/activeDeviceInfo.txt")
-        }
-        mBinding?.tvVersion?.setDebouncedClickListener {
-            UrlConfigDialog(this).show()
-        }
-
-        mBinding?.main?.setBackgroundResource(R.mipmap.login_bg)
-
-        val pairList = mutableListOf(
-            Pair(getString(R.string.login_face), R.mipmap.login_face),
-            Pair(getString(R.string.login_fingerprint), R.mipmap.login_fingerprint),
-            Pair(getString(R.string.login_card), R.mipmap.login_card),
-            Pair(getString(R.string.login_account), R.mipmap.login_account)
-        )
-
-        mBinding?.rvType?.adapter =
-            object : CommonAdapter<Pair<String, Int>>(this, R.layout.item_rv_login, pairList) {
-                override fun convert(holder: ViewHolder, pair: Pair<String, Int>, position: Int) {
-                    holder.setVisible(R.id.iv_dot, position == 1 || position == 2)
-                    holder.setText(R.id.tv_name, pair.first)
-                    holder.getView<ImageView>(R.id.iv_icon).setImageResource(pair.second)
-                    holder.setOnClickListener(R.id.root) {
-                        if (position == 0 || position == 3) {
-                            showLoginDialog(position)
-                        }
-                    }
-                }
-            }
+        mBinding?.vp2?.adapter = TwoFragmentAdapter(this, mBinding?.vp2)
+        mBinding?.vp2?.offscreenPageLimit = 1 // 只两页,提前缓存,不卡顿
+        mBinding?.vp2?.isUserInputEnabled = false
 
         // TODO 只适配armeabi-v7a
-        BusinessManager.connectDock(true)
-    }
 
-    /**
-     * @param loginType 0:人脸 1:指纹 2:工卡 3:账号
-     */
-    private fun showLoginDialog(loginType: Int) {
-        cardLoginDialog ?: run {
-            LogUtil.i("创建Swipe dialog : ${presenter == null}")
-            cardLoginDialog = LoginDialog(presenter, this) { isSuccess, userInfoRespVO ->
-                if (isSuccess) {
-                    goHome(userInfoRespVO)
-                }
-            }
-        }
-        cardLoginDialog?.setOnDismissListener {
-            LogUtil.i("隐藏软键盘")
-            KeyboardUtils.hideSoftKeyboard()
-        }
-        cardLoginDialog?.showByType(loginType)
-    }
-
-    override fun onResume() {
-        super.onResume()
         if (ModBusController.isRunning() != true) {
             BusinessManager.connectDock(true)
         }
+        presenter?.downloadLicense()
         presenter?.registerListener()
         FingerprintUtil.init(this)
         FingerprintUtil.start()
@@ -122,6 +52,7 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
                 }
             }
         })
+        BusinessManager.connectDock(true)
     }
 
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -152,11 +83,11 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
         }
         startActivity(intent)
         BusinessManager.submitKeyData(this@LoginActivity)
+        finish()
     }
 
     override fun onStop() {
         super.onStop()
-        cardLoginDialog?.dismiss()
         presenter?.unregisterListener()
         cardNo = ""
         FingerprintUtil.stop()

+ 15 - 0
app/src/main/java/com/grkj/iscs_mars/view/adapter/TwoFragmentAdapter.kt

@@ -0,0 +1,15 @@
+package com.grkj.iscs_mars.view.adapter
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import com.grkj.iscs_mars.view.fragment.LoginFragment
+import com.grkj.iscs_mars.view.fragment.SwitchStatusFragment
+
+class TwoFragmentAdapter(activity: FragmentActivity, val vp2: ViewPager2?) : FragmentStateAdapter(activity) {
+    override fun getItemCount() = 2
+    override fun createFragment(position: Int): Fragment =
+        if (position == 0) SwitchStatusFragment(vp2) else LoginFragment(vp2)
+
+}

+ 0 - 1
app/src/main/java/com/grkj/iscs_mars/view/dialog/FaceCaptureDialog.kt

@@ -83,7 +83,6 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
         ArcSoftUtil.initEngine(context)
         ArcSoftUtil.initCamera(
             context,
-            ctx.windowManager,
             mBinding?.preview!!,
             mBinding?.faceOverlayView!!,
             true

+ 1 - 7
app/src/main/java/com/grkj/iscs_mars/view/dialog/LoginDialog.kt

@@ -4,11 +4,8 @@ import android.content.Context
 import android.graphics.Bitmap
 import android.view.InputDevice
 import android.view.KeyEvent
-import android.view.MotionEvent
 import android.view.View
 import android.view.WindowManager
-import android.view.inputmethod.InputMethodManager
-import android.widget.EditText
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.databinding.DialogLoginBinding
@@ -20,15 +17,13 @@ import com.grkj.iscs_mars.util.FingerprintUtil
 import com.grkj.iscs_mars.util.KeyboardUtils
 import com.grkj.iscs_mars.util.ToastUtils
 import com.grkj.iscs_mars.util.log.LogUtil
-import com.grkj.iscs_mars.view.base.BaseActivity
 import com.grkj.iscs_mars.view.base.BaseDialog
 import com.grkj.iscs_mars.view.presenter.LoginPresenter
-import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.thread.ThreadUtils
 
 class LoginDialog(
     val presenter: LoginPresenter?,
-    val ctx: BaseActivity<*>,
+    val ctx: Context,
     private var callBack: ((Boolean, UserInfoRespVO?) -> Unit)? = null
 ) :
     BaseDialog<DialogLoginBinding>(ctx) {
@@ -138,7 +133,6 @@ class LoginDialog(
     private fun startFace() {
         ArcSoftUtil.initCamera(
             context,
-            ctx.windowManager,
             mBinding?.preview!!,
             null,
             false

+ 112 - 0
app/src/main/java/com/grkj/iscs_mars/view/fragment/LoginFragment.kt

@@ -0,0 +1,112 @@
+package com.grkj.iscs_mars.view.fragment
+
+import android.content.Intent
+import android.widget.ImageView
+import androidx.viewpager2.widget.ViewPager2
+import com.arcsoft.face.FaceEngine
+import com.arcsoft.face.model.ActiveDeviceInfo
+import com.grkj.iscs_mars.BusinessManager
+import com.grkj.iscs_mars.R
+import com.grkj.iscs_mars.databinding.FragmentLoginBinding
+import com.grkj.iscs_mars.extentions.serialNo
+import com.grkj.iscs_mars.model.vo.user.UserInfoRespVO
+import com.grkj.iscs_mars.util.AppUtils
+import com.grkj.iscs_mars.util.KeyboardUtils
+import com.grkj.iscs_mars.util.log.LogUtil
+import com.grkj.iscs_mars.view.activity.HomeActivity
+import com.grkj.iscs_mars.view.base.BaseMvpFragment
+import com.grkj.iscs_mars.view.dialog.LoginDialog
+import com.grkj.iscs_mars.view.dialog.UrlConfigDialog
+import com.grkj.iscs_mars.view.iview.ILoginView
+import com.grkj.iscs_mars.view.presenter.LoginPresenter
+import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikcore.shell.ShellUtils
+import com.zhy.adapter.recyclerview.CommonAdapter
+import com.zhy.adapter.recyclerview.base.ViewHolder
+
+/**
+ * 登录界面
+ */
+class LoginFragment(private val vp2: ViewPager2?) : BaseMvpFragment<ILoginView, LoginPresenter, FragmentLoginBinding>() {
+    private var cardLoginDialog: LoginDialog? = null
+    override fun initPresenter(): LoginPresenter {
+        return LoginPresenter()
+    }
+
+    override val viewBinding: FragmentLoginBinding
+        get() = FragmentLoginBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        mBinding?.tvVersion?.post {
+            mBinding?.tvVersion?.text = "v${AppUtils.getPkgVerName(requireContext())}-${requireContext().serialNo()}"
+        }
+        mBinding?.cbMotor?.setDebouncedClickListener {
+            vp2?.currentItem = 0
+        }
+        mBinding?.mainTitle?.setDebouncedClickListener {
+            val activeDeviceInfo = ActiveDeviceInfo()
+            FaceEngine.getActiveDeviceInfo(requireContext(), activeDeviceInfo)
+            ShellUtils.execCmd("echo ${activeDeviceInfo.deviceInfo} > /sdcard/iscs/activeDeviceInfo.txt")
+        }
+        mBinding?.tvVersion?.setDebouncedClickListener {
+            UrlConfigDialog(requireContext()).show()
+        }
+
+        mBinding?.main?.setBackgroundResource(R.mipmap.login_bg)
+
+        val pairList = mutableListOf(
+            Pair(getString(R.string.login_face), R.mipmap.login_face),
+            Pair(getString(R.string.login_fingerprint), R.mipmap.login_fingerprint),
+            Pair(getString(R.string.login_card), R.mipmap.login_card),
+            Pair(getString(R.string.login_account), R.mipmap.login_account)
+        )
+
+        mBinding?.rvType?.adapter =
+            object : CommonAdapter<Pair<String, Int>>(
+                requireContext(),
+                R.layout.item_rv_login,
+                pairList
+            ) {
+                override fun convert(holder: ViewHolder, pair: Pair<String, Int>, position: Int) {
+                    holder.setVisible(R.id.iv_dot, position == 1 || position == 2)
+                    holder.setText(R.id.tv_name, pair.first)
+                    holder.getView<ImageView>(R.id.iv_icon).setImageResource(pair.second)
+                    holder.setOnClickListener(R.id.root) {
+                        if (position == 0 || position == 3) {
+                            showLoginDialog(position)
+                        }
+                    }
+                }
+            }
+    }
+
+    /**
+     * @param loginType 0:人脸 1:指纹 2:工卡 3:账号
+     */
+    private fun showLoginDialog(loginType: Int) {
+        cardLoginDialog ?: run {
+            LogUtil.i("创建Swipe dialog : ${presenter == null}")
+            cardLoginDialog =
+                LoginDialog(presenter, requireContext()) { isSuccess, userInfoRespVO ->
+                    if (isSuccess) {
+                        goHome(userInfoRespVO)
+                    }
+                }
+        }
+        cardLoginDialog?.setOnDismissListener {
+            LogUtil.i("隐藏软键盘")
+            KeyboardUtils.hideSoftKeyboard()
+        }
+        cardLoginDialog?.showByType(loginType)
+    }
+
+    private fun goHome(userInfoRespVO: UserInfoRespVO?) {
+        val intent = Intent(requireContext(), HomeActivity::class.java)
+        if (userInfoRespVO != null) {
+            intent.putExtra("userInfo", userInfoRespVO)
+        }
+        startActivity(intent)
+        BusinessManager.submitKeyData(requireContext())
+        requireActivity().finish()
+    }
+}

+ 72 - 44
app/src/main/java/com/grkj/iscs_mars/view/activity/SwitchStatusActivity.kt → app/src/main/java/com/grkj/iscs_mars/view/fragment/SwitchStatusFragment.kt

@@ -1,10 +1,11 @@
-package com.grkj.iscs_mars.view.activity
+package com.grkj.iscs_mars.view.fragment
 
 import android.view.GestureDetector
 import android.view.Gravity
 import android.view.MotionEvent
-import android.widget.LinearLayout.HORIZONTAL
+import android.widget.LinearLayout
 import androidx.core.view.isVisible
+import androidx.viewpager2.widget.ViewPager2
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
 import com.drake.brv.utils.dividerSpace
@@ -13,17 +14,17 @@ 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.FragmentSwitchStatusBinding
 import com.grkj.iscs_mars.databinding.ItemMapBinding
 import com.grkj.iscs_mars.databinding.ItemSwitchBinding
+import com.grkj.iscs_mars.extentions.serialNo
 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.eventmsg.MsgEventConstants
 import com.grkj.iscs_mars.model.vo.map.LotoSwitchMapPageRespVO
 import com.grkj.iscs_mars.model.vo.map.MotorMapInfoRespVO
 import com.grkj.iscs_mars.util.CommonUtils
 import com.grkj.iscs_mars.util.ToastUtils
-import com.grkj.iscs_mars.util.log.LogUtil
-import com.grkj.iscs_mars.view.base.BaseMvpActivity
+import com.grkj.iscs_mars.view.base.BaseMvpFragment
 import com.grkj.iscs_mars.view.dialog.SwitchInfoDialog
 import com.grkj.iscs_mars.view.iview.ISwitchStatusView
 import com.grkj.iscs_mars.view.presenter.SwitchStatusPresenter
@@ -32,33 +33,35 @@ import com.onlylemi.mapview.library.MapViewListener
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.thread.ThreadUtils
 
-class SwitchStatusActivity :
-    BaseMvpActivity<ISwitchStatusView, SwitchStatusPresenter, ActivitySwitchStatusBinding>() {
+class SwitchStatusFragment(private val vp2: ViewPager2?) :
+    BaseMvpFragment<ISwitchStatusView, SwitchStatusPresenter, FragmentSwitchStatusBinding>() {
     private var stationLayer: CustomSwitchStationLayer? = null
     private lateinit var gestureDetector: GestureDetector
     private lateinit var switchInfoDialog: SwitchInfoDialog
     private var currentMotorMapId = 0L
     private var currentLotoId = ""
+    private var isMapLoaded = false
 
     override fun initPresenter(): SwitchStatusPresenter {
         return SwitchStatusPresenter()
     }
 
-    override val viewBinding: ActivitySwitchStatusBinding
-        get() = ActivitySwitchStatusBinding.inflate(layoutInflater)
+    override val viewBinding: FragmentSwitchStatusBinding
+        get() = FragmentSwitchStatusBinding.inflate(layoutInflater)
 
     override fun initView() {
-        switchInfoDialog = SwitchInfoDialog(this)
+        switchInfoDialog = SwitchInfoDialog(requireContext())
         switchInfoDialog.popupGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
-        mBinding?.cbBack?.setDebouncedClickListener {
-            finish()
+        gestureDetector =
+            GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
+                override fun onDoubleTap(e: MotionEvent): Boolean {
+                    mBinding?.mapview?.currentRotateDegrees = 0f
+                    return super.onDoubleTap(e)
+                }
+            })
+        mBinding?.cbLogin?.setDebouncedClickListener {
+            vp2?.currentItem = 1
         }
-        gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
-            override fun onDoubleTap(e: MotionEvent): Boolean {
-                mBinding?.mapview?.currentRotateDegrees = 0f
-                return super.onDoubleTap(e)
-            }
-        })
         mBinding?.cbShow?.setDebouncedClickListener {
             if (mBinding?.pointList?.isVisible == true) {
                 mBinding?.pointList?.isVisible = false
@@ -68,28 +71,31 @@ class SwitchStatusActivity :
                 mBinding?.cbShow?.rotation = 180f
             }
         }
-        mBinding?.mapview?.setBackgroundColorInt(getColor(R.color.color_map_base))
+        mBinding?.mapview?.setBackgroundColorInt(requireContext().getColor(R.color.color_map_base))
         mBinding?.rvList?.linear()?.dividerSpace(10, DividerOrientation.GRID)?.setup {
             addType<MotorMapInfoRespVO.IsMotorMapPoint>(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.motorMapName
-                itemBinding.mapName.isSelected = item.motorMapId == currentMotorMapId
-                itemBinding.mapName.setDebouncedClickListener {
-                    getMap(item.motorMapId.toString(), item.lotoId.toString())
-                    currentMotorMapId = item.motorMapId ?: 0
-                    currentLotoId = item.lotoId.toString()
-                    adapter.notifyDataSetChanged()
+        mBinding?.mapRv?.linear(LinearLayout.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.motorMapName
+                    itemBinding.mapName.isSelected = item.motorMapId == currentMotorMapId
+                    itemBinding.mapName.setDebouncedClickListener {
+                        isMapLoaded = false
+                        getMap(item.motorMapId.toString(), item.lotoId.toString())
+                        currentMotorMapId = item.motorMapId ?: 0
+                        currentLotoId = item.lotoId.toString()
+                        mBinding?.mapTitle?.text = item.motorMapName
+                        adapter.notifyDataSetChanged()
+                    }
                 }
             }
-        }
         initMap()
     }
 
@@ -101,7 +107,7 @@ class SwitchStatusActivity :
         itemBinding.switchId.text = context.getString(R.string.switch_id, item.motorCode)
         val switchStatus = switchData
             .find { it.idx == item.pointSerialNumber?.toInt() }?.enabled
-            ?: (item.switchStatus == "1")
+            ?: (item.status == "1")
         when (switchStatus) {
             false -> {
                 itemBinding.switchStatus.setBackgroundResource(R.drawable.bg_switch_off)
@@ -125,25 +131,44 @@ class SwitchStatusActivity :
 
     override fun onResume() {
         super.onResume()
+        isMapLoaded = false
+        mBinding?.mapview?.setRenderEnabled(true)    // ✅ 当前页开始渲染
         BusinessManager.mEventBus.observe(this) {
             when (it.code) {
-                MSG_EVENT_SWITCH_COLLECTION_UPDATE -> {
-                    getMap(currentMotorMapId.toString(), currentLotoId)
-                    mBinding?.rvList?.adapter?.notifyDataSetChanged()
+                MsgEventConstants.MSG_EVENT_SWITCH_COLLECTION_UPDATE_RESULT -> {
+                    if (it.data == 2) {
+                        getMap(currentMotorMapId.toString(), currentLotoId)
+                        mBinding?.rvList?.adapter?.notifyDataSetChanged()
+                    }
                 }
             }
         }
         presenter?.getMapPage {
             if (it?.records?.isNotEmpty() == true) {
-                currentMotorMapId = it.records[0].motorMapId ?: 0
-                currentLotoId = it.records[0].lotoId.toString()
-                LogUtil.i("地图数据:${it.records}")
+                currentMotorMapId =
+                    (it.records.find { it.lotoSerialNumber == requireContext().serialNo() }
+                        ?: it.records[0]).motorMapId ?: 0
+                currentLotoId =
+                    (it.records.find { it.lotoSerialNumber == requireContext().serialNo() }
+                        ?: it.records[0]).lotoId.toString()
                 mBinding?.mapRv?.models = it.records
-                getMap(it.records[0].motorMapId.toString(), it.records[0].lotoId.toString())
+                mBinding?.mapTitle?.text =
+                    (it.records.find { it.lotoSerialNumber == requireContext().serialNo() }
+                        ?: it.records[0]).motorMapName
+                getMap((it.records.find { it.lotoSerialNumber == requireContext().serialNo() }
+                    ?: it.records[0]).motorMapId.toString(),
+                    (it.records.find { it.lotoSerialNumber == requireContext().serialNo() }
+                        ?: it.records[0]).lotoId.toString()
+                )
             }
         }
     }
 
+    override fun onPause() {
+        super.onPause()
+        mBinding?.mapview?.setRenderEnabled(false)   // ✅ 离开当前页立刻停渲染
+    }
+
     private fun getMap(mapId: String, lotoId: String) {
         if (lotoId.isEmpty() || mapId.isEmpty()) {
             return
@@ -153,14 +178,17 @@ class SwitchStatusActivity :
                 mBinding?.rvList?.models = itMotorMapInfo?.data
                 ThreadUtils.runOnIO {
                     presenter?.mapDataHandle(
-                        this@SwitchStatusActivity,
+                        requireContext(),
                         itMapInfo,
                         itMotorMapInfo,
                         { mBinding?.mapview },
                         stationLayer
                     ) { mapBmp ->
                         ThreadUtils.runOnMain {
-                            mBinding?.mapview?.loadMap(mapBmp)
+                            if (!isMapLoaded) {
+                                mBinding?.mapview?.loadMap(mapBmp)
+                                isMapLoaded = true
+                            }
                         }
                     }
                 }
@@ -202,7 +230,7 @@ class SwitchStatusActivity :
             }
 
             override fun onMapLoadFail() {
-                ToastUtils.tip("onMapLoadFail")
+                ToastUtils.Companion.tip("onMapLoadFail")
             }
         })
     }

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/view/fragment/WorkshopFragment.kt

@@ -147,7 +147,7 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                             val isAllBitmapLoaded =
                                 mPointList.all { it.ticketList.take(4).all { it.bitmap != null } }
                             if (isAllBitmapLoaded) {
-                                mBinding?.mapview?.refresh()
+                                mBinding?.mapview?.refreshWorld()
                             }
                             return@repeatOnMain !isAllBitmapLoaded
                         }, 100, true)

+ 8 - 0
app/src/main/java/com/grkj/iscs_mars/view/presenter/LoginPresenter.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs_mars.view.presenter
 
 import android.content.Context
 import android.graphics.Bitmap
+import cn.zhxu.okhttps.HttpUtils
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.extentions.removeLeadingZeros
@@ -128,4 +129,11 @@ class LoginPresenter : BasePresenter<ILoginView>() {
     fun unregisterListener() {
         BusinessManager.unregisterListener(this)
     }
+
+    /**
+     * 下载授权
+     */
+    fun downloadLicense() {
+
+    }
 }

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

@@ -130,7 +130,7 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
                     }
 
                     val switchStatus =
-                        if (pt.switchStatus == "1") CustomSwitchStationLayer.STATUS_ON
+                        if (pt.status == "1") CustomSwitchStationLayer.STATUS_ON
                         else CustomSwitchStationLayer.STATUS_OFF
 
                     val p = CustomSwitchStationLayer.IsolationPoint(

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

@@ -165,7 +165,7 @@ class CustomMarkLayer @JvmOverloads constructor(
 
         if (listener != null && isClickMark) {
             listener!!.markIsClick(num, btnIndex, isClickIcon)
-            mapView.refresh()
+            mapView.refreshWorld()
         }
     }
 

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

@@ -45,7 +45,7 @@ class CustomStationLayer @JvmOverloads constructor(
 
     private val FRAME_INTERVAL_MS = 32L
     private val invalidateRunnable = Runnable {
-        mapView?.refresh()
+        mapView?.refreshWorld()
     }
 
     init {
@@ -95,7 +95,7 @@ class CustomStationLayer @JvmOverloads constructor(
         try {
             canvas.save()
             try {
-                canvas.concat(currentMatrix)
+//                canvas.concat(currentMatrix)
 
                 val tempPointList = synchronized(dataLock) { pointList.toList() }
 

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

@@ -25,7 +25,7 @@ import kotlin.math.abs
 class CustomSwitchStationLayer @JvmOverloads constructor(
     mapView: MapView?,
     private var stationList: MutableList<IsolationPoint> = mutableListOf()
-) : MapBaseLayer(mapView) {
+) : MapBaseLayer(mapView), MapView.PausableLayer {
 
     // ===== 数据结构 =====
     data class IsolationPoint(
@@ -79,7 +79,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     private var nextInvalidateScheduled = false
     private val invalidateRunnable = Runnable {
         nextInvalidateScheduled = false
-        mapView?.refresh()
+        mapView?.refreshWorld()
     }
 
     private val refreshRunnable: Runnable = object : Runnable {
@@ -369,6 +369,10 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         }
     }
 
+    override fun onHostVisibilityChanged(visible: Boolean) {
+        if (visible) startAnimation() else stopAnimation()
+    }
+
     // ================= 绘制 =================
     override fun draw(
         canvas: Canvas,
@@ -382,7 +386,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
 
         try {
             canvas.save()
-            canvas.concat(currentMatrix)  // 地图照常缩放/旋转
+//            canvas.concat(currentMatrix)  // 地图照常缩放/旋转
 
             val points = synchronized(dataLock) { stationList.toList() }
 
@@ -396,7 +400,12 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
             // 画到“地图坐标”层面的尺寸需要 /zoom 才能抵消缩放,获得恒定屏幕像素。
             val mapR = screenR
             val mapStroke = screenStroke
-
+            val t = pulsePhase
+            val s = kotlin.math.sin(2f * Math.PI.toFloat() * t) // 只算一次
+            val a = (160 + (95 * kotlin.math.abs(s))).toInt()
+            val pulScale = 1.0f + 0.10f * s
+            val pulHaloA = (a * 0.35f).toInt()
+            val col = if (t < 0.5f) colRed else colOrange
             for (p in points) {
                 val c = centerOf(p)
 
@@ -415,14 +424,11 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
                     }
 
                     STATUS_ALARM -> {
-                        val t = pulsePhase
-                        val color = if (t < 0.5f) colRed else colOrange
-                        val a =
-                            (160 + 95 * kotlin.math.abs(kotlin.math.sin(2f * Math.PI.toFloat() * t))).toInt()
+                        val color = col
                         paint.color = color; paint.alpha = a
-                        val r = mapR * (1.0f + 0.10f * kotlin.math.sin(2f * Math.PI.toFloat() * t))
+                        val r = mapR * pulScale
                         canvas.drawCircle(c.x, c.y, r, paint)
-                        paint.alpha = (a * 0.35f).toInt()
+                        paint.alpha = pulHaloA
                         canvas.drawCircle(c.x, c.y, r * 1.25f, paint)
                         paint.alpha = 255
                     }

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

@@ -6,11 +6,14 @@ import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Matrix;
+import android.graphics.Paint;
 import android.graphics.Picture;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.os.Looper;
 import android.util.AttributeSet;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.TextureView;
 import android.view.animation.AccelerateDecelerateInterpolator;
@@ -29,7 +32,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     private SurfaceTexture surface;
     private boolean isMapLoadFinish = false;
 
-    /** 用 COWList 避免 refresh() 遍历时被并发修改 */
+    /**
+     * 用 COWList 避免 refresh() 遍历时被并发修改
+     */
     private final CopyOnWriteArrayList<MapBaseLayer> layers = new CopyOnWriteArrayList<>();
     private MapLayer mapLayer;
 
@@ -62,6 +67,103 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     private Picture framePicture;                 // 每帧录制的世界坐标内容
     private int frameW = 0, frameH = 0;          // 录制尺寸(通常等于地图宽高)
 
+    // ===== 线程 & 合帧控制 =====
+    private android.os.HandlerThread renderThread;
+    private android.os.Handler renderHandler;
+    private final java.util.concurrent.atomic.AtomicBoolean frameDirty = new java.util.concurrent.atomic.AtomicBoolean(false);
+    private final java.util.concurrent.atomic.AtomicBoolean drawScheduled = new java.util.concurrent.atomic.AtomicBoolean(false);
+
+    // 拷贝当前状态用于渲染,避免锁 UI 线程
+    private final Object stateLock = new Object();
+    private final Matrix drawMatrix = new Matrix();
+    private float drawZoom = 1.0f;
+    private float drawRotateDeg = 0.0f;
+
+    // 目标帧率(可调)
+    private static final long FRAME_INTERVAL_MS = 16; // ~60fps
+
+    // 过滤采样,减少缩放“抖光”
+    private final Paint composePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG);
+
+    private Bitmap frontBmp, backBmp;
+    private Canvas backCanvas;
+    private int worldW = 0, worldH = 0;
+    private final Object worldLock = new Object();
+    private volatile boolean worldDirty = true; // 地图或图层内容变更时置 true
+
+    // ==== 交互优先:手势锁 + 延后一次相机操作 ====
+    private volatile boolean userGestureActive = false;
+    private final Object cameraOpLock = new Object();
+    private Runnable pendingCameraOp = null;     // 只保留“最后一次”
+    private android.animation.ValueAnimator cameraAnimator; // 如果你有平移动画,按需取消
+
+    private volatile boolean renderEnabled = true;
+
+    private final Runnable renderRunnable = new Runnable() {
+        @Override public void run() {
+            try {
+                if (!renderEnabled) return;
+                drawScheduled.set(false);
+
+                if (!frameDirty.getAndSet(false)) return;
+                if (surface == null || !isMapLoadFinish) return;
+
+                // 复制当前状态(避免 UI 线程锁)
+                final Matrix m = new Matrix();
+                final float zoom, deg;
+                synchronized (stateLock) {
+                    m.set(drawMatrix);
+                    zoom = drawZoom;
+                    deg  = drawRotateDeg;
+                }
+
+                // 1) 世界缓冲(offscreen 双缓冲)
+                final int w = Math.max(1, (int) getMapWidth());
+                final int h = Math.max(1, (int) getMapHeight());
+                ensureWorldBuffers(w, h);
+
+                if (worldDirty) {
+                    synchronized (worldLock) {
+                        // 清透明,避免残影
+                        backCanvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
+
+                        // ★ 关键:传恒等矩阵;Layer 内禁止再 concat 外部矩阵
+                        for (MapBaseLayer layer : layers) {
+                            if (layer.isVisible) layer.draw(backCanvas, IDENTITY, zoom, deg);
+                        }
+                        // 原子交换 front/back
+                        Bitmap tmp = frontBmp; frontBmp = backBmp; backBmp = tmp;
+                        backCanvas.setBitmap(backBmp);
+                        worldDirty = false;
+                    }
+                }
+
+                // 2) 合成到屏幕(只做一次 concat)
+                Canvas screen = lockCanvas();
+                if (screen != null) {
+                    try {
+                        screen.drawColor(backgroundColor);
+                        screen.save();
+                        screen.concat(m);
+                        // FILTER/DITHER 让缩放更稳,不闪像素点
+                        screen.drawBitmap(frontBmp, null, new Rect(0, 0, worldW, worldH), composePaint);
+                        screen.restore();
+                    } finally {
+                        unlockCanvasAndPost(screen);
+                    }
+                }
+
+                // 3) 如果期间又有新的 refresh(),排到下一帧(≈60fps)
+                if (frameDirty.get()) {
+                    if (renderHandler != null) {
+                        renderHandler.postDelayed(this, FRAME_INTERVAL_MS);
+                        drawScheduled.set(true);
+                    }
+                }
+            } catch (Throwable ignore) { /* 保住渲染线程 */ }
+        }
+    };
+
     public MapView(Context context) {
         this(context, null);
     }
@@ -84,6 +186,10 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         setSurfaceTextureListener(this);
         setOpaque(true);
         setClickable(true);
+
+        renderThread = new android.os.HandlerThread("MapRenderThread", android.os.Process.THREAD_PRIORITY_DISPLAY);
+        renderThread.start();
+        renderHandler = new android.os.Handler(renderThread.getLooper());
     }
 
     @Override
@@ -93,16 +199,94 @@ 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) {
         surface = null;
+        if (renderThread != null) {
+            renderThread.quitSafely();
+            renderThread = null;
+            renderHandler = null;
+        }
         return true;
     }
 
     @Override
-    public void onSurfaceTextureUpdated(SurfaceTexture st) { }
+    public void onSurfaceTextureUpdated(SurfaceTexture st) {
+    }
+
+    private final Choreographer.FrameCallback vsyncCallback = frameTimeNanos -> {
+        if (!renderEnabled || renderHandler == null) { drawScheduled.set(false); return; }
+        renderHandler.post(renderRunnable);
+        drawScheduled.set(false);
+    };
+
+    // 外部可调用:Fragment 可见/不可见时切换
+    public void setRenderEnabled(boolean enabled) {
+        if (renderEnabled == enabled) return;
+        renderEnabled = enabled;
+        notifyLayersVisible(enabled);  // ✅ 通知子层
+
+        if (!enabled) {
+            // 关机:停止一切排队的渲染
+            drawScheduled.set(false);
+            frameDirty.set(false);
+            if (renderHandler != null) renderHandler.removeCallbacks(renderRunnable);
+        } else {
+            // 开机:若有待渲染帧,下一次 vsync 再画
+            if (frameDirty.get()) Choreographer.getInstance().postFrameCallback(vsyncCallback);
+        }
+    }
+
+    public interface PausableLayer {
+        void onHostVisibilityChanged(boolean visible);
+    }
+
+    private void notifyLayersVisible(boolean visible) {
+        for (MapBaseLayer layer : layers) {
+            if (layer instanceof PausableLayer) {
+                ((PausableLayer) layer).onHostVisibilityChanged(visible);
+            }
+        }
+    }
+
+    // 尺寸变化或首次构建时调用
+    private void ensureWorldBuffers(int w, int h) {
+        if (w <= 0 || h <= 0) return;
+        if (frontBmp != null && frontBmp.getWidth() == w && frontBmp.getHeight() == h) return;
+        // 回收旧的
+        if (frontBmp != null) frontBmp.recycle();
+        if (backBmp != null) backBmp.recycle();
+
+        frontBmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+        backBmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+        backCanvas = new Canvas(backBmp);
+        worldW = w;
+        worldH = h;
+        worldDirty = true; // 新缓冲,需要重绘世界
+    }
+
+    private void setGestureActive(boolean active) {
+        userGestureActive = active;
+        if (!active) flushPendingCameraOp();
+    }
+
+    private void runOrDeferCameraOp(Runnable op) {
+        if (userGestureActive) {
+            synchronized (cameraOpLock) { pendingCameraOp = op; }
+        } else {
+            // 放到主线程执行,避免线程问题
+            post(op);
+        }
+    }
+
+    private void flushPendingCameraOp() {
+        Runnable r;
+        synchronized (cameraOpLock) { r = pendingCameraOp; pendingCameraOp = null; }
+        if (r != null) post(r);
+    }
 
     public void loadMap(Bitmap bitmap) {
         Bitmap safe = ensureSafeBitmapSize(bitmap);
@@ -157,56 +341,14 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     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);
-            }
+        frameDirty.set(true);
+        synchronized (stateLock) {
+            drawMatrix.set(currentMatrix);
+            drawZoom = currentZoom;
+            drawRotateDeg = currentRotateDegrees;
         }
-        framePicture.endRecording();
-
-        // 3) 把录制好的内容一次性按 currentMatrix 贴到屏幕
-        Canvas canvas = lockCanvas();
-        if (canvas != null) {
-            try {
-                canvas.drawColor(backgroundColor);
-                canvas.save();
-                canvas.concat(currentMatrix);  // 整体缩放/平移/旋转应用在这里
-                framePicture.draw(canvas);     // (0,0) → (worldW, worldH)
-                canvas.restore();
-            } finally {
-                unlockCanvasAndPost(canvas);
-            }
+        if (renderEnabled && drawScheduled.compareAndSet(false, true)) {
+            Choreographer.getInstance().postFrameCallback(vsyncCallback);
         }
     }
 
@@ -216,6 +358,8 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         int action = event.getAction() & MotionEvent.ACTION_MASK;
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                setGestureActive(true);
+                if (cameraAnimator != null && cameraAnimator.isRunning()) cameraAnimator.cancel();
                 saveMatrix.set(currentMatrix);
                 startTouch.set(event.getX(), event.getY());
                 lastMove.set(event.getX(), event.getY());
@@ -225,6 +369,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                 }
                 break;
             case MotionEvent.ACTION_POINTER_DOWN:
+                setGestureActive(true);
                 if (event.getPointerCount() == 2) {
                     saveMatrix.set(currentMatrix);
                     saveZoom = currentZoom;
@@ -235,6 +380,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
                 }
                 break;
             case MotionEvent.ACTION_UP:
+                setGestureActive(false);   // 这里会自动 flush 最后一次相机请求
                 if (withFloorPlan(event.getX(), event.getY())) {
                     for (MapBaseLayer layer : layers) {
                         layer.onTouch(event);
@@ -275,7 +421,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
     }
 
     public float[] mapXYToScreenXY(float x, float y) {
-        float[] pts = { x, y };
+        float[] pts = {x, y};
         currentMatrix.mapPoints(pts); // map -> screen(与 onDraw 完全一致)
         return pts;
     }
@@ -292,7 +438,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         return isMapLoadFinish;
     }
 
-    /** 在主线程添加图层;避免并发改动引发 CME */
+    /**
+     * 在主线程添加图层;避免并发改动引发 CME
+     */
     public void addLayer(final MapBaseLayer layer) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
             post(() -> addLayer(layer));
@@ -306,7 +454,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         }
     }
 
-    /** 在主线程移除图层 */
+    /**
+     * 在主线程移除图层
+     */
     public void removeLayer(final MapBaseLayer layer) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
             post(() -> removeLayer(layer));
@@ -319,7 +469,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         }
     }
 
-    /** 清空所有图层(主线程) */
+    /**
+     * 清空所有图层(主线程)
+     */
     public void clearLayers() {
         if (Looper.myLooper() != Looper.getMainLooper()) {
             post(this::clearLayers);
@@ -349,6 +501,15 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
         refresh();
     }
 
+    public void markWorldDirty() {
+        worldDirty = true;
+    }
+
+    public void refreshWorld() {
+        worldDirty = true;
+        refresh();      // 仍然走你的 vsync 合帧
+    }
+
     public float getCurrentRotateDegrees() {
         return currentRotateDegrees;
     }
@@ -434,12 +595,16 @@ 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);
     }
 
-    /** 瞬间中心并缩放到 zoom(zoom 需在 min~max 之间) */
+    /**
+     * 瞬间中心并缩放到 zoom(zoom 需在 min~max 之间)
+     */
     public void centerAndZoom(float x, float y, float zoom) {
         setCurrentZoom(zoom);
         mapCenterWithPoint(x, y);
@@ -471,7 +636,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
             float[] pts = {x, y};
             currentMatrix.mapPoints(pts);
 
-            float dx = getWidth()  / 2f - pts[0];
+            float dx = getWidth() / 2f - pts[0];
             float dy = getHeight() / 2f - pts[1];
             currentMatrix.postTranslate(dx, dy);
 

+ 5 - 27
app/src/main/res/layout/activity_login.xml

@@ -1,38 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/main"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:padding="@dimen/page_padding"
     tools:context=".view.activity.LoginActivity">
 
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/rv_type"
-        style="@style/CommonRecyclerView"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:orientation="horizontal" />
 
-    <TextView
-        android:id="@+id/main_title"
-        style="@style/CommonTextView"
-        android:layout_above="@id/rv_type"
-        android:layout_centerHorizontal="true"
-        android:layout_marginBottom="25dp"
-        android:text="@string/loto"
-        android:textSize="30sp" />
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/vp2"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
 
-    <TextView
-        style="@style/CommonTextView"
-        android:layout_alignParentBottom="true"
-        android:layout_centerHorizontal="true"
-        android:text="@string/login_method_tip" />
-
-    <TextView
-        android:id="@+id/tv_version"
-        style="@style/CommonTextView"
-        android:layout_alignParentRight="true"
-        android:layout_alignParentBottom="true" />
 </RelativeLayout>

+ 50 - 0
app/src/main/res/layout/fragment_login.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="@dimen/page_padding"
+    tools:context=".view.activity.LoginActivity">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rv_type"
+        style="@style/CommonRecyclerView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:orientation="horizontal" />
+
+    <TextView
+        android:id="@+id/main_title"
+        style="@style/CommonTextView"
+        android:layout_above="@id/rv_type"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="25dp"
+        android:text="@string/loto"
+        android:textSize="30sp" />
+
+    <com.grkj.iscs_mars.view.widget.CommonBtn
+        android:id="@+id/cb_motor"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_above="@+id/login_tip"
+        android:layout_centerHorizontal="true"
+        android:layout_marginVertical="@dimen/common_spacing"
+        app:btn_bg="@drawable/common_btn_blue_bg"
+        app:btn_name="@string/motor_map" />
+
+    <TextView
+        android:id="@+id/login_tip"
+        style="@style/CommonTextView"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:text="@string/login_method_tip" />
+
+    <TextView
+        android:id="@+id/tv_version"
+        style="@style/CommonTextView"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentBottom="true" />
+</RelativeLayout>

+ 24 - 13
app/src/main/res/layout/activity_switch_status.xml → app/src/main/res/layout/fragment_switch_status.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
@@ -9,9 +9,21 @@
     android:orientation="vertical"
     android:showDividers="middle">
 
+    <TextView
+        android:id="@+id/map_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/main_color_dark"
+        android:gravity="center"
+        android:paddingVertical="@dimen/common_text_padding"
+        android:textColor="@color/white"
+        android:textSize="@dimen/map_title_text_size"
+        tools:text="123" />
+
     <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:layout_below="@+id/map_title"
         android:divider="@drawable/divider_horizontal"
         android:orientation="horizontal"
         android:showDividers="middle">
@@ -41,21 +53,11 @@
                 app:layout_constraintStart_toStartOf="@id/cb_show"
                 app:layout_constraintTop_toTopOf="parent">
 
-
-                <com.grkj.iscs_mars.view.widget.CommonBtn
-                    android:id="@+id/cb_back"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_above="@id/tv_tip"
-                    android:layout_gravity="center_horizontal"
-                    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"/>
+                    android:layout_marginTop="@dimen/common_spacing" />
 
                 <TextView
                     android:layout_width="match_parent"
@@ -87,5 +89,14 @@
 
         </androidx.constraintlayout.widget.ConstraintLayout>
 
+        <com.grkj.iscs_mars.view.widget.CommonBtn
+            android:id="@+id/cb_login"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|bottom"
+            android:layout_marginVertical="@dimen/common_spacing_small"
+            app:btn_bg="@drawable/common_btn_blue_bg"
+            app:btn_name="@string/login" />
     </FrameLayout>
-</LinearLayout>
+
+</RelativeLayout>

+ 1 - 0
app/src/main/res/values-en/strings.xml

@@ -412,4 +412,5 @@
     <string name="switch_id">ID:%1$s</string>
     <string name="switch_status_tv">Switch Status:</string>
     <string name="device_inputting">Hardware inputting……</string>
+    <string name="motor_map">Motor</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -412,4 +412,5 @@
     <string name="switch_id">编号:%1$s</string>
     <string name="switch_status_tv">电机状态:</string>
     <string name="device_inputting">硬件录入中……</string>
+    <string name="motor_map">开关电机</string>
 </resources>

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -92,4 +92,5 @@
     <dimen name="device_registration_common_icon_width">111dp</dimen>
     <dimen name="device_registration_common_icon_height">49dp</dimen>
     <dimen name="device_registration_common_text_size">12sp</dimen>
+    <dimen name="map_title_text_size">30sp</dimen>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -412,4 +412,5 @@
     <string name="switch_id">编号:%1$s</string>
     <string name="switch_status_tv">电机状态:</string>
     <string name="device_inputting">硬件录入中……</string>
+    <string name="motor_map">开关电机</string>
 </resources>