expo-gaode-map-navigation 1.1.5-next.0 → 1.1.5-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +213 -73
  2. package/android/build.gradle +10 -0
  3. package/android/src/main/cpp/CMakeLists.txt +24 -0
  4. package/android/src/main/cpp/cluster_jni.cpp +848 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
  8. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
  9. package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
  10. package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
  11. package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
  12. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
  13. package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
  14. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
  15. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
  16. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
  17. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
  18. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
  19. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
  20. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
  21. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
  22. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
  23. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
  24. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
  25. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
  26. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
  27. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
  28. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
  29. package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
  30. package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
  31. package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
  32. package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
  33. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
  34. package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
  35. package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
  36. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -3
  37. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +4 -0
  38. package/build/ExpoGaodeMapNaviView.d.ts +7 -7
  39. package/build/ExpoGaodeMapNaviView.js +10 -11
  40. package/build/index.d.ts +1 -1
  41. package/build/index.js +2 -2
  42. package/build/map/ExpoGaodeMapModule.d.ts +2 -201
  43. package/build/map/ExpoGaodeMapModule.js +586 -18
  44. package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
  45. package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
  46. package/build/map/ExpoGaodeMapView.js +66 -58
  47. package/build/map/components/FoldableMapView.d.ts +38 -0
  48. package/build/map/components/FoldableMapView.js +209 -0
  49. package/build/map/components/MapContext.d.ts +12 -0
  50. package/build/map/components/MapContext.js +54 -0
  51. package/build/map/components/MapUI.d.ts +18 -0
  52. package/build/map/components/MapUI.js +29 -0
  53. package/build/map/components/overlays/Circle.js +34 -3
  54. package/build/map/components/overlays/Cluster.d.ts +3 -1
  55. package/build/map/components/overlays/Cluster.js +31 -2
  56. package/build/map/components/overlays/HeatMap.d.ts +3 -1
  57. package/build/map/components/overlays/HeatMap.js +33 -3
  58. package/build/map/components/overlays/Marker.d.ts +1 -1
  59. package/build/map/components/overlays/Marker.js +37 -32
  60. package/build/map/components/overlays/MultiPoint.js +1 -1
  61. package/build/map/components/overlays/Polygon.js +30 -3
  62. package/build/map/components/overlays/Polyline.js +36 -3
  63. package/build/map/index.d.ts +25 -5
  64. package/build/map/index.js +59 -18
  65. package/build/map/types/common.types.d.ts +40 -0
  66. package/build/map/types/common.types.js +0 -4
  67. package/build/map/types/index.d.ts +3 -2
  68. package/build/map/types/map-view.types.d.ts +108 -3
  69. package/build/map/types/native-module.types.d.ts +363 -0
  70. package/build/map/types/native-module.types.js +5 -0
  71. package/build/map/types/offline.types.d.ts +132 -0
  72. package/build/map/types/offline.types.js +5 -0
  73. package/build/map/types/overlays.types.d.ts +137 -24
  74. package/build/map/utils/ErrorHandler.d.ts +110 -0
  75. package/build/map/utils/ErrorHandler.js +421 -0
  76. package/build/map/utils/GeoUtils.d.ts +20 -0
  77. package/build/map/utils/GeoUtils.js +76 -0
  78. package/build/map/utils/OfflineMapManager.d.ts +148 -0
  79. package/build/map/utils/OfflineMapManager.js +217 -0
  80. package/build/map/utils/PermissionUtils.d.ts +91 -0
  81. package/build/map/utils/PermissionUtils.js +255 -0
  82. package/build/map/utils/PlatformDetector.d.ts +102 -0
  83. package/build/map/utils/PlatformDetector.js +186 -0
  84. package/build/types/naviview.types.d.ts +6 -1
  85. package/expo-module.config.json +12 -10
  86. package/ios/ExpoGaodeMapNavigation.podspec +9 -0
  87. package/ios/map/ExpoGaodeMapModule.swift +485 -75
  88. package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
  89. package/ios/map/ExpoGaodeMapView.swift +611 -62
  90. package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
  91. package/ios/map/MapPreloadManager.swift +348 -0
  92. package/ios/map/cpp/ClusterEngine.cpp +110 -0
  93. package/ios/map/cpp/ClusterEngine.hpp +20 -0
  94. package/ios/map/cpp/ColorParser.cpp +135 -0
  95. package/ios/map/cpp/ColorParser.hpp +14 -0
  96. package/ios/map/cpp/GeometryEngine.cpp +574 -0
  97. package/ios/map/cpp/GeometryEngine.hpp +159 -0
  98. package/ios/map/cpp/QuadTree.cpp +92 -0
  99. package/ios/map/cpp/QuadTree.hpp +42 -0
  100. package/ios/map/cpp/README.md +55 -0
  101. package/ios/map/cpp/tests/benchmark_js.js +41 -0
  102. package/ios/map/cpp/tests/run.sh +17 -0
  103. package/ios/map/cpp/tests/test_main.cpp +276 -0
  104. package/ios/map/managers/UIManager.swift +72 -1
  105. package/ios/map/modules/LocationManager.swift +123 -166
  106. package/ios/map/overlays/CircleView.swift +16 -32
  107. package/ios/map/overlays/CircleViewModule.swift +12 -12
  108. package/ios/map/overlays/ClusterAnnotation.swift +32 -0
  109. package/ios/map/overlays/ClusterView.swift +331 -45
  110. package/ios/map/overlays/ClusterViewModule.swift +20 -6
  111. package/ios/map/overlays/HeatMapView.swift +135 -32
  112. package/ios/map/overlays/HeatMapViewModule.swift +20 -8
  113. package/ios/map/overlays/MarkerView.swift +613 -130
  114. package/ios/map/overlays/MarkerViewModule.swift +38 -18
  115. package/ios/map/overlays/MultiPointView.swift +168 -10
  116. package/ios/map/overlays/MultiPointViewModule.swift +27 -5
  117. package/ios/map/overlays/PolygonView.swift +62 -23
  118. package/ios/map/overlays/PolygonViewModule.swift +18 -12
  119. package/ios/map/overlays/PolylineView.swift +21 -13
  120. package/ios/map/overlays/PolylineViewModule.swift +18 -12
  121. package/ios/map/utils/ClusterNative.h +96 -0
  122. package/ios/map/utils/ClusterNative.mm +377 -0
  123. package/ios/map/utils/ColorParser.swift +12 -1
  124. package/ios/map/utils/CppBridging.mm +13 -0
  125. package/ios/map/utils/GeometryUtils.swift +34 -0
  126. package/ios/map/utils/LatLngParser.swift +87 -0
  127. package/ios/map/utils/PermissionManager.swift +135 -6
  128. package/package.json +2 -2
  129. package/build/map/ExpoGaodeMap.types.d.ts +0 -41
  130. package/build/map/ExpoGaodeMap.types.js +0 -24
  131. package/build/map/utils/EventManager.d.ts +0 -10
  132. package/build/map/utils/EventManager.js +0 -26
  133. package/build/map/utils/ModuleLoader.d.ts +0 -73
  134. package/build/map/utils/ModuleLoader.js +0 -112
@@ -5,15 +5,19 @@ import android.content.Context
5
5
  import android.graphics.Bitmap
6
6
  import android.graphics.BitmapFactory
7
7
  import android.graphics.Canvas
8
- import android.graphics.Color
8
+
9
9
  import android.os.Handler
10
10
  import android.os.Looper
11
11
  import android.view.View
12
12
  import com.amap.api.maps.AMap
13
+
13
14
  import com.amap.api.maps.model.BitmapDescriptorFactory
14
15
  import com.amap.api.maps.model.LatLng
15
16
  import com.amap.api.maps.model.Marker
16
17
  import com.amap.api.maps.model.MarkerOptions
18
+
19
+ import com.amap.api.maps.utils.SpatialRelationUtil
20
+ import com.amap.api.maps.utils.overlay.MovingPointOverlay
17
21
  import expo.modules.kotlin.AppContext
18
22
  import expo.modules.kotlin.viewevent.EventDispatcher
19
23
  import expo.modules.kotlin.views.ExpoView
@@ -23,742 +27,1315 @@ import java.net.URL
23
27
  import kotlin.concurrent.thread
24
28
  import androidx.core.view.isNotEmpty
25
29
  import androidx.core.view.contains
26
- import androidx.core.graphics.createBitmap
30
+
27
31
  import androidx.core.view.isEmpty
28
- import androidx.core.graphics.toColorInt
29
32
  import androidx.core.graphics.scale
33
+ import android.view.ViewGroup
34
+ import android.widget.ImageView
35
+ import android.widget.TextView
36
+ import com.amap.api.maps.model.animation.AlphaAnimation
37
+ import com.amap.api.maps.model.animation.AnimationSet
38
+ import com.amap.api.maps.model.animation.ScaleAnimation
39
+ import android.view.animation.DecelerateInterpolator
40
+ import expo.modules.gaodemap.map.companion.BitmapDescriptorCache
41
+ import expo.modules.gaodemap.map.companion.IconBitmapCache
42
+ import expo.modules.gaodemap.map.utils.GeometryUtils
43
+ import kotlin.text.StringBuilder
44
+
45
+ import java.util.concurrent.CountDownLatch
46
+ import java.util.concurrent.TimeUnit
47
+ import androidx.core.graphics.createBitmap
48
+ import expo.modules.gaodemap.map.utils.LatLngParser
30
49
 
31
50
  class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
32
-
33
- init {
34
- // 不可交互,通过父视图定位到屏幕外
35
- isClickable = false
36
- isFocusable = false
37
- // 设置为水平方向(默认),让子视图自然布局
38
- orientation = HORIZONTAL
39
- }
40
-
41
- override fun generateDefaultLayoutParams(): LayoutParams {
42
- return LayoutParams(
43
- LayoutParams.WRAP_CONTENT,
44
- LayoutParams.WRAP_CONTENT
45
- )
46
- }
47
-
48
- override fun generateLayoutParams(attrs: android.util.AttributeSet?): LayoutParams {
49
- return LayoutParams(context, attrs)
50
- }
51
-
52
- override fun generateLayoutParams(lp: android.view.ViewGroup.LayoutParams?): LayoutParams {
53
- return when (lp) {
54
- is LayoutParams -> lp
55
- is android.widget.FrameLayout.LayoutParams -> LayoutParams(lp.width, lp.height)
56
- is MarginLayoutParams -> LayoutParams(lp.width, lp.height)
57
- else -> LayoutParams(
58
- lp?.width ?: LayoutParams.WRAP_CONTENT,
59
- lp?.height ?: LayoutParams.WRAP_CONTENT
60
- )
61
- }
62
- }
63
-
64
- override fun checkLayoutParams(p: android.view.ViewGroup.LayoutParams?): Boolean {
65
- return p is android.widget.LinearLayout.LayoutParams
66
- }
67
-
68
- @SuppressLint("DrawAllocation")
69
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
70
- val selfParams = this.layoutParams
71
- if (selfParams == null || selfParams !is LayoutParams) {
72
- val width = if (customViewWidth > 0) {
73
- customViewWidth
74
- } else if (selfParams != null && selfParams.width > 0) {
75
- selfParams.width
76
- } else {
77
- LayoutParams.WRAP_CONTENT
78
- }
79
-
80
- val height = if (customViewHeight > 0) {
81
- customViewHeight
82
- } else if (selfParams != null && selfParams.height > 0) {
83
- selfParams.height
84
- } else {
85
- LayoutParams.WRAP_CONTENT
86
- }
87
-
88
- this.layoutParams = LayoutParams(width, height)
51
+
52
+ init {
53
+ // 不可交互,通过父视图定位到屏幕外
54
+ isClickable = false
55
+ isFocusable = false
56
+ // 设置为水平方向(默认),让子视图自然布局
57
+ orientation = HORIZONTAL
89
58
  }
90
-
91
- for (i in 0 until childCount) {
92
- val child = getChildAt(i)
93
- val params = child.layoutParams
94
- if (params == null || params !is LayoutParams) {
95
- child.layoutParams = LayoutParams(
96
- params?.width ?: LayoutParams.WRAP_CONTENT,
97
- params?.height ?: LayoutParams.WRAP_CONTENT
59
+
60
+ override fun generateDefaultLayoutParams(): LayoutParams {
61
+ return LayoutParams(
62
+ LayoutParams.WRAP_CONTENT,
63
+ LayoutParams.WRAP_CONTENT
98
64
  )
99
- }
100
65
  }
101
-
102
- try {
103
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
104
- } catch (e: Exception) {
105
- throw e
66
+
67
+ override fun generateLayoutParams(attrs: android.util.AttributeSet?): LayoutParams {
68
+ return LayoutParams(context, attrs)
106
69
  }
107
- }
108
-
109
- private val onMarkerPress by EventDispatcher()
110
- private val onMarkerDragStart by EventDispatcher()
111
- private val onMarkerDrag by EventDispatcher()
112
- private val onMarkerDragEnd by EventDispatcher()
113
-
114
- internal var marker: Marker? = null
115
- private var aMap: AMap? = null
116
- private var pendingPosition: LatLng? = null
117
- private var pendingLatitude: Double? = null // 临时存储纬度
118
- private var pendingLongitude: Double? = null // 临时存储经度
119
- private var iconWidth: Int = 0 // 用于自定义图标的宽度
120
- private var iconHeight: Int = 0 // 用于自定义图标的高度
121
- private var customViewWidth: Int = 0 // 用于自定义视图(children)的宽度
122
- private var customViewHeight: Int = 0 // 用于自定义视图(children)的高度
123
- private val mainHandler = Handler(Looper.getMainLooper())
124
- private var isRemoving = false // 标记是否正在被移除
125
-
126
- // 缓存属性,在 marker 创建前保存
127
- private var pendingTitle: String? = null
128
- private var pendingSnippet: String? = null
129
- private var pendingDraggable: Boolean? = null
130
- private var pendingOpacity: Float? = null
131
- private var pendingFlat: Boolean? = null
132
- private var pendingZIndex: Float? = null
133
- private var pendingAnchor: Pair<Float, Float>? = null
134
- private var pendingIconUri: String? = null
135
- private var pendingPinColor: String? = null
136
-
137
- /**
138
- * 设置地图实例
139
- */
140
- @Suppress("unused")
141
- fun setMap(map: AMap) {
142
- aMap = map
143
- createOrUpdateMarker()
144
-
145
- pendingPosition?.let { pos ->
146
- marker?.position = pos
147
- pendingPosition = null
70
+
71
+ override fun generateLayoutParams(lp: android.view.ViewGroup.LayoutParams?): LayoutParams {
72
+ return when (lp) {
73
+ is LayoutParams -> lp
74
+ is android.widget.FrameLayout.LayoutParams -> LayoutParams(lp.width, lp.height)
75
+ is MarginLayoutParams -> LayoutParams(lp.width, lp.height)
76
+ else -> LayoutParams(
77
+ lp?.width ?: LayoutParams.WRAP_CONTENT,
78
+ lp?.height ?: LayoutParams.WRAP_CONTENT
79
+ )
80
+ }
81
+ }
82
+
83
+ override fun checkLayoutParams(p: android.view.ViewGroup.LayoutParams?): Boolean {
84
+ return p is LayoutParams
148
85
  }
86
+
87
+ @SuppressLint("DrawAllocation")
88
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
89
+ val selfParams = this.layoutParams
90
+ if (selfParams == null || selfParams !is LayoutParams) {
91
+ val width = if (customViewWidth > 0) {
92
+ customViewWidth
93
+ } else if (selfParams != null && selfParams.width > 0) {
94
+ selfParams.width
95
+ } else {
96
+ LayoutParams.WRAP_CONTENT
97
+ }
98
+
99
+ val height = if (customViewHeight > 0) {
100
+ customViewHeight
101
+ } else if (selfParams != null && selfParams.height > 0) {
102
+ selfParams.height
103
+ } else {
104
+ LayoutParams.WRAP_CONTENT
105
+ }
106
+
107
+ this.layoutParams = LayoutParams(width, height)
108
+ }
109
+
110
+ for (i in 0 until childCount) {
111
+ val child = getChildAt(i)
112
+ val params = child.layoutParams
113
+ if (params == null || params !is LayoutParams) {
114
+ child.layoutParams = LayoutParams(
115
+ params?.width ?: LayoutParams.WRAP_CONTENT,
116
+ params?.height ?: LayoutParams.WRAP_CONTENT
117
+ )
118
+ }
119
+ }
120
+
121
+ try {
122
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
123
+ } catch (e: Exception) {
124
+ throw e
125
+ }
126
+ }
127
+
128
+ private val onMarkerPress by EventDispatcher()
129
+ private val onMarkerDragStart by EventDispatcher()
130
+ private val onMarkerDrag by EventDispatcher()
131
+ private val onMarkerDragEnd by EventDispatcher()
132
+
133
+ internal var marker: Marker? = null
134
+ private var aMap: AMap? = null
135
+ private var pendingPosition: LatLng? = null
136
+ private var pendingLatitude: Double? = null // 临时存储纬度
137
+ private var pendingLongitude: Double? = null // 临时存储经度
138
+ private var iconWidth: Int = 0 // 用于自定义图标的宽度
139
+ private var iconHeight: Int = 0 // 用于自定义图标的高度
140
+ private var customViewWidth: Int = 0 // 用于自定义视图(children)的宽度
141
+ private var customViewHeight: Int = 0 // 用于自定义视图(children)的高度
142
+ private val mainHandler = Handler(Looper.getMainLooper())
143
+ private var isRemoving = false // 标记是否正在被移除
144
+
145
+ // 缓存属性,在 marker 创建前保存
146
+ private var pendingTitle: String? = null
147
+ private var pendingSnippet: String? = null
148
+ private var pendingDraggable: Boolean? = null
149
+ private var pendingOpacity: Float? = null
150
+ private var pendingFlat: Boolean? = null
151
+ private var pendingZIndex: Float? = null
152
+ private var pendingAnchor: Pair<Float, Float>? = null
153
+ private var pendingIconUri: String? = null
154
+ private var pendingPinColor: String? = null
155
+ private var cacheKey: String? = null
156
+
157
+ // 平滑移动相关
158
+ private var smoothMoveMarker: MovingPointOverlay? = null
159
+ private var smoothMovePath: List<LatLng>? = null
160
+ private var smoothMoveDuration: Double = 10.0 // 默认 10 秒
149
161
 
150
- if (isNotEmpty() && marker != null) {
151
- mainHandler.postDelayed({
152
- updateMarkerIcon()
153
- }, 100)
154
-
155
- mainHandler.postDelayed({
156
- updateMarkerIcon()
157
- }, 300)
162
+ // 生长动画相关
163
+ private var growAnimation: Boolean = false
164
+ private var hasAnimated: Boolean = false
165
+ private var pendingShowMarker: Boolean = false
166
+
167
+ private fun isPositionReady(): Boolean {
168
+ return pendingLatitude == null && pendingLongitude == null && pendingPosition == null
158
169
  }
159
- }
160
-
161
- /**
162
- * 设置纬度
163
- */
164
- fun setLatitude(lat: Double) {
165
- try {
166
- if (lat < -90 || lat > 90) {
167
- return
168
- }
169
-
170
- pendingLatitude = lat
171
- pendingLongitude?.let { lng ->
172
- updatePosition(lat, lng)
173
- }
174
- } catch (_: Exception) {
175
- // 忽略异常
170
+
171
+ /**
172
+ * 设置生长动画
173
+ */
174
+ fun setGrowAnimation(enable: Boolean) {
175
+ growAnimation = enable
176
176
  }
177
- }
178
-
179
- /**
180
- * 设置经度
181
- */
182
- fun setLongitude(lng: Double) {
183
- try {
184
- if (lng < -180 || lng > 180) {
185
- return
186
- }
187
-
188
- pendingLongitude = lng
189
- pendingLatitude?.let { lat ->
190
- updatePosition(lat, lng)
191
- }
192
- } catch (_: Exception) {
193
- // 忽略异常
177
+
178
+ /**
179
+ * 启动显示动画
180
+ * 组合使用 AlphaAnimation 和微幅 ScaleAnimation
181
+ * Scale 从 0.5 开始而不是 0,可以显著减少因 SDK 锚点偏移导致的视觉平移感,
182
+ * 同时配合 Alpha 渐变,达成“柔和生长”的效果。
183
+ */
184
+ private fun startGrowAnimation(m: Marker) {
185
+ try {
186
+ val set = AnimationSet(true)
187
+ set.setInterpolator(DecelerateInterpolator())
188
+ set.setDuration(500)
189
+
190
+ // 透明度:0 -> 1
191
+ val alpha = AlphaAnimation(0f, 1f)
192
+ set.addAnimation(alpha)
193
+
194
+ // 缩放:0.5 -> 1.0 (避免从0开始,减少位移幅度)
195
+ val scale = ScaleAnimation(0.8f, 1f, 0.8f, 1f)
196
+ set.addAnimation(scale)
197
+
198
+ m.setAnimation(set)
199
+ m.startAnimation()
200
+ } catch (e: Exception) {
201
+ android.util.Log.e("MarkerView", "startGrowAnimation error", e)
202
+ }
194
203
  }
195
- }
196
-
197
- /**
198
- * 更新标记位置(当经纬度都设置后)
199
- */
200
- private fun updatePosition(lat: Double, lng: Double) {
201
- try {
202
- val latLng = LatLng(lat, lng)
204
+
205
+ /**
206
+ * 显示标记(统一处理可见性和动画)
207
+ */
208
+ private fun showMarker(m: Marker) {
209
+ if (!isPositionReady()) {
210
+ pendingShowMarker = true
211
+ return
212
+ }
213
+
214
+ doShowMarker(m)
215
+ }
216
+
217
+ private fun doShowMarker(m: Marker) {
218
+
219
+ val targetAlpha = pendingOpacity ?: 1.0f
220
+
221
+ if (growAnimation && !hasAnimated) {
222
+ m.isVisible = true
223
+ // 不再手动设置 alpha=0,交给 startGrowAnimation 处理
224
+ // 避免时序问题导致的一帧闪烁
225
+ startGrowAnimation(m)
226
+ hasAnimated = true
227
+ } else {
228
+ m.alpha = targetAlpha
229
+ m.isVisible = true
230
+ }
231
+ }
232
+
233
+ private fun flushPendingShowIfNeeded() {
234
+ if (!pendingShowMarker || !isPositionReady()) return
235
+ pendingShowMarker = false
236
+ marker?.let { doShowMarker(it) }
237
+ }
238
+
239
+ /**
240
+ * 设置地图实例
241
+ */
242
+ @Suppress("unused")
243
+ fun setMap(map: AMap) {
244
+ aMap = map
245
+ createOrUpdateMarker(pendingPosition)
203
246
 
204
- marker?.let {
205
- it.position = latLng
247
+ pendingPosition?.let { pos ->
248
+ marker?.position = pos
206
249
  pendingPosition = null
207
250
  pendingLatitude = null
208
251
  pendingLongitude = null
209
- } ?: run {
210
- if (aMap != null) {
211
- createOrUpdateMarker()
212
- marker?.position = latLng
213
- pendingLatitude = null
214
- pendingLongitude = null
215
- } else {
216
- pendingPosition = latLng
252
+ }
253
+
254
+ flushPendingShowIfNeeded()
255
+
256
+ // 🔑 修复:需要延迟更新图标,等待 children 完成布局
257
+ // 使用 post 延迟到下一帧,确保 children 完成测量和布局
258
+ if (isNotEmpty() && marker != null) {
259
+ mainHandler.post {
260
+ if (!isRemoving && marker != null && isNotEmpty()) {
261
+ updateMarkerIcon()
262
+ }
217
263
  }
218
264
  }
219
- } catch (_: Exception) {
220
- // 忽略异常
221
265
  }
222
- }
223
-
224
266
 
225
- /**
226
- * 设置标题
227
- */
228
- fun setTitle(title: String) {
229
- pendingTitle = title
230
- marker?.let {
231
- it.title = title
232
- // 如果信息窗口正在显示,刷新它
233
- if (it.isInfoWindowShown) {
234
- it.showInfoWindow()
235
- }
267
+ /**
268
+ * 设置位置(支持多种格式)
269
+ */
270
+ fun setPosition(positionData: Map<String, Any>?) {
271
+ LatLngParser.parseLatLng(positionData)?.let {
272
+ updatePosition(it.latitude, it.longitude)
273
+ }
236
274
  }
237
- }
238
-
239
- /**
240
- * 设置描述
241
- */
242
- fun setDescription(description: String) {
243
- pendingSnippet = description
244
- marker?.let {
245
- it.snippet = description
246
- // 如果信息窗口正在显示,刷新它
247
- if (it.isInfoWindowShown) {
248
- it.showInfoWindow()
249
- }
275
+
276
+ /**
277
+ * 设置纬度
278
+ */
279
+ fun setLatitude(lat: Double) {
280
+ try {
281
+ if (lat < -90 || lat > 90) {
282
+ return
283
+ }
284
+
285
+ pendingLatitude = lat
286
+ pendingLongitude?.let { lng ->
287
+ updatePosition(lat, lng)
288
+ }
289
+ } catch (_: Exception) {
290
+ // 忽略异常
291
+ }
250
292
  }
251
- }
252
-
253
- /**
254
- * 设置是否可拖拽
255
- */
256
- fun setDraggable(draggable: Boolean) {
257
- pendingDraggable = draggable
258
- marker?.let { it.isDraggable = draggable }
259
- }
260
-
261
293
 
262
- /**
263
- * 设置透明度
264
- */
265
- fun setOpacity(opacity: Float) {
266
- pendingOpacity = opacity
267
- marker?.let { it.alpha = opacity }
268
- }
269
-
270
- /**
271
- * 设置锚点
272
- */
273
- @SuppressLint("SuspiciousIndentation")
274
- fun setAnchor(anchor: Map<String, Float>) {
275
- val x = anchor["x"] ?: 0.5f
276
- val y = anchor["y"] ?: 1.0f
277
- pendingAnchor = Pair(x, y)
278
- marker?.setAnchor(x, y)
279
- }
280
-
281
- /**
282
- * 设置是否平贴地图
283
- */
284
- fun setFlat(flat: Boolean) {
285
- pendingFlat = flat
286
- marker?.let { it.isFlat = flat }
287
- }
288
-
289
- /**
290
- * 设置图标
291
- */
292
- fun setMarkerIcon(iconUri: String?) {
293
- pendingIconUri = iconUri
294
- iconUri?.let {
295
- marker?.let { m ->
296
- loadAndSetIcon(it, m)
297
- }
294
+ /**
295
+ * 设置经度
296
+ */
297
+ fun setLongitude(lng: Double) {
298
+ try {
299
+ if (lng < -180 || lng > 180) {
300
+ return
301
+ }
302
+
303
+ pendingLongitude = lng
304
+ pendingLatitude?.let { lat ->
305
+ updatePosition(lat, lng)
306
+ }
307
+ } catch (_: Exception) {
308
+ // 忽略异常
309
+ }
298
310
  }
299
- }
300
-
301
- /**
302
- * 加载并设置图标
303
- * 支持: http/https 网络图片, file:// 本地文件, 本地资源名
304
- */
305
- private fun loadAndSetIcon(iconUri: String, marker: Marker) {
306
- try {
307
- when {
308
- // 网络图片
309
- iconUri.startsWith("http://") || iconUri.startsWith("https://") -> {
310
- loadImageFromUrl(iconUri) { bitmap ->
311
- bitmap?.let {
312
- val resized = resizeBitmap(it, iconWidth, iconHeight)
313
- mainHandler.post {
314
- marker.setIcon(BitmapDescriptorFactory.fromBitmap(resized))
315
- marker.setAnchor(0.5f, 1.0f)
316
- }
311
+
312
+ /**
313
+ * 更新标记位置(当经纬度都设置后)
314
+ */
315
+ private fun updatePosition(lat: Double, lng: Double) {
316
+ try {
317
+ val latLng = LatLng(lat, lng)
318
+
319
+ marker?.let {
320
+ it.position = latLng
321
+ pendingPosition = null
322
+ pendingLatitude = null
323
+ pendingLongitude = null
324
+
325
+ flushPendingShowIfNeeded()
317
326
  } ?: run {
318
- mainHandler.post {
319
- marker.setIcon(BitmapDescriptorFactory.defaultMarker())
320
- }
327
+ if (aMap != null) {
328
+ createOrUpdateMarker(latLng)
329
+ marker?.position = latLng
330
+ pendingLatitude = null
331
+ pendingLongitude = null
332
+
333
+ flushPendingShowIfNeeded()
334
+ } else {
335
+ pendingPosition = latLng
336
+ pendingLatitude = null
337
+ pendingLongitude = null
338
+ }
321
339
  }
322
- }
340
+ } catch (_: Exception) {
341
+ // 忽略异常
323
342
  }
324
- // 本地文件
325
- iconUri.startsWith("file://") -> {
326
- val path = iconUri.substring(7) // 移除 "file://" 前缀
327
- val bitmap = BitmapFactory.decodeFile(path)
328
- if (bitmap != null) {
329
- val resized = resizeBitmap(bitmap, iconWidth, iconHeight)
330
- marker.setIcon(BitmapDescriptorFactory.fromBitmap(resized))
331
- marker.setAnchor(0.5f, 1.0f)
332
- } else {
333
- marker.setIcon(BitmapDescriptorFactory.defaultMarker())
334
- }
343
+ }
344
+
345
+
346
+ /**
347
+ * 设置标题
348
+ */
349
+ fun setTitle(title: String) {
350
+ pendingTitle = title
351
+ marker?.let {
352
+ it.title = title
353
+ // 如果信息窗口正在显示,刷新它
354
+ if (it.isInfoWindowShown) {
355
+ it.showInfoWindow()
356
+ }
335
357
  }
336
- // 本地资源名
337
- else -> {
338
- val resourceId = context.resources.getIdentifier(
339
- iconUri,
340
- "drawable",
341
- context.packageName
342
- )
343
- if (resourceId != 0) {
344
- val bitmap = BitmapFactory.decodeResource(context.resources, resourceId)
345
- val resized = resizeBitmap(bitmap, iconWidth, iconHeight)
346
- marker.setIcon(BitmapDescriptorFactory.fromBitmap(resized))
347
- marker.setAnchor(0.5f, 1.0f)
348
- } else {
358
+ }
359
+
360
+ /**
361
+ * 设置描述
362
+ */
363
+ fun setDescription(description: String) {
364
+ pendingSnippet = description
365
+ marker?.let {
366
+ it.snippet = description
367
+ // 如果信息窗口正在显示,刷新它
368
+ if (it.isInfoWindowShown) {
369
+ it.showInfoWindow()
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * 设置是否可拖拽
376
+ */
377
+ fun setDraggable(draggable: Boolean) {
378
+ pendingDraggable = draggable
379
+ marker?.let { it.isDraggable = draggable }
380
+ }
381
+
382
+
383
+ /**
384
+ * 设置透明度
385
+ */
386
+ fun setOpacity(opacity: Float) {
387
+ pendingOpacity = opacity
388
+ marker?.let { it.alpha = opacity }
389
+ }
390
+
391
+ /**
392
+ * JS 端传入稳定的缓存 key
393
+ */
394
+ fun setCacheKey(key: String?) {
395
+ cacheKey = key
396
+ }
397
+
398
+ /**
399
+ * 设置锚点
400
+ */
401
+ @SuppressLint("SuspiciousIndentation")
402
+ fun setAnchor(anchor: Map<String, Float>) {
403
+ val x = anchor["x"] ?: 0.5f
404
+ val y = anchor["y"] ?: 1.0f
405
+ pendingAnchor = Pair(x, y)
406
+ marker?.setAnchor(x, y)
407
+ }
408
+
409
+ /**
410
+ * 设置是否平贴地图
411
+ */
412
+ fun setFlat(flat: Boolean) {
413
+ pendingFlat = flat
414
+ marker?.let { it.isFlat = flat }
415
+ }
416
+
417
+ /**
418
+ * 设置图标
419
+ */
420
+ fun setMarkerIcon(iconUri: String?) {
421
+ pendingIconUri = iconUri
422
+ iconUri?.let {
423
+ marker?.let { m ->
424
+ loadAndSetIcon(it, m)
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * 加载并设置图标
431
+ * 支持: http/https 网络图片, file:// 本地文件, 本地资源名
432
+ */
433
+ private fun loadAndSetIcon(iconUri: String, marker: Marker) {
434
+ try {
435
+ // 构建缓存 key
436
+ val keyPart = cacheKey ?: "icon|$iconUri"
437
+ val fullCacheKey = "$keyPart|${iconWidth}x${iconHeight}"
438
+
439
+ // ✅ 优先尝试 BitmapDescriptorCache
440
+ BitmapDescriptorCache.get(fullCacheKey)?.let {
441
+ marker.setIcon(it)
442
+ marker.setAnchor(0.5f, 1.0f)
443
+ showMarker(marker)
444
+ return
445
+ }
446
+
447
+ when {
448
+ iconUri.startsWith("http://") || iconUri.startsWith("https://") -> {
449
+ loadImageFromUrl(iconUri) { bitmap ->
450
+ bitmap?.let {
451
+ val resized = resizeBitmap(it, iconWidth, iconHeight)
452
+ // 缓存 bitmap
453
+ IconBitmapCache.put(fullCacheKey, resized)
454
+ // 生成 Descriptor 并缓存
455
+ val descriptor = BitmapDescriptorFactory.fromBitmap(resized)
456
+ BitmapDescriptorCache.putDescriptor(fullCacheKey, descriptor)
457
+
458
+ mainHandler.post {
459
+ marker.setIcon(descriptor)
460
+ marker.setAnchor(0.5f, 1.0f)
461
+ showMarker(marker)
462
+ }
463
+ } ?: run {
464
+ mainHandler.post {
465
+ marker.setIcon(BitmapDescriptorFactory.defaultMarker())
466
+ showMarker(marker)
467
+ }
468
+ }
469
+ }
470
+ }
471
+ iconUri.startsWith("file://") -> {
472
+ val path = iconUri.substring(7)
473
+ val bitmap = BitmapFactory.decodeFile(path)
474
+ if (bitmap != null) {
475
+ val resized = resizeBitmap(bitmap, iconWidth, iconHeight)
476
+ IconBitmapCache.put(fullCacheKey, resized)
477
+ val descriptor = BitmapDescriptorFactory.fromBitmap(resized)
478
+ BitmapDescriptorCache.putDescriptor(fullCacheKey, descriptor)
479
+ marker.setIcon(descriptor)
480
+ marker.setAnchor(0.5f, 1.0f)
481
+ showMarker(marker)
482
+ } else {
483
+ marker.setIcon(BitmapDescriptorFactory.defaultMarker())
484
+ showMarker(marker)
485
+ }
486
+ }
487
+ else -> { // 本地资源名
488
+ val resId = context.resources.getIdentifier(iconUri, "drawable", context.packageName)
489
+ if (resId != 0) {
490
+ val bitmap = BitmapFactory.decodeResource(context.resources, resId)
491
+ val resized = resizeBitmap(bitmap, iconWidth, iconHeight)
492
+ IconBitmapCache.put(fullCacheKey, resized)
493
+ val descriptor = BitmapDescriptorFactory.fromBitmap(resized)
494
+ BitmapDescriptorCache.putDescriptor(fullCacheKey, descriptor)
495
+ marker.setIcon(descriptor)
496
+ marker.setAnchor(0.5f, 1.0f)
497
+ showMarker(marker)
498
+ } else {
499
+ marker.setIcon(BitmapDescriptorFactory.defaultMarker())
500
+ showMarker(marker)
501
+ }
502
+ }
503
+ }
504
+ } catch (_: Exception) {
349
505
  marker.setIcon(BitmapDescriptorFactory.defaultMarker())
350
- }
506
+ showMarker(marker)
351
507
  }
352
- }
353
- } catch (_: Exception) {
354
- marker.setIcon(BitmapDescriptorFactory.defaultMarker())
355
508
  }
356
- }
357
-
358
- /**
359
- * 从网络加载图片
360
- */
361
- private fun loadImageFromUrl(url: String, callback: (Bitmap?) -> Unit) {
362
- thread {
363
- var connection: HttpURLConnection? = null
364
- var inputStream: InputStream? = null
365
- try {
366
- val urlConnection = URL(url)
367
- connection = urlConnection.openConnection() as HttpURLConnection
368
- connection.connectTimeout = 10000
369
- connection.readTimeout = 10000
370
- connection.doInput = true
371
- connection.connect()
372
-
373
- if (connection.responseCode == HttpURLConnection.HTTP_OK) {
374
- inputStream = connection.inputStream
375
- val bitmap = BitmapFactory.decodeStream(inputStream)
376
- callback(bitmap)
509
+
510
+
511
+ /**
512
+ * 从网络加载图片
513
+ */
514
+ private fun loadImageFromUrl(url: String, callback: (Bitmap?) -> Unit) {
515
+ thread {
516
+ var connection: HttpURLConnection? = null
517
+ var inputStream: InputStream? = null
518
+ try {
519
+ val urlConnection = URL(url)
520
+ connection = urlConnection.openConnection() as HttpURLConnection
521
+ connection.connectTimeout = 10000
522
+ connection.readTimeout = 10000
523
+ connection.doInput = true
524
+ connection.connect()
525
+
526
+ if (connection.responseCode == HttpURLConnection.HTTP_OK) {
527
+ inputStream = connection.inputStream
528
+ val bitmap = BitmapFactory.decodeStream(inputStream)
529
+ callback(bitmap)
530
+ } else {
531
+ callback(null)
532
+ }
533
+ } catch (_: Exception) {
534
+ callback(null)
535
+ } finally {
536
+ inputStream?.close()
537
+ connection?.disconnect()
538
+ }
539
+ }
540
+ }
541
+
542
+ /**
543
+ * 调整图片尺寸
544
+ */
545
+ private fun resizeBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap {
546
+ // 如果没有指定尺寸,使用原图尺寸或默认值
547
+ val finalWidth = if (width > 0) width else bitmap.width
548
+ val finalHeight = if (height > 0) height else bitmap.height
549
+
550
+ return if (bitmap.width == finalWidth && bitmap.height == finalHeight) {
551
+ bitmap
377
552
  } else {
378
- callback(null)
553
+ bitmap.scale(finalWidth, finalHeight)
379
554
  }
380
- } catch (_: Exception) {
381
- callback(null)
382
- } finally {
383
- inputStream?.close()
384
- connection?.disconnect()
385
- }
386
555
  }
387
- }
388
-
389
- /**
390
- * 调整图片尺寸
391
- */
392
- private fun resizeBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap {
393
- // 如果没有指定尺寸,使用原图尺寸或默认值
394
- val finalWidth = if (width > 0) width else bitmap.width
395
- val finalHeight = if (height > 0) height else bitmap.height
396
-
397
- return if (bitmap.width == finalWidth && bitmap.height == finalHeight) {
398
- bitmap
399
- } else {
400
- bitmap.scale(finalWidth, finalHeight)
556
+
557
+ /**
558
+ * 设置大头针颜色
559
+ */
560
+ fun setPinColor(color: String?) {
561
+ pendingPinColor = color
562
+ // 颜色变化时需要重新创建 marker
563
+ aMap?.let { _ ->
564
+ marker?.let { oldMarker ->
565
+ val position = oldMarker.position
566
+ oldMarker.remove()
567
+ marker = null
568
+
569
+ createOrUpdateMarker(position)
570
+ marker?.position = position
571
+ }
572
+ }
401
573
  }
402
- }
403
-
404
- /**
405
- * 设置大头针颜色
406
- */
407
- fun setPinColor(color: String?) {
408
- pendingPinColor = color
409
- // 颜色变化时需要重新创建 marker
410
- aMap?.let { _ ->
411
- marker?.let { oldMarker ->
412
- val position = oldMarker.position
413
- oldMarker.remove()
414
- marker = null
415
-
416
- createOrUpdateMarker()
417
- marker?.position = position
418
- }
574
+
575
+ /**
576
+ * 应用大头针颜色(使用缓存优化性能)
577
+ */
578
+ private fun applyPinColor(color: String, marker: Marker) {
579
+ try {
580
+ val hue = when (color.lowercase()) {
581
+ "red" -> BitmapDescriptorFactory.HUE_RED
582
+ "orange" -> BitmapDescriptorFactory.HUE_ORANGE
583
+ "yellow" -> BitmapDescriptorFactory.HUE_YELLOW
584
+ "green" -> BitmapDescriptorFactory.HUE_GREEN
585
+ "cyan" -> BitmapDescriptorFactory.HUE_CYAN
586
+ "blue" -> BitmapDescriptorFactory.HUE_BLUE
587
+ "violet" -> BitmapDescriptorFactory.HUE_VIOLET
588
+ "magenta" -> BitmapDescriptorFactory.HUE_MAGENTA
589
+ "rose" -> BitmapDescriptorFactory.HUE_ROSE
590
+ "purple" -> BitmapDescriptorFactory.HUE_VIOLET
591
+ else -> BitmapDescriptorFactory.HUE_RED
592
+ }
593
+
594
+ // 🔑 性能优化:使用缓存避免重复创建 BitmapDescriptor
595
+ val cacheKey = "pin_$color"
596
+ val descriptor = BitmapDescriptorCache.get(cacheKey) ?: run {
597
+ val newDescriptor = BitmapDescriptorFactory.defaultMarker(hue)
598
+ BitmapDescriptorCache.putDescriptor(cacheKey, newDescriptor)
599
+ newDescriptor
600
+ }
601
+
602
+ marker.setIcon(descriptor)
603
+ showMarker(marker)
604
+ } catch (_: Exception) {
605
+ // 忽略异常
606
+ }
419
607
  }
420
- }
421
-
422
- /**
423
- * 应用大头针颜色
424
- */
425
- private fun applyPinColor(color: String, marker: Marker) {
426
- try {
427
- val hue = when (color.lowercase()) {
428
- "red" -> BitmapDescriptorFactory.HUE_RED
429
- "orange" -> BitmapDescriptorFactory.HUE_ORANGE
430
- "yellow" -> BitmapDescriptorFactory.HUE_YELLOW
431
- "green" -> BitmapDescriptorFactory.HUE_GREEN
432
- "cyan" -> BitmapDescriptorFactory.HUE_CYAN
433
- "blue" -> BitmapDescriptorFactory.HUE_BLUE
434
- "violet" -> BitmapDescriptorFactory.HUE_VIOLET
435
- "magenta" -> BitmapDescriptorFactory.HUE_MAGENTA
436
- "rose" -> BitmapDescriptorFactory.HUE_ROSE
437
- "purple" -> BitmapDescriptorFactory.HUE_VIOLET
438
- else -> BitmapDescriptorFactory.HUE_RED
439
- }
440
- marker.setIcon(BitmapDescriptorFactory.defaultMarker(hue))
441
- } catch (_: Exception) {
442
- // 忽略异常
608
+
609
+ /**
610
+ * 设置 z-index
611
+ */
612
+ fun setZIndex(zIndex: Float) {
613
+ pendingZIndex = zIndex
614
+ marker?.let { it.zIndex = zIndex }
443
615
  }
444
- }
445
-
446
- /**
447
- * 设置 z-index
448
- */
449
- fun setZIndex(zIndex: Float) {
450
- pendingZIndex = zIndex
451
- marker?.let { it.zIndex = zIndex }
452
- }
453
-
454
- /**
455
- * 设置图标宽度(用于自定义图标 icon 属性)
456
- * 注意:React Native 传入的是 DP 值,需要转换为 PX
457
- */
458
- fun setIconWidth(width: Int) {
459
- val density = context.resources.displayMetrics.density
460
- iconWidth = (width * density).toInt()
461
- }
462
-
463
- /**
464
- * 设置图标高度(用于自定义图标 icon 属性)
465
- * 注意:React Native 传入的是 DP 值,需要转换为 PX
466
- */
467
- fun setIconHeight(height: Int) {
468
- val density = context.resources.displayMetrics.density
469
- iconHeight = (height * density).toInt()
470
- }
471
-
472
- /**
473
- * 设置自定义视图宽度(用于 children 属性)
474
- * 注意:React Native 传入的是 DP 值,需要转换为 PX
475
- */
476
- fun setCustomViewWidth(width: Int) {
477
- val density = context.resources.displayMetrics.density
478
- customViewWidth = (width * density).toInt()
479
- }
480
-
481
- /**
482
- * 设置自定义视图高度(用于 children 属性)
483
- * 注意:React Native 传入的是 DP 值,需要转换为 PX
484
- */
485
- fun setCustomViewHeight(height: Int) {
486
- val density = context.resources.displayMetrics.density
487
- customViewHeight = (height * density).toInt()
488
- }
489
-
490
- /**
491
- * 全局的 Marker 点击监听器
492
- * 必须在 ExpoGaodeMapView 中设置,不能在每个 MarkerView 中重复设置
493
- */
494
- companion object {
495
- private val markerViewMap = mutableMapOf<Marker, MarkerView>()
496
-
497
- fun registerMarker(marker: Marker, view: MarkerView) {
498
- markerViewMap[marker] = view
616
+
617
+ /**
618
+ * 设置图标宽度(用于自定义图标 icon 属性)
619
+ * 注意:React Native 传入的是 DP 值,需要转换为 PX
620
+ */
621
+ fun setIconWidth(width: Int) {
622
+ val density = context.resources.displayMetrics.density
623
+ iconWidth = (width * density).toInt()
499
624
  }
500
-
501
- fun unregisterMarker(marker: Marker) {
502
- markerViewMap.remove(marker)
625
+
626
+ /**
627
+ * 设置图标高度(用于自定义图标 icon 属性)
628
+ * 注意:React Native 传入的是 DP 值,需要转换为 PX
629
+ */
630
+ fun setIconHeight(height: Int) {
631
+ val density = context.resources.displayMetrics.density
632
+ iconHeight = (height * density).toInt()
503
633
  }
504
-
505
- fun handleMarkerClick(marker: Marker): Boolean {
506
- markerViewMap[marker]?.let { view ->
507
- view.onMarkerPress.invoke(mapOf(
508
- "latitude" to marker.position.latitude,
509
- "longitude" to marker.position.longitude
510
- ))
511
- // 只有在没有自定义内容(children)且有 title snippet 时才显示信息窗口
512
- // 如果有自定义内容,说明用户已经自定义了显示内容,不需要默认信息窗口
513
- if (view.isEmpty() && (!marker.title.isNullOrEmpty() || !marker.snippet.isNullOrEmpty())) {
514
- marker.showInfoWindow()
515
- }
516
- return true
517
- }
518
- return false
634
+
635
+ /**
636
+ * 设置自定义视图宽度(用于 children 属性)
637
+ * 注意:React Native 传入的是 DP 值,需要转换为 PX
638
+ */
639
+ fun setCustomViewWidth(width: Int) {
640
+ val density = context.resources.displayMetrics.density
641
+ customViewWidth = (width * density).toInt()
519
642
  }
520
-
521
- fun handleMarkerDragStart(marker: Marker) {
522
- markerViewMap[marker]?.onMarkerDragStart?.invoke(mapOf(
523
- "latitude" to marker.position.latitude,
524
- "longitude" to marker.position.longitude
525
- ))
643
+
644
+ /**
645
+ * 设置自定义视图高度(用于 children 属性)
646
+ * 注意:React Native 传入的是 DP 值,需要转换为 PX
647
+ */
648
+ fun setCustomViewHeight(height: Int) {
649
+ val density = context.resources.displayMetrics.density
650
+ customViewHeight = (height * density).toInt()
526
651
  }
527
-
528
- fun handleMarkerDrag(marker: Marker) {
529
- markerViewMap[marker]?.onMarkerDrag?.invoke(mapOf(
530
- "latitude" to marker.position.latitude,
531
- "longitude" to marker.position.longitude
532
- ))
652
+
653
+ /**
654
+ * 全局的 Marker 点击监听器
655
+ * 必须在 ExpoGaodeMapView 中设置,不能在每个 MarkerView 中重复设置
656
+ */
657
+ companion object {
658
+ private val markerViewMap = mutableMapOf<Marker, MarkerView>()
659
+
660
+ fun registerMarker(marker: Marker, view: MarkerView) {
661
+ markerViewMap[marker] = view
662
+ }
663
+
664
+ fun unregisterMarker(marker: Marker) {
665
+ markerViewMap.remove(marker)
666
+ }
667
+
668
+ fun handleMarkerClick(marker: Marker): Boolean {
669
+ markerViewMap[marker]?.let { view ->
670
+ view.onMarkerPress.invoke(mapOf(
671
+ "latitude" to marker.position.latitude,
672
+ "longitude" to marker.position.longitude
673
+ ))
674
+ // 只有在没有自定义内容(children)且有 title 或 snippet 时才显示信息窗口
675
+ // 如果有自定义内容,说明用户已经自定义了显示内容,不需要默认信息窗口
676
+ return !(view.isEmpty() && (!marker.title.isNullOrEmpty() || !marker.snippet.isNullOrEmpty()))
677
+ // marker.showInfoWindow()
678
+ }
679
+ return false
680
+ }
681
+
682
+ fun handleMarkerDragStart(marker: Marker) {
683
+ markerViewMap[marker]?.onMarkerDragStart?.invoke(mapOf(
684
+ "latitude" to marker.position.latitude,
685
+ "longitude" to marker.position.longitude
686
+ ))
687
+ }
688
+
689
+ fun handleMarkerDrag(marker: Marker) {
690
+ markerViewMap[marker]?.onMarkerDrag?.invoke(mapOf(
691
+ "latitude" to marker.position.latitude,
692
+ "longitude" to marker.position.longitude
693
+ ))
694
+ }
695
+
696
+ fun handleMarkerDragEnd(marker: Marker) {
697
+ markerViewMap[marker]?.onMarkerDragEnd?.invoke(mapOf(
698
+ "latitude" to marker.position.latitude,
699
+ "longitude" to marker.position.longitude
700
+ ))
701
+ }
533
702
  }
534
-
535
- fun handleMarkerDragEnd(marker: Marker) {
536
- markerViewMap[marker]?.onMarkerDragEnd?.invoke(mapOf(
537
- "latitude" to marker.position.latitude,
538
- "longitude" to marker.position.longitude
539
- ))
703
+
704
+ /**
705
+ * 创建或更新标记
706
+ */
707
+ private fun createOrUpdateMarker(initialPosition: LatLng? = null) {
708
+ aMap?.let { map ->
709
+ if (marker == null) {
710
+ // 🔑 修复:如果没有任何坐标信息,暂不创建 Marker,等待坐标就绪
711
+ // 这确保 Marker 永远在正确的位置出生,彻底解决动画位移问题
712
+ val pos = initialPosition ?: pendingPosition ?: if (pendingLatitude != null && pendingLongitude != null) {
713
+ LatLng(pendingLatitude!!, pendingLongitude!!)
714
+ } else null
715
+
716
+ if (pos == null) {
717
+ return
718
+ }
719
+
720
+ hasAnimated = false // 重置动画状态
721
+ val options = MarkerOptions()
722
+ // 恢复默认的 visible(false),因为我们已经有了严谨的创建逻辑
723
+ // 如果需要动画,showMarker 会处理 visible
724
+ options.visible(false)
725
+ options.position(pos)
726
+
727
+ // 🔑 修复:设置初始锚点,避免动画时的位置跳变
728
+ // 如果是自定义 View(非空),默认锚点设为中心 (0.5, 0.5)
729
+ // 如果是默认大头针(空且无 icon/color),默认锚点设为底部中心 (0.5, 1.0)
730
+ val isDefaultMarker = isEmpty() && pendingIconUri == null && pendingPinColor == null
731
+ val defaultAnchorX = 0.5f
732
+ val defaultAnchorY = if (isDefaultMarker) 1.0f else 0.5f
733
+
734
+ val anchorX = pendingAnchor?.first ?: defaultAnchorX
735
+ val anchorY = pendingAnchor?.second ?: defaultAnchorY
736
+
737
+ options.anchor(anchorX, anchorY)
738
+
739
+ marker = map.addMarker(options)
740
+
741
+ // 注册到全局 map
742
+ marker?.let { m ->
743
+ registerMarker(m, this)
744
+
745
+ // 应用缓存的属性
746
+ pendingTitle?.let { m.title = it }
747
+ pendingSnippet?.let { m.snippet = it }
748
+ pendingDraggable?.let { m.isDraggable = it }
749
+ pendingOpacity?.let { m.alpha = it }
750
+ pendingFlat?.let { m.isFlat = it }
751
+ pendingZIndex?.let { m.zIndex = it }
752
+ pendingAnchor?.let { m.setAnchor(it.first, it.second) }
753
+
754
+ // 优先级:children > icon > pinColor
755
+ if (isEmpty()) {
756
+ if (pendingIconUri != null) {
757
+ loadAndSetIcon(pendingIconUri!!, m)
758
+ } else if (pendingPinColor != null) {
759
+ applyPinColor(pendingPinColor!!, m)
760
+ } else {
761
+ // 延迟检查,如果是默认 Marker 且没有子视图加入,才显示
762
+ mainHandler.post {
763
+ if (marker != null && isEmpty() && pendingIconUri == null && pendingPinColor == null) {
764
+ showMarker(m)
765
+ }
766
+ }
767
+ }
768
+ }
769
+ }
770
+ }
771
+ }
540
772
  }
541
- }
542
-
543
- /**
544
- * 创建或更新标记
545
- */
546
- private fun createOrUpdateMarker() {
547
- aMap?.let { map ->
548
- if (marker == null) {
549
- val options = MarkerOptions()
550
- marker = map.addMarker(options)
551
-
552
- // 注册到全局 map
553
- marker?.let { m ->
554
- registerMarker(m, this)
555
-
556
- // 应用缓存的属性
557
- pendingTitle?.let { m.title = it }
558
- pendingSnippet?.let { m.snippet = it }
559
- pendingDraggable?.let { m.isDraggable = it }
560
- pendingOpacity?.let { m.alpha = it }
561
- pendingFlat?.let { m.isFlat = it }
562
- pendingZIndex?.let { m.zIndex = it }
563
- pendingAnchor?.let { m.setAnchor(it.first, it.second) }
564
-
565
- // 优先级:children > icon > pinColor
566
- if (isEmpty()) {
567
- if (pendingIconUri != null) {
568
- loadAndSetIcon(pendingIconUri!!, m)
569
- } else if (pendingPinColor != null) {
570
- applyPinColor(pendingPinColor!!, m)
773
+
774
+
775
+ /**
776
+ * 将视图转换为 Bitmap
777
+ * 改良的 createBitmapFromView:支持缓存(IconBitmapCache)与稳定 fingerprint key。
778
+ * - 如果 view 为空或没有 children,直接返回 null(和你之前一致)
779
+ * - 首先尝试命中缓存 key(fingerprint + size)
780
+ * - 如果未命中,在 UI 线程进行 measure/layout/draw,生成 bitmap 并缓存
781
+ *
782
+ * 注意:render 会在 UI 线程执行;如果当前线程不是 UI 线程,会同步等待 UI 线程完成(有超时)。
783
+ */
784
+ private fun createBitmapFromView(): Bitmap? {
785
+ if (isEmpty()) return null
786
+
787
+ // 优先使用 JS 传入的 cacheKey,如果没有则 fallback 为 fingerprint
788
+ val keyPart = cacheKey ?: computeViewFingerprint(this)
789
+
790
+ val measuredChild = if (isNotEmpty()) getChildAt(0) else null
791
+ val measuredWidth = measuredChild?.measuredWidth ?: 0
792
+ val measuredHeight = measuredChild?.measuredHeight ?: 0
793
+
794
+ val finalWidth = if (measuredWidth > 0) measuredWidth else (if (customViewWidth > 0) customViewWidth else 0)
795
+ val finalHeight = if (measuredHeight > 0) measuredHeight else (if (customViewHeight > 0) customViewHeight else 0)
796
+
797
+ // 🔑 修复:如果尺寸为 0,说明 View 还没准备好,不要生成 Bitmap,否则会导致动画位置偏移
798
+ if (finalWidth <= 0 || finalHeight <= 0) {
799
+ return null
800
+ }
801
+
802
+ val fullCacheKey = "$keyPart|${finalWidth}x${finalHeight}"
803
+
804
+ // 1) 尝试缓存命中
805
+ IconBitmapCache.get(fullCacheKey)?.let { return it }
806
+
807
+ // 2) 未命中,则生成 bitmap(同之前逻辑)
808
+ val bitmap: Bitmap? = if (Looper.myLooper() == Looper.getMainLooper()) {
809
+ renderViewToBitmapInternal(finalWidth, finalHeight)
810
+ } else {
811
+ val latch = CountDownLatch(1)
812
+ var result: Bitmap? = null
813
+ mainHandler.post {
814
+ try {
815
+ result = renderViewToBitmapInternal(finalWidth, finalHeight)
816
+ } finally {
817
+ latch.countDown()
818
+ }
571
819
  }
572
- }
820
+ try { latch.await(200, TimeUnit.MILLISECONDS) } catch (_: InterruptedException) {}
821
+ result
573
822
  }
574
- }
823
+
824
+ bitmap?.let { IconBitmapCache.put(fullCacheKey, it) }
825
+ return bitmap
575
826
  }
576
- }
577
-
578
827
 
579
- /**
580
- * 将视图转换为 Bitmap
581
- */
582
- private fun createBitmapFromView(): Bitmap? {
583
- if (isEmpty()) return null
584
-
585
- return try {
586
- val childView = getChildAt(0)
587
- val measuredWidth = childView.measuredWidth
588
- val measuredHeight = childView.measuredHeight
589
-
590
- val finalWidth = if (measuredWidth > 0) measuredWidth else (if (customViewWidth > 0) customViewWidth else 240)
591
- val finalHeight = if (measuredHeight > 0) measuredHeight else (if (customViewHeight > 0) customViewHeight else 80)
592
828
 
593
- if (measuredWidth != finalWidth || measuredHeight != finalHeight) {
594
- childView.measure(
595
- MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
596
- MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
597
- )
598
- childView.layout(0, 0, finalWidth, finalHeight)
599
- }
600
-
601
- val bitmap = createBitmap(finalWidth, finalHeight)
602
- val canvas = Canvas(bitmap)
603
- canvas.drawColor(Color.TRANSPARENT)
604
- childView.draw(canvas)
605
-
606
- bitmap
607
- } catch (_: Exception) {
608
- null
829
+ /**
830
+ * 真正把 view measure/layout/draw 到 Bitmap 的内部方法(必须在主线程调用)
831
+ */
832
+ private fun renderViewToBitmapInternal(finalWidth: Int, finalHeight: Int): Bitmap? {
833
+ try {
834
+ val childView = if (isNotEmpty()) getChildAt(0) else return null
835
+
836
+
837
+ // 🔑 优化:如果 View 尺寸已经符合要求,直接复用现有布局,避免破坏 React Native 的排版
838
+ if (childView.width != finalWidth || childView.height != finalHeight) {
839
+ // 🔑 关键修复:如果子 View 还没完成布局(宽高为 0),不要强行 measure,这会导致布局错乱(如 0x0 -> 252x75)。
840
+ // 直接返回 null,等待下一次 layout(当子 View 准备好时会再次触发)。
841
+ if (childView.width == 0 || childView.height == 0) {
842
+ return null
843
+ }
844
+
845
+ // 使用给定的尺寸强制测量布局
846
+ val widthSpec = MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY)
847
+ val heightSpec = MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
848
+
849
+ // measure + layout
850
+ childView.measure(widthSpec, heightSpec)
851
+ childView.layout(0, 0, finalWidth, finalHeight)
852
+ } else {
853
+ // 如果复用布局,必须检查 left/top 是否为 0。如果不为 0,绘制到 bitmap 时会发生偏移。
854
+ // 很多时候 RN 会给 view 设置 left/top。
855
+ if (childView.left != 0 || childView.top != 0) {
856
+ childView.layout(0, 0, finalWidth, finalHeight)
857
+ }
858
+ }
859
+
860
+ // 🔑 修复:创建支持透明度的 bitmap 配置
861
+ val bitmap = createBitmap(finalWidth, finalHeight)
862
+ val canvas = Canvas(bitmap)
863
+
864
+ // 🔑 关键修复:强制启用 view 的绘制缓存,确保内容正确渲染
865
+ childView.isDrawingCacheEnabled = true
866
+ childView.buildDrawingCache(true)
867
+
868
+ // 绘制 view 到 canvas
869
+ childView.draw(canvas)
870
+
871
+ // 清理绘制缓存
872
+ childView.isDrawingCacheEnabled = false
873
+ childView.destroyDrawingCache()
874
+
875
+ return bitmap
876
+ } catch (_: Exception) {
877
+ // 遇到异常时返回 null,让上层使用默认图标
878
+ return null
879
+ }
609
880
  }
610
- }
611
-
612
881
 
613
- /**
614
- * 更新 marker 图标
615
- */
616
- private fun updateMarkerIcon() {
617
- if (isNotEmpty()) {
618
- val customBitmap = createBitmapFromView()
619
- customBitmap?.let {
620
- marker?.setIcon(BitmapDescriptorFactory.fromBitmap(it))
621
- marker?.setAnchor(0.5f, 1.0f)
622
- } ?: run {
623
- marker?.setIcon(BitmapDescriptorFactory.defaultMarker())
624
- }
625
- } else {
626
- marker?.setIcon(BitmapDescriptorFactory.defaultMarker())
627
- marker?.setAnchor(0.5f, 1.0f)
882
+ /**
883
+ * 更新 marker 图标
884
+ */
885
+ private fun updateMarkerIcon() {
886
+ if (isEmpty()) {
887
+ // 如果确实为空(没有子视图),恢复默认样式
888
+ marker?.setIcon(BitmapDescriptorFactory.defaultMarker())
889
+ // 恢复默认锚点(底部中心),除非用户指定了锚点
890
+ val anchorX = pendingAnchor?.first ?: 0.5f
891
+ val anchorY = pendingAnchor?.second ?: 1.0f
892
+ marker?.setAnchor(anchorX, anchorY)
893
+ marker?.let { showMarker(it) }
894
+ return
895
+ }
896
+
897
+ // 构建缓存 key(优先 JS 端 cacheKey)
898
+ val keyPart = cacheKey ?: computeViewFingerprint(this)
899
+ val child = getChildAt(0)
900
+ val measuredWidth = child?.measuredWidth ?: customViewWidth
901
+ val measuredHeight = child?.measuredHeight ?: customViewHeight
902
+ val fullCacheKey = "$keyPart|${measuredWidth}x${measuredHeight}"
903
+
904
+ // 确定锚点:优先使用用户指定的 pendingAnchor,否则对于自定义 View 使用中心点 (0.5, 0.5)
905
+ val anchorX = pendingAnchor?.first ?: 0.5f
906
+ val anchorY = pendingAnchor?.second ?: 0.5f
907
+
908
+ // 1) 尝试 BitmapDescriptor 缓存
909
+ BitmapDescriptorCache.get(fullCacheKey)?.let { it ->
910
+ marker?.setIcon(it)
911
+ marker?.setAnchor(anchorX, anchorY)
912
+ marker?.let { showMarker(it) }
913
+ return
914
+ }
915
+
916
+ // 2) Bitmap 缓存命中则生成 Descriptor,或者重新生成
917
+ val bitmap = IconBitmapCache.get(fullCacheKey) ?: createBitmapFromView() ?: run {
918
+ // 🔑 关键修复:如果生成 Bitmap 失败(例如 View 还没准备好)
919
+ // 不要急着切回默认 Marker,这会导致闪烁和位置跳变。
920
+ // 只有在 Marker 从未显示过的情况下,才考虑兜底策略。
921
+ if (marker?.isVisible != true) {
922
+ // 如果从未显示过,可以暂不显示,等待下一次尝试,或者显示默认(取决于需求)
923
+ // 这里选择暂不显示,避免闪现蓝点
924
+ }
925
+ return
926
+ }
927
+
928
+ // 生成并缓存 BitmapDescriptor
929
+ val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
930
+ BitmapDescriptorCache.putDescriptor(fullCacheKey, descriptor)
931
+
932
+ // 设置到 Marker
933
+ marker?.setIcon(descriptor)
934
+ marker?.setAnchor(anchorX, anchorY)
935
+ marker?.let { showMarker(it) }
628
936
  }
629
- }
630
937
 
631
-
632
- override fun removeView(child: View?) {
633
- try {
634
- if (child != null && contains(child)) {
635
- super.removeView(child)
636
- // 不要在这里恢复默认图标
637
- // 如果 MarkerView 整体要被移除,onDetachedFromWindow 会处理
638
- // 如果只是移除 children 并保留 Marker,应该由外部重新设置 children
639
- }
640
- } catch (_: Exception) {
641
- // 忽略异常
938
+
939
+
940
+ override fun removeView(child: View?) {
941
+ try {
942
+ if (child != null && contains(child)) {
943
+ super.removeView(child)
944
+ // 不要在这里恢复默认图标
945
+ // 如果 MarkerView 整体要被移除,onDetachedFromWindow 会处理
946
+ // 如果只是移除 children 并保留 Marker,应该由外部重新设置 children
947
+ }
948
+ } catch (_: Exception) {
949
+ // 忽略异常
950
+ }
642
951
  }
643
- }
644
-
645
- override fun removeViewAt(index: Int) {
646
- try {
647
- if (index in 0..<childCount) {
648
- super.removeViewAt(index)
649
- // 只在还有子视图时更新图标
650
- if (!isRemoving && childCount > 1 && marker != null) {
651
- mainHandler.postDelayed({
652
- if (!isRemoving && marker != null && isNotEmpty()) {
653
- updateMarkerIcon()
952
+
953
+ override fun removeViewAt(index: Int) {
954
+ try {
955
+ if (index in 0..<childCount) {
956
+ super.removeViewAt(index)
957
+ // 只在还有子视图时更新图标
958
+ if (!isRemoving && childCount > 1 && marker != null) {
959
+ mainHandler.postDelayed({
960
+ if (!isRemoving && marker != null && isNotEmpty()) {
961
+ updateMarkerIcon()
962
+ }
963
+ }, 50)
964
+ }
965
+ // 如果最后一个子视图被移除,什么都不做
966
+ // 让 onDetachedFromWindow 处理完整的清理
654
967
  }
655
- }, 50)
968
+ } catch (_: Exception) {
969
+ // 忽略异常
656
970
  }
657
- // 如果最后一个子视图被移除,什么都不做
658
- // 让 onDetachedFromWindow 处理完整的清理
659
- }
660
- } catch (_: Exception) {
661
- // 忽略异常
662
- }
663
- }
664
- /**
665
- * 递归修复子视图的 LayoutParams,确保所有子视图都使用正确的 LayoutParams 类型
666
- */
667
- private fun fixChildLayoutParams(view: View) {
668
- if (view is android.view.ViewGroup) {
669
- for (i in 0 until view.childCount) {
670
- val child = view.getChildAt(i)
671
- val currentParams = child.layoutParams
672
- if (currentParams != null && currentParams !is LayoutParams) {
673
- child.layoutParams = LayoutParams(
674
- currentParams.width,
675
- currentParams.height
971
+ }
972
+ /**
973
+ * 递归修复子视图的 LayoutParams,确保所有子视图都使用正确的 LayoutParams 类型
974
+ */
975
+ private fun fixChildLayoutParams(view: View) {
976
+ if (view is ViewGroup) {
977
+ for (i in 0 until view.childCount) {
978
+ val child = view.getChildAt(i)
979
+ val currentParams = child.layoutParams
980
+ if (currentParams != null && currentParams !is LayoutParams) {
981
+ child.layoutParams = LayoutParams(
982
+ currentParams.width,
983
+ currentParams.height
984
+ )
985
+ }
986
+ fixChildLayoutParams(child)
987
+ }
988
+ }
989
+ }
990
+
991
+
992
+ override fun addView(child: View?, index: Int, params: android.view.ViewGroup.LayoutParams?) {
993
+ // 🔑 关键修复:记录添加前的子视图数量
994
+ val childCountBefore = childCount
995
+
996
+ val finalParams = LayoutParams(
997
+ if (customViewWidth > 0) customViewWidth else LayoutParams.WRAP_CONTENT,
998
+ if (customViewHeight > 0) customViewHeight else LayoutParams.WRAP_CONTENT
999
+ )
1000
+
1001
+ super.addView(child, index, finalParams)
1002
+
1003
+ child?.let {
1004
+ val childParams = it.layoutParams
1005
+ if (childParams !is LayoutParams) {
1006
+ it.layoutParams = LayoutParams(
1007
+ childParams?.width ?: LayoutParams.WRAP_CONTENT,
1008
+ childParams?.height ?: LayoutParams.WRAP_CONTENT
676
1009
  )
677
1010
  }
678
- fixChildLayoutParams(child)
1011
+ fixChildLayoutParams(it)
679
1012
  }
680
- }
681
- }
682
-
683
-
684
- override fun addView(child: View?, index: Int, params: android.view.ViewGroup.LayoutParams?) {
685
- // 🔑 关键修复:记录添加前的子视图数量
686
- val childCountBefore = childCount
687
-
688
- val finalParams = LayoutParams(
689
- if (customViewWidth > 0) customViewWidth else LayoutParams.WRAP_CONTENT,
690
- if (customViewHeight > 0) customViewHeight else LayoutParams.WRAP_CONTENT
691
- )
692
-
693
- super.addView(child, index, finalParams)
694
-
695
- child?.let {
696
- val childParams = it.layoutParams
697
- if (childParams !is LayoutParams) {
698
- it.layoutParams = LayoutParams(
699
- childParams?.width ?: LayoutParams.WRAP_CONTENT,
700
- childParams?.height ?: LayoutParams.WRAP_CONTENT
701
- )
1013
+
1014
+ // 🔑 修复:需要延迟更新图标,等待 children 完成布局
1015
+ // 原因:立即更新会在 children 还未完成测量/布局时就渲染,导致内容为空
1016
+ if (!isRemoving && marker != null && childCount > childCountBefore) {
1017
+ mainHandler.post {
1018
+ if (!isRemoving && marker != null && isNotEmpty()) {
1019
+ updateMarkerIcon()
1020
+ }
1021
+ }
702
1022
  }
703
- fixChildLayoutParams(it)
704
1023
  }
705
-
706
- // 🔑 关键修复:只有在子视图数量真正变化且 marker 已存在时才更新图标
707
- // 避免在其他覆盖物添加时触发不必要的刷新
708
- if (!isRemoving && marker != null && childCount > childCountBefore) {
709
- mainHandler.postDelayed({
710
- if (!isRemoving && marker != null) {
711
- updateMarkerIcon()
712
- }
713
- }, 50)
714
-
715
- mainHandler.postDelayed({
716
- if (!isRemoving && marker != null) {
717
- updateMarkerIcon()
1024
+
1025
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
1026
+ super.onLayout(changed, left, top, right, bottom)
1027
+ // 🔑 修复:布局完成后延迟更新图标
1028
+ // 即使 changed 为 false,只要有内容,也应该检查是否需要更新(例如子 View 尺寸变化但 MarkerView 没变)
1029
+ if (!isRemoving && isNotEmpty() && marker != null) {
1030
+ mainHandler.post {
1031
+ if (!isRemoving && marker != null && isNotEmpty()) {
1032
+ updateMarkerIcon()
1033
+ }
718
1034
  }
719
- }, 150)
1035
+ }
720
1036
  }
721
- }
1037
+
722
1038
 
723
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
724
- super.onLayout(changed, left, top, right, bottom)
725
- // 🔑 关键修复:只有在有子视图且 marker 存在时才更新,避免不必要的刷新
726
- if (changed && !isRemoving && isNotEmpty() && marker != null) {
727
- mainHandler.postDelayed({
728
- if (!isRemoving && marker != null) {
729
- updateMarkerIcon()
1039
+
1040
+ /**
1041
+ * 设置平滑移动路径
1042
+ */
1043
+ fun setSmoothMovePath(path: List<Any>?) {
1044
+ try {
1045
+ // 转换为 LatLng 列表
1046
+ smoothMovePath = LatLngParser.parseLatLngList(path)
1047
+
1048
+ // 当路径和时长都设置时,启动平滑移动
1049
+ if (smoothMovePath?.isNotEmpty() == true && smoothMoveDuration > 0 && aMap != null) {
1050
+ startSmoothMove()
1051
+ }
1052
+ } catch (e: Exception) {
1053
+ android.util.Log.e("MarkerView", "setSmoothMovePath error", e)
730
1054
  }
731
- }, 200)
732
1055
  }
733
- }
734
-
735
- /**
736
- * 移除标记
737
- */
738
- fun removeMarker() {
739
- marker?.let {
740
- unregisterMarker(it)
741
- it.remove()
742
- }
743
- marker = null
744
- }
745
-
746
- override fun onDetachedFromWindow() {
747
- super.onDetachedFromWindow()
748
-
749
- // 🔑 关键修复:使用 post 延迟检查
750
- // 清理所有延迟任务
751
- mainHandler.removeCallbacksAndMessages(null)
752
-
753
- // 延迟检查 parent 状态
754
- mainHandler.post {
755
- if (parent == null) {
756
- // 标记正在移除
757
- isRemoving = true
758
- // 移除 marker
759
- removeMarker()
760
- }
1056
+
1057
+ /**
1058
+ * 设置平滑移动时长(秒)
1059
+ */
1060
+ fun setSmoothMoveDuration(duration: Double) {
1061
+ smoothMoveDuration = if (duration > 0) duration else 10.0
1062
+
1063
+ // 当路径和时长都设置时,启动平滑移动
1064
+ if (smoothMovePath?.isNotEmpty() == true && aMap != null) {
1065
+ startSmoothMove()
1066
+ }
761
1067
  }
762
- }
763
- }
764
1068
 
1069
+ /**
1070
+ * 启动平滑移动
1071
+ */
1072
+ private fun startSmoothMove() {
1073
+ val path = smoothMovePath ?: run {
1074
+ android.util.Log.e("MarkerView", "smoothMovePath is null")
1075
+ return
1076
+ }
1077
+ val map = aMap ?: run {
1078
+ android.util.Log.e("MarkerView", "aMap is null")
1079
+ return
1080
+ }
1081
+ (smoothMoveDuration * 1000).toInt() // 转换为毫秒
1082
+
1083
+
1084
+ mainHandler.post {
1085
+ try {
1086
+ // 创建或获取 MovingPointOverlay
1087
+ if (smoothMoveMarker == null) {
1088
+ // 创建一个专门用于平滑移动的内部 Marker
1089
+ val options = MarkerOptions()
1090
+ // 设置初始位置为当前位置或路径第一个点
1091
+ val initialPos = if (isNotEmpty()) {
1092
+ val currentLat = pendingLatitude ?: marker?.position?.latitude
1093
+ val currentLng = pendingLongitude ?: marker?.position?.longitude
1094
+ if (currentLat != null && currentLng != null) {
1095
+ LatLng(currentLat, currentLng)
1096
+ } else {
1097
+ path.first()
1098
+ }
1099
+ } else {
1100
+ path.first()
1101
+ }
1102
+ options.position(initialPos)
1103
+
1104
+ val internalMarker = map.addMarker(options)
1105
+ smoothMoveMarker = MovingPointOverlay(map, internalMarker)
1106
+
1107
+ // 设置图标 - 优先使用自定义 icon,其次使用 pinColor
1108
+ var iconSetSuccessfully = false
1109
+ try {
1110
+ // 优先:从原始 Marker 直接获取图标
1111
+ marker?.let { _ ->
1112
+ // 1. 尝试使用缓存的自定义 icon
1113
+ if (pendingIconUri != null) {
1114
+ // 尝试不同的缓存 key 格式
1115
+ val possibleKeys = listOfNotNull(
1116
+ cacheKey?.let { "$it|${iconWidth}x${iconHeight}" },
1117
+ "icon|$pendingIconUri|${iconWidth}x${iconHeight}",
1118
+ cacheKey,
1119
+ "icon|$pendingIconUri"
1120
+ )
1121
+
1122
+ for (key in possibleKeys) {
1123
+ if (iconSetSuccessfully) break
1124
+
1125
+ // 先尝试 BitmapDescriptorCache
1126
+ BitmapDescriptorCache.get(key)?.let { icon ->
1127
+ internalMarker.setIcon(icon)
1128
+ iconSetSuccessfully = true
1129
+ }
1130
+
1131
+ if (iconSetSuccessfully) break
1132
+
1133
+ // 再尝试 IconBitmapCache
1134
+ IconBitmapCache.get(key)?.let { bitmap ->
1135
+ val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
1136
+ internalMarker.setIcon(descriptor)
1137
+ iconSetSuccessfully = true
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ // 只有当自定义图标未设置成功时,才使用 pinColor
1144
+ if (!iconSetSuccessfully) {
1145
+ val color = pendingPinColor ?: "red"
1146
+ val hue = when (color.lowercase()) {
1147
+ "red" -> BitmapDescriptorFactory.HUE_RED
1148
+ "orange" -> BitmapDescriptorFactory.HUE_ORANGE
1149
+ "yellow" -> BitmapDescriptorFactory.HUE_YELLOW
1150
+ "green" -> BitmapDescriptorFactory.HUE_GREEN
1151
+ "cyan" -> BitmapDescriptorFactory.HUE_CYAN
1152
+ "blue" -> BitmapDescriptorFactory.HUE_BLUE
1153
+ "violet" -> BitmapDescriptorFactory.HUE_VIOLET
1154
+ "magenta" -> BitmapDescriptorFactory.HUE_MAGENTA
1155
+ "rose" -> BitmapDescriptorFactory.HUE_ROSE
1156
+ "purple" -> BitmapDescriptorFactory.HUE_VIOLET
1157
+ else -> BitmapDescriptorFactory.HUE_RED
1158
+ }
1159
+
1160
+ val icon = BitmapDescriptorFactory.defaultMarker(hue)
1161
+ internalMarker.setIcon(icon)
1162
+ }
1163
+ } catch (e: Exception) {
1164
+ android.util.Log.e("MarkerView", "Failed to set icon for smooth move", e)
1165
+ val defaultIcon = BitmapDescriptorFactory.defaultMarker()
1166
+ internalMarker.setIcon(defaultIcon)
1167
+ }
1168
+ }
1169
+
1170
+ // 获取内部 Marker
1171
+ val internalMarker = smoothMoveMarker?.getObject() as? Marker
1172
+
1173
+ // 停止之前的移动
1174
+ smoothMoveMarker?.stopMove()
1175
+
1176
+ // 计算路径的起始点(如果提供了 position,使用它作为起点)
1177
+ val startPoint = if (isNotEmpty()) {
1178
+ val currentLat = pendingLatitude ?: marker?.position?.latitude
1179
+ val currentLng = pendingLongitude ?: marker?.position?.longitude
1180
+ if (currentLat != null && currentLng != null) {
1181
+ LatLng(currentLat, currentLng)
1182
+ } else {
1183
+ path.first()
1184
+ }
1185
+ } else {
1186
+ path.first()
1187
+ }
1188
+
1189
+
1190
+ // 使用 C++ 优化计算路径中的最近点
1191
+ var adjustedPath: List<LatLng>? = null
1192
+ val nearestResult = GeometryUtils.getNearestPointOnPath(path, startPoint)
1193
+
1194
+ if (nearestResult != null) {
1195
+ val startIndex = nearestResult.index
1196
+ if (startIndex >= 0 && startIndex < path.size - 1) {
1197
+ val subPath = path.subList(startIndex + 1, path.size).toMutableList()
1198
+ subPath.add(0, nearestResult.point)
1199
+ adjustedPath = subPath
1200
+ }
1201
+ }
1202
+
1203
+ // 如果 C++ 计算失败,降级使用 SpatialRelationUtil
1204
+ if (adjustedPath == null) {
1205
+ val pair = SpatialRelationUtil.calShortestDistancePoint(path, startPoint)
1206
+ adjustedPath = path.subList(pair.first, path.size)
1207
+ }
1208
+
1209
+ if (adjustedPath.isEmpty()) {
1210
+ adjustedPath = path
1211
+ }
1212
+
1213
+
1214
+ // 🔑 关键修复:先设置内部 Marker 的位置
1215
+ internalMarker?.position = adjustedPath.first()
1216
+ smoothMoveMarker?.setVisible(true)
1217
+
1218
+ // 设置移动路径
1219
+ smoothMoveMarker?.setPoints(adjustedPath)
1220
+
1221
+ // 设置总时长(MovingPointOverlay 的 setTotalDuration 需要秒为单位)
1222
+ smoothMoveMarker?.setTotalDuration(smoothMoveDuration.toInt())
1223
+
1224
+ // 开始平滑移动
1225
+ smoothMoveMarker?.startSmoothMove()
1226
+
1227
+ // 隐藏原始 Marker,避免重复显示
1228
+ marker?.isVisible = false
1229
+ } catch (e: Exception) {
1230
+ android.util.Log.e("MarkerView", "Start smooth move failed", e)
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * 停止平滑移动
1237
+ */
1238
+ private fun stopSmoothMove() {
1239
+ smoothMoveMarker?.stopMove()
1240
+ smoothMoveMarker?.setVisible(false)
1241
+ marker?.let { showMarker(it) }
1242
+ }
1243
+
1244
+ /**
1245
+ * 移除标记
1246
+ */
1247
+ fun removeMarker() {
1248
+ // 停止平滑移动
1249
+ stopSmoothMove()
1250
+ smoothMoveMarker?.destroy()
1251
+ smoothMoveMarker = null
1252
+
1253
+ marker?.let {
1254
+ unregisterMarker(it)
1255
+ it.remove()
1256
+ }
1257
+ marker = null
1258
+ }
1259
+
1260
+ override fun onDetachedFromWindow() {
1261
+ super.onDetachedFromWindow()
1262
+
1263
+ // 🔑 关键修复:使用 post 延迟检查
1264
+ // 清理所有延迟任务
1265
+ mainHandler.removeCallbacksAndMessages(null)
1266
+
1267
+ // 延迟检查 parent 状态
1268
+ mainHandler.post {
1269
+ if (parent == null) {
1270
+ // 标记正在移除
1271
+ isRemoving = true
1272
+
1273
+ // 🔑 修复:不要清空全局缓存
1274
+ // 理由:会影响其他 Marker 的性能
1275
+ // 缓存应该由 LruCache 自动管理,或在合适的时机(如内存警告)统一清理
1276
+
1277
+ // 移除 marker
1278
+ removeMarker()
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+
1284
+ /**
1285
+ * 为 view 和其子树生成一个轻量“指纹”字符串,用作缓存 key。
1286
+ * 注意:这是启发式的,不追求 100% 唯一性,但在大部分自定义 view 场景下能稳定复用。
1287
+ */
1288
+ fun computeViewFingerprint(view: View?): String {
1289
+ if (view == null) return "null"
1290
+
1291
+ val sb = StringBuilder()
1292
+ // 首先尝试使用开发者可能预设的 tag 或 contentDescription 作为优先标识(稳定且快速)
1293
+ val tag = view.tag
1294
+ if (tag != null) {
1295
+ sb.append("tag=").append(tag.toString()).append(";")
1296
+ return sb.toString()
1297
+ }
1298
+
1299
+ val contentDesc = view.contentDescription
1300
+ if (!contentDesc.isNullOrEmpty()) {
1301
+ sb.append("cdesc=").append(contentDesc.toString()).append(";")
1302
+ return sb.toString()
1303
+ }
1304
+
1305
+ // 否则做一个递归采样:className + 对于 TextView 获取 text + 对于 ImageView 获取 resourceId 或 drawable hash
1306
+ fun appendFor(v: View) {
1307
+ sb.append(v.javaClass.simpleName)
1308
+ when (v) {
1309
+ is TextView -> {
1310
+ val t = v.text?.toString() ?: ""
1311
+ if (t.isNotEmpty()) {
1312
+ sb.append("[text=").append(t).append("]")
1313
+ }
1314
+ }
1315
+ is ImageView -> {
1316
+ // 尝试读取资源 id(若使用 setImageResource 时可取到),否则取 drawable 的 hashCode 作为近似
1317
+ val resId = v.tag // 开发者可将资源 id 放到 tag 以便稳定识别
1318
+ if (resId is Int && resId != 0) {
1319
+ sb.append("[imgRes=").append(resId).append("]")
1320
+ } else {
1321
+ val dr = v.drawable
1322
+ if (dr != null) {
1323
+ sb.append("[drawableHash=").append(dr.hashCode()).append("]")
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1328
+ sb.append(";")
1329
+ if (v is ViewGroup) {
1330
+ for (i in 0 until v.childCount) {
1331
+ val c = v.getChildAt(i)
1332
+ appendFor(c)
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ appendFor(view)
1338
+ // 最终返回一个截断的 sha-like 形式(避免 key 过长)
1339
+ return sb.toString().take(1024)
1340
+ }
1341
+ }