expo-gaode-map 2.2.33 → 2.2.34

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 (57) hide show
  1. package/README.md +20 -14
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +14 -0
  4. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +7 -8
  5. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +150 -27
  6. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +24 -14
  7. package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +38 -41
  8. package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +18 -17
  9. package/android/src/main/java/expo/modules/gaodemap/overlays/CircleView.kt +3 -1
  10. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +6 -1
  11. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +124 -10
  12. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +2 -2
  13. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +10 -9
  14. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +7 -11
  15. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +3 -1
  16. package/android/src/main/java/expo/modules/gaodemap/overlays/PolygonView.kt +2 -1
  17. package/android/src/main/java/expo/modules/gaodemap/overlays/PolylineView.kt +1 -0
  18. package/android/src/main/java/expo/modules/gaodemap/search/ExpoGaodeMapSearchModule.kt +751 -0
  19. package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +5 -5
  20. package/android/src/main/java/expo/modules/gaodemap/utils/PermissionHelper.kt +13 -16
  21. package/build/ExpoGaodeMapOfflineModule.d.ts +5 -0
  22. package/build/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  23. package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
  24. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  25. package/build/components/overlays/HeatMap.js +21 -2
  26. package/build/components/overlays/HeatMap.js.map +1 -1
  27. package/build/index.d.ts +3 -0
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +3 -0
  30. package/build/index.js.map +1 -1
  31. package/build/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  32. package/build/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  33. package/build/search/ExpoGaodeMapSearch.types.js +19 -0
  34. package/build/search/ExpoGaodeMapSearch.types.js.map +1 -0
  35. package/build/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  36. package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  37. package/build/search/ExpoGaodeMapSearchModule.js +47 -0
  38. package/build/search/ExpoGaodeMapSearchModule.js.map +1 -0
  39. package/build/search/index.d.ts +156 -0
  40. package/build/search/index.d.ts.map +1 -0
  41. package/build/search/index.js +171 -0
  42. package/build/search/index.js.map +1 -0
  43. package/build/types/map-view.types.d.ts +4 -2
  44. package/build/types/map-view.types.d.ts.map +1 -1
  45. package/build/types/map-view.types.js.map +1 -1
  46. package/build/utils/OfflineMapManager.d.ts +4 -0
  47. package/build/utils/OfflineMapManager.d.ts.map +1 -1
  48. package/build/utils/OfflineMapManager.js +6 -0
  49. package/build/utils/OfflineMapManager.js.map +1 -1
  50. package/expo-module.config.json +4 -2
  51. package/ios/ExpoGaodeMap.podspec +2 -2
  52. package/ios/ExpoGaodeMapOfflineModule.swift +60 -0
  53. package/ios/ExpoGaodeMapSearchModule.swift +773 -0
  54. package/ios/modules/LocationManager.swift +9 -3
  55. package/ios/overlays/PolylineView.swift +6 -12
  56. package/package.json +1 -1
  57. package/plugin/build/withGaodeMap.js +12 -0
@@ -87,36 +87,46 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
87
87
  // ==================== 图层显示 ====================
88
88
 
89
89
  private var currentLocationStyle: MyLocationStyle? = null
90
+ private var currentFollowUserLocation: Boolean = false
91
+ private var explicitLocationType: Int? = null
92
+
93
+ private fun parseLocationType(locationType: String): Int? {
94
+ return when (locationType) {
95
+ "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
96
+ "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
97
+ "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
98
+ "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
99
+ "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
100
+ "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
101
+ "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
102
+ "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
103
+ else -> null
104
+ }
105
+ }
106
+
107
+ private fun resolveLocationType(followUserLocation: Boolean): Int {
108
+ explicitLocationType?.let { return it }
109
+ return if (followUserLocation) {
110
+ MyLocationStyle.LOCATION_TYPE_FOLLOW
111
+ } else {
112
+ MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
113
+ }
114
+ }
90
115
 
91
116
  /**
92
117
  * 设置是否显示用户位置
93
118
  */
94
119
  fun setShowsUserLocation(show: Boolean, followUserLocation: Boolean = false) {
120
+ currentFollowUserLocation = followUserLocation
95
121
  if (show) {
96
- // 创建默认的定位样式
97
122
  if (currentLocationStyle == null) {
98
- currentLocationStyle = MyLocationStyle().apply {
99
- // 根据是否跟随设置定位类型
100
- val locationType = if (followUserLocation) {
101
- MyLocationStyle.LOCATION_TYPE_FOLLOW // 连续定位并跟随
102
- } else {
103
- MyLocationStyle.LOCATION_TYPE_SHOW // 只显示定位点,不跟随
104
- }
105
- myLocationType(locationType)
106
- interval(2000) // 2秒定位一次
107
- showMyLocation(true)
108
- }
109
- } else {
110
- // 更新定位类型
111
- val locationType = if (followUserLocation) {
112
- MyLocationStyle.LOCATION_TYPE_FOLLOW
113
- } else {
114
- MyLocationStyle.LOCATION_TYPE_SHOW
115
- }
116
- currentLocationStyle?.apply {
117
- myLocationType(locationType)
118
- interval(2000)
119
- }
123
+ currentLocationStyle = MyLocationStyle()
124
+ }
125
+
126
+ currentLocationStyle?.apply {
127
+ myLocationType(resolveLocationType(followUserLocation))
128
+ interval(2000) // 2秒定位一次
129
+ showMyLocation(true)
120
130
  }
121
131
 
122
132
  // 监听定位变化(用于通知 React Native)
@@ -171,9 +181,10 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
171
181
  */
172
182
  @SuppressLint("DiscouragedApi")
173
183
  fun setUserLocationRepresentation(config: Map<String, Any>) {
184
+ explicitLocationType = (config["locationType"] as? String)?.let(::parseLocationType)
185
+
174
186
  if (currentLocationStyle == null) {
175
187
  currentLocationStyle = MyLocationStyle().apply {
176
- myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE)
177
188
  interval(2000)
178
189
  showMyLocation(true)
179
190
  }
@@ -182,21 +193,7 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
182
193
  val style = currentLocationStyle!!
183
194
 
184
195
  // 定位蓝点展现模式 (locationType) - Android 支持8种模式
185
- val locationType = config["locationType"] as? String
186
- if (locationType != null) {
187
- val locationTypeValue = when (locationType) {
188
- "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
189
- "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
190
- "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
191
- "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
192
- "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
193
- "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
194
- "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
195
- "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
196
- else -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE // 默认值
197
- }
198
- style.myLocationType(locationTypeValue)
199
- }
196
+ style.myLocationType(resolveLocationType(currentFollowUserLocation))
200
197
 
201
198
  // 是否显示定位蓝点 (showMyLocation) - Android 5.1.0+ 支持
202
199
  // 注意:这个属性在 iOS 中没有对应项,是 Android 特有的
@@ -240,7 +237,7 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
240
237
 
241
238
  // 自定义图标 (image)
242
239
  val imagePath = config["image"] as? String
243
- if (imagePath != null && imagePath.isNotEmpty()) {
240
+ if (!imagePath.isNullOrEmpty()) {
244
241
  val density = context.resources.displayMetrics.density
245
242
  val imageWidth = (config["imageWidth"] as? Number)?.let { (it.toFloat() * density).toInt() }
246
243
  val imageHeight = (config["imageHeight"] as? Number)?.let { (it.toFloat() * density).toInt() }
@@ -357,7 +354,7 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
357
354
  fun setMapType(type: Int) {
358
355
  aMap.mapType = when (type) {
359
356
  2 -> AMap.MAP_TYPE_SATELLITE // 卫星地图
360
- 3 -> AMap.MAP_TYPE_NIGHT // 夜间地图
357
+ 3 -> AMap.MAP_TYPE_NIGHT // 黑夜地图
361
358
  4 -> AMap.MAP_TYPE_NAVI // 导航地图
362
359
  5 -> AMap.MAP_TYPE_BUS // 公交地图
363
360
  else -> AMap.MAP_TYPE_NORMAL // 标准地图 (1, 以及兼容旧值 0)
@@ -3,6 +3,7 @@ package expo.modules.gaodemap.modules
3
3
  import android.content.Context
4
4
  import com.amap.api.location.AMapLocationClient
5
5
  import com.amap.api.maps.MapsInitializer
6
+ import androidx.core.content.edit
6
7
 
7
8
  /**
8
9
  * SDK 初始化管理器
@@ -14,7 +15,7 @@ import com.amap.api.maps.MapsInitializer
14
15
  * - 获取 SDK 版本信息
15
16
  */
16
17
  object SDKInitializer {
17
- private const val PREFS_NAME = "expo_gaodemap_privacy"
18
+ private const val PREFS_NAME = "expo_demagogue_privacy"
18
19
  private const val KEY_PRIVACY_SHOWN = "privacy_shown"
19
20
  private const val KEY_PRIVACY_CONTAINS = "privacy_contains"
20
21
  private const val KEY_PRIVACY_AGREED = "privacy_agreed"
@@ -170,13 +171,13 @@ object SDKInitializer {
170
171
  }
171
172
 
172
173
  private fun persistState(context: Context) {
173
- prefs(context).edit()
174
- .putBoolean(KEY_PRIVACY_SHOWN, privacyShown)
175
- .putBoolean(KEY_PRIVACY_CONTAINS, privacyContains)
176
- .putBoolean(KEY_PRIVACY_AGREED, privacyAgreed)
177
- .putString(KEY_PRIVACY_VERSION, privacyVersion)
178
- .putString(KEY_AGREED_PRIVACY_VERSION, agreedPrivacyVersion)
179
- .apply()
174
+ prefs(context).edit {
175
+ putBoolean(KEY_PRIVACY_SHOWN, privacyShown)
176
+ .putBoolean(KEY_PRIVACY_CONTAINS, privacyContains)
177
+ .putBoolean(KEY_PRIVACY_AGREED, privacyAgreed)
178
+ .putString(KEY_PRIVACY_VERSION, privacyVersion)
179
+ .putString(KEY_AGREED_PRIVACY_VERSION, agreedPrivacyVersion)
180
+ }
180
181
  }
181
182
 
182
183
  private fun clearConsentPersistedState(context: Context, keepCurrentVersion: Boolean) {
@@ -185,19 +186,19 @@ object SDKInitializer {
185
186
  privacyAgreed = false
186
187
  agreedPrivacyVersion = null
187
188
 
188
- val editor = prefs(context).edit()
189
- .putBoolean(KEY_PRIVACY_SHOWN, false)
189
+ prefs(context).edit {
190
+ putBoolean(KEY_PRIVACY_SHOWN, false)
190
191
  .putBoolean(KEY_PRIVACY_CONTAINS, false)
191
192
  .putBoolean(KEY_PRIVACY_AGREED, false)
192
193
  .remove(KEY_AGREED_PRIVACY_VERSION)
193
194
 
194
- if (keepCurrentVersion) {
195
- editor.putString(KEY_PRIVACY_VERSION, privacyVersion)
196
- } else {
197
- privacyVersion = null
198
- editor.remove(KEY_PRIVACY_VERSION)
199
- }
195
+ if (keepCurrentVersion) {
196
+ putString(KEY_PRIVACY_VERSION, privacyVersion)
197
+ } else {
198
+ privacyVersion = null
199
+ remove(KEY_PRIVACY_VERSION)
200
+ }
200
201
 
201
- editor.apply()
202
+ }
202
203
  }
203
204
  }
@@ -1,5 +1,6 @@
1
1
  package expo.modules.gaodemap.overlays
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import expo.modules.gaodemap.utils.LatLngParser
4
5
  import android.content.Context
5
6
  import android.graphics.Color
@@ -12,6 +13,7 @@ import expo.modules.kotlin.AppContext
12
13
  import expo.modules.kotlin.viewevent.EventDispatcher
13
14
  import expo.modules.kotlin.views.ExpoView
14
15
 
16
+ @SuppressLint("ViewConstructor")
15
17
  class CircleView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
16
18
 
17
19
  @Suppress("unused")
@@ -155,7 +157,7 @@ class CircleView(context: Context, appContext: AppContext) : ExpoView(context, a
155
157
  // 如果是真正的移除,parent 会保持为 null
156
158
  // 如果只是 TabView 切换,parent 会在短时间内恢复
157
159
  post {
158
- // 延迟后再次检查 parent,如果仍然为 null,说明是真正的移除
160
+ // 延迟后再次检查 parent,如果仍然为 null,说明是真正移除
159
161
  if (parent == null) {
160
162
  removeCircle()
161
163
  aMap = null
@@ -37,6 +37,7 @@ import java.util.concurrent.ConcurrentHashMap
37
37
  * 点聚合视图
38
38
  * 实现真正的点聚合逻辑,支持自定义样式和点击事件
39
39
  */
40
+ @SuppressLint("ViewConstructor")
40
41
  class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext), AMap.OnCameraChangeListener {
41
42
 
42
43
 
@@ -139,7 +140,11 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
139
140
  * 设置最小聚合数量
140
141
  */
141
142
  fun setMinClusterSize(size: Int) {
142
- minClusterSize = size
143
+ val nextSize = size.coerceAtLeast(1)
144
+ if (minClusterSize != nextSize) {
145
+ minClusterSize = nextSize
146
+ styleChanged = true
147
+ }
143
148
  updateClusters()
144
149
  }
145
150
 
@@ -1,21 +1,29 @@
1
1
  package expo.modules.gaodemap.overlays
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
5
+ import android.graphics.Color
4
6
  import android.os.Looper
5
7
  import android.util.Log
6
8
  import com.amap.api.maps.AMap
9
+ import com.amap.api.maps.CameraUpdateFactory
10
+ import com.amap.api.maps.model.Gradient
11
+ import com.amap.api.maps.model.HeatmapTileProvider
7
12
  import com.amap.api.maps.model.LatLng
8
13
  import com.amap.api.maps.model.TileOverlay
9
14
  import com.amap.api.maps.model.TileOverlayOptions
10
- import com.amap.api.maps.model.HeatmapTileProvider
11
- import com.amap.api.maps.CameraUpdateFactory
15
+ import com.amap.api.maps.model.WeightedLatLng
12
16
  import expo.modules.kotlin.AppContext
13
17
  import expo.modules.kotlin.views.ExpoView
18
+ import expo.modules.gaodemap.utils.LatLngParser
14
19
  import java.util.concurrent.ExecutorService
15
20
  import java.util.concurrent.Executors
16
- import expo.modules.gaodemap.utils.LatLngParser
17
21
 
22
+ @SuppressLint("ViewConstructor")
18
23
  class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
24
+ private companion object {
25
+ const val TAG = "HeatMapView"
26
+ }
19
27
 
20
28
  private val executor: ExecutorService = Executors.newSingleThreadExecutor()
21
29
  private val applyUpdateRunnable = Runnable { applyUpdateOnMain() }
@@ -25,9 +33,10 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
25
33
 
26
34
  private var heatmapOverlay: TileOverlay? = null
27
35
  private var aMap: AMap? = null
28
- private var dataList: MutableList<LatLng> = mutableListOf()
36
+ private var dataList: MutableList<WeightedLatLng> = mutableListOf()
29
37
  private var radius: Int = 50
30
38
  private var opacity: Double = 0.6
39
+ private var gradient: Gradient? = null
31
40
 
32
41
  /**
33
42
  * 设置地图实例
@@ -36,6 +45,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
36
45
  fun setMap(map: AMap) {
37
46
  aMap = map
38
47
  needsRebuild = true
48
+ Log.d(TAG, "setMap: map attached")
39
49
  scheduleUpdate()
40
50
  }
41
51
 
@@ -46,7 +56,8 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
46
56
  */
47
57
  fun setData(data: List<Any>?) {
48
58
  dataList.clear()
49
- dataList.addAll(LatLngParser.parseLatLngList(data))
59
+ dataList.addAll(parseWeightedLatLngList(data))
60
+ Log.d(TAG, "setData: raw=${data?.size ?: 0}, parsed=${dataList.size}, ${formatPointStats(dataList)}")
50
61
  needsRebuild = true
51
62
  scheduleUpdate()
52
63
  }
@@ -56,6 +67,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
56
67
  */
57
68
  fun setRadius(radiusValue: Int) {
58
69
  radius = radiusValue
70
+ Log.d(TAG, "setRadius: $radiusValue")
59
71
  needsRebuild = true
60
72
  scheduleUpdate()
61
73
  }
@@ -65,10 +77,20 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
65
77
  */
66
78
  fun setOpacity(opacityValue: Double) {
67
79
  opacity = opacityValue
68
- applyOverlayOpacity()
80
+ Log.d(TAG, "setOpacity: $opacityValue")
81
+ needsRebuild = true
82
+ scheduleUpdate()
83
+ }
84
+
85
+ fun setGradient(gradientValue: Map<String, Any>?) {
86
+ gradient = parseGradient(gradientValue)
87
+ Log.d(TAG, "setGradient: hasGradient=${gradient != null}, raw=${gradientValue != null}")
88
+ needsRebuild = true
89
+ scheduleUpdate()
69
90
  }
70
91
 
71
92
  fun setVisible(visibleValue: Boolean) {
93
+ Log.d(TAG, "setVisible: $visibleValue, points=${dataList.size}, hasOverlay=${heatmapOverlay != null}, needsRebuild=$needsRebuild")
72
94
  if (!visibleValue) {
73
95
  visible = false
74
96
  updateToken += 1
@@ -95,6 +117,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
95
117
  private fun scheduleUpdate() {
96
118
  updateToken += 1
97
119
  removeCallbacks(applyUpdateRunnable)
120
+ Log.d(TAG, "scheduleUpdate: token=$updateToken, mapAttached=${aMap != null}, visible=$visible, points=${dataList.size}")
98
121
  postDelayed(applyUpdateRunnable, 32)
99
122
  }
100
123
 
@@ -142,8 +165,11 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
142
165
  val map = aMap ?: return
143
166
  val token = updateToken
144
167
  val pointsSnapshot = ArrayList(dataList)
168
+ val latLngSnapshot = pointsSnapshot.map { it.latLng }
145
169
  val radiusValue = radius.coerceIn(10, 200)
146
170
  val opacityValue = opacity.coerceIn(0.0, 1.0)
171
+ val gradientValue = gradient
172
+ Log.d(TAG, "applyUpdate: token=$token, visible=$visible, points=${pointsSnapshot.size}, radius=$radiusValue, opacity=$opacityValue, gradient=${gradientValue != null}, ${formatPointStats(pointsSnapshot)}")
147
173
 
148
174
  if (!visible) {
149
175
  applyOverlayVisibility()
@@ -151,6 +177,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
151
177
  }
152
178
 
153
179
  if (pointsSnapshot.isEmpty()) {
180
+ Log.w(TAG, "applyUpdate: no valid heatmap points, removing overlay")
154
181
  heatmapOverlay?.remove()
155
182
  heatmapOverlay = null
156
183
  return
@@ -164,19 +191,25 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
164
191
 
165
192
  executor.execute {
166
193
  try {
167
- val provider = HeatmapTileProvider.Builder()
168
- .data(pointsSnapshot)
194
+ val builder = HeatmapTileProvider.Builder()
195
+ .data(latLngSnapshot)
169
196
  .radius(radiusValue)
170
- .build()
197
+
198
+ gradientValue?.let { builder.gradient(it) }
199
+
200
+ val provider = builder.build()
171
201
 
172
202
  post {
173
203
  if (token != updateToken) {
204
+ Log.d(TAG, "addOverlay skipped: stale token=$token, current=$updateToken")
174
205
  return@post
175
206
  }
176
207
  if (aMap !== map) {
208
+ Log.d(TAG, "addOverlay skipped: map instance changed")
177
209
  return@post
178
210
  }
179
211
  if (!visible) {
212
+ Log.d(TAG, "addOverlay skipped: hidden")
180
213
  return@post
181
214
  }
182
215
 
@@ -200,9 +233,10 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
200
233
  applyOverlayVisibility()
201
234
  applyOverlayOpacity()
202
235
  forceRefresh()
236
+ Log.i(TAG, "addOverlay success: id=${runCatching { heatmapOverlay?.id }.getOrNull()}, points=${pointsSnapshot.size}, radius=$radiusValue, opacity=$opacityValue")
203
237
  }
204
238
  } catch (t: Throwable) {
205
- Log.e("HeatMapView", "Failed to build heatmap", t)
239
+ Log.e(TAG, "Failed to build heatmap", t)
206
240
  }
207
241
  }
208
242
  }
@@ -217,11 +251,91 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
217
251
  fun removeHeatMap() {
218
252
  updateToken += 1
219
253
  removeCallbacks(applyUpdateRunnable)
254
+ Log.d(TAG, "removeHeatMap")
220
255
  heatmapOverlay?.remove()
221
256
  heatmapOverlay = null
222
257
  dataList.clear()
223
258
  needsRebuild = true
224
259
  }
260
+
261
+ private fun parseWeightedLatLngList(data: Any?): List<WeightedLatLng> {
262
+ if (data == null || data !is List<*>) return emptyList()
263
+
264
+ val result = mutableListOf<WeightedLatLng>()
265
+ for (item in data) {
266
+ val point = parseWeightedLatLng(item)
267
+ if (point != null) {
268
+ result.add(point)
269
+ } else if (item is List<*>) {
270
+ result.addAll(parseWeightedLatLngList(item))
271
+ }
272
+ }
273
+ return result
274
+ }
275
+
276
+ private fun parseWeightedLatLng(data: Any?): WeightedLatLng? {
277
+ val latLng = LatLngParser.parseLatLng(data) ?: return null
278
+ val intensity = when (data) {
279
+ is Map<*, *> -> listOf(data["intensity"], data["weight"], data["count"], data["value"])
280
+ .firstOrNull { it is Number }
281
+ is List<*> -> data.getOrNull(2)
282
+ else -> null
283
+ }
284
+ val weight = ((intensity as? Number)?.toDouble() ?: 1.0).takeIf { it.isFinite() && it > 0.0 } ?: 1.0
285
+ return WeightedLatLng(latLng, weight)
286
+ }
287
+
288
+ private fun parseGradient(gradientValue: Map<String, Any>?): Gradient? {
289
+ if (gradientValue == null) return null
290
+ val rawColors = gradientValue["colors"] as? List<*> ?: return null
291
+ val rawStartPoints = gradientValue["startPoints"] as? List<*> ?: return null
292
+ if (rawColors.size < 2 || rawColors.size != rawStartPoints.size) {
293
+ Log.w(TAG, "parseGradient ignored: colors=${rawColors.size}, startPoints=${rawStartPoints.size}")
294
+ return null
295
+ }
296
+
297
+ val colors = IntArray(rawColors.size)
298
+ val startPoints = FloatArray(rawStartPoints.size)
299
+ for (index in rawColors.indices) {
300
+ val color = parseColor(rawColors[index]) ?: run {
301
+ Log.w(TAG, "parseGradient ignored: invalid color at index=$index, value=${rawColors[index]}")
302
+ return null
303
+ }
304
+ val startPoint = (rawStartPoints[index] as? Number)?.toFloat() ?: run {
305
+ Log.w(TAG, "parseGradient ignored: invalid startPoint at index=$index, value=${rawStartPoints[index]}")
306
+ return null
307
+ }
308
+ colors[index] = color
309
+ startPoints[index] = startPoint.coerceIn(0f, 1f)
310
+ }
311
+ return Gradient(colors, startPoints)
312
+ }
313
+
314
+ private fun parseColor(value: Any?): Int? {
315
+ return when (value) {
316
+ is Number -> value.toInt()
317
+ is String -> runCatching { Color.parseColor(value) }.getOrNull()
318
+ else -> null
319
+ }
320
+ }
321
+
322
+ private fun formatPointStats(points: List<WeightedLatLng>): String {
323
+ if (points.isEmpty()) return "bounds=empty"
324
+ var minLat = Double.POSITIVE_INFINITY
325
+ var maxLat = Double.NEGATIVE_INFINITY
326
+ var minLng = Double.POSITIVE_INFINITY
327
+ var maxLng = Double.NEGATIVE_INFINITY
328
+ var totalIntensity = 0.0
329
+ points.forEach { point ->
330
+ val latLng: LatLng = point.latLng
331
+ minLat = minOf(minLat, latLng.latitude)
332
+ maxLat = maxOf(maxLat, latLng.latitude)
333
+ minLng = minOf(minLng, latLng.longitude)
334
+ maxLng = maxOf(maxLng, latLng.longitude)
335
+ totalIntensity += point.intensity
336
+ }
337
+ return "bounds=[$minLat,$minLng]-[$maxLat,$maxLng], totalIntensity=$totalIntensity"
338
+ }
225
339
 
226
340
  override fun onDetachedFromWindow() {
227
341
  super.onDetachedFromWindow()
@@ -27,8 +27,8 @@ class HeatMapViewModule : Module() {
27
27
  view.setOpacity(opacity)
28
28
  }
29
29
 
30
- Prop<Map<String, Any>?>("gradient") { _: HeatMapView, _ ->
31
- // iOS only, ignore on Android
30
+ Prop<Map<String, Any>?>("gradient") { view: HeatMapView, gradient ->
31
+ view.setGradient(gradient)
32
32
  }
33
33
 
34
34
  Prop<Boolean>("allowRetinaAdapting") { _: HeatMapView, _ ->
@@ -15,6 +15,9 @@ import expo.modules.gaodemap.companion.IconBitmapCache
15
15
  import java.util.concurrent.CountDownLatch
16
16
  import java.util.concurrent.TimeUnit
17
17
  import kotlin.text.StringBuilder
18
+ import androidx.core.view.isEmpty
19
+ import androidx.core.graphics.get
20
+ import androidx.core.view.isNotEmpty
18
21
 
19
22
  internal data class MarkerBitmapSnapshot(
20
23
  val keyPart: String,
@@ -55,7 +58,7 @@ internal object MarkerBitmapRenderer {
55
58
  contentHeight: Int,
56
59
  cacheKey: String?,
57
60
  ): MarkerBitmapSnapshot? {
58
- if (container.childCount == 0) {
61
+ if (container.isEmpty()) {
59
62
  return null
60
63
  }
61
64
 
@@ -69,12 +72,10 @@ internal object MarkerBitmapRenderer {
69
72
  contentBounds?.width()
70
73
  ?: contentView?.measuredWidth
71
74
  ?: child.measuredWidth
72
- ?: 0
73
75
  val measuredHeight =
74
76
  contentBounds?.height()
75
77
  ?: contentView?.measuredHeight
76
78
  ?: child.measuredHeight
77
- ?: 0
78
79
 
79
80
  val finalWidth = if (measuredWidth > 0) measuredWidth else contentWidth
80
81
  val finalHeight = if (measuredHeight > 0) measuredHeight else contentHeight
@@ -146,7 +147,7 @@ internal object MarkerBitmapRenderer {
146
147
 
147
148
  var resolvedBounds: Rect? = null
148
149
 
149
- if (view is ViewGroup && view.childCount > 0) {
150
+ if (view is ViewGroup && view.isNotEmpty()) {
150
151
  for (i in 0 until view.childCount) {
151
152
  val child = view.getChildAt(i) ?: continue
152
153
  val childBounds = computeContentBounds(child) ?: continue
@@ -308,7 +309,7 @@ internal object MarkerBitmapRenderer {
308
309
 
309
310
  for (y in 0 until bitmap.height) {
310
311
  for (x in 0 until bitmap.width) {
311
- if (Color.alpha(bitmap.getPixel(x, y)) != 0) {
312
+ if (Color.alpha(bitmap[x, y]) != 0) {
312
313
  if (x < minX) minX = x
313
314
  if (y < minY) minY = y
314
315
  if (x > maxX) maxX = x
@@ -337,10 +338,10 @@ internal object MarkerBitmapRenderer {
337
338
  var rightEdgeHasPixel = false
338
339
 
339
340
  for (x in 0 until bitmap.width) {
340
- if (!topEdgeHasPixel && Color.alpha(bitmap.getPixel(x, 0)) != 0) {
341
+ if (!topEdgeHasPixel && Color.alpha(bitmap[x, 0]) != 0) {
341
342
  topEdgeHasPixel = true
342
343
  }
343
- if (!bottomEdgeHasPixel && Color.alpha(bitmap.getPixel(x, bitmap.height - 1)) != 0) {
344
+ if (!bottomEdgeHasPixel && Color.alpha(bitmap[x, bitmap.height - 1]) != 0) {
344
345
  bottomEdgeHasPixel = true
345
346
  }
346
347
  if (topEdgeHasPixel && bottomEdgeHasPixel) {
@@ -349,10 +350,10 @@ internal object MarkerBitmapRenderer {
349
350
  }
350
351
 
351
352
  for (y in 0 until bitmap.height) {
352
- if (!leftEdgeHasPixel && Color.alpha(bitmap.getPixel(0, y)) != 0) {
353
+ if (!leftEdgeHasPixel && Color.alpha(bitmap[0, y]) != 0) {
353
354
  leftEdgeHasPixel = true
354
355
  }
355
- if (!rightEdgeHasPixel && Color.alpha(bitmap.getPixel(bitmap.width - 1, y)) != 0) {
356
+ if (!rightEdgeHasPixel && Color.alpha(bitmap[bitmap.width - 1, y]) != 0) {
356
357
  rightEdgeHasPixel = true
357
358
  }
358
359
  if (leftEdgeHasPixel && rightEdgeHasPixel) {
@@ -28,7 +28,7 @@ import androidx.core.view.contains
28
28
 
29
29
  import androidx.core.view.isEmpty
30
30
  import androidx.core.graphics.scale
31
- import android.view.ViewGroup
31
+
32
32
  import com.amap.api.maps.model.animation.AlphaAnimation
33
33
  import com.amap.api.maps.model.animation.AnimationSet
34
34
  import com.amap.api.maps.model.animation.ScaleAnimation
@@ -40,6 +40,7 @@ import kotlin.math.max
40
40
  import kotlin.math.min
41
41
  import expo.modules.gaodemap.utils.LatLngParser
42
42
 
43
+ @SuppressLint("ViewConstructor")
43
44
  class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
44
45
 
45
46
  init {
@@ -112,7 +113,7 @@ class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, a
112
113
  }
113
114
  }
114
115
 
115
- if (childCount == 0) {
116
+ if (isEmpty()) {
116
117
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
117
118
  return
118
119
  }
@@ -927,9 +928,7 @@ class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, a
927
928
  }
928
929
 
929
930
  val snapshot = resolveMarkerBitmapSnapshot() ?: run {
930
- if (marker?.isVisible != true) {
931
- // 自定义 view 还没准备好时,继续等待下一次 layout/update。
932
- }
931
+
933
932
  return
934
933
  }
935
934
  val fullCacheKey = snapshot.fullCacheKey
@@ -958,10 +957,7 @@ class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, a
958
957
  // 🔑 关键修复:如果生成 Bitmap 失败(例如 View 还没准备好)
959
958
  // 不要急着切回默认 Marker,这会导致闪烁和位置跳变。
960
959
  // 只有在 Marker 从未显示过的情况下,才考虑兜底策略。
961
- if (marker?.isVisible != true) {
962
- // 如果从未显示过,可以暂不显示,等待下一次尝试,或者显示默认(取决于需求)
963
- // 这里选择暂不显示,避免闪现蓝点
964
- }
960
+
965
961
  return
966
962
  }
967
963
 
@@ -997,7 +993,7 @@ class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, a
997
993
  }
998
994
  }
999
995
 
1000
- private fun invalidateAppliedCustomMarkerCaches(clearGlobalCache: Boolean = false) {
996
+ private fun invalidateAppliedCustomMarkerCaches(clearGlobalCache: Boolean) {
1001
997
  val key = lastAppliedCustomMarkerKey ?: return
1002
998
  if (clearGlobalCache) {
1003
999
  BitmapDescriptorCache.remove(key)
@@ -1040,7 +1036,7 @@ class MarkerView(context: Context, appContext: AppContext) : ExpoView(context, a
1040
1036
  if (index in 0..<childCount) {
1041
1037
  super.removeViewAt(index)
1042
1038
  // 只在还有子视图时更新图标
1043
- if (!isRemoving && childCount > 0 && marker != null) {
1039
+ if (!isRemoving && isNotEmpty() && marker != null) {
1044
1040
  markCustomMarkerContentDirty(50)
1045
1041
  }
1046
1042
  // 如果最后一个子视图被移除,什么都不做
@@ -1,5 +1,6 @@
1
1
  package expo.modules.gaodemap.overlays
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
4
5
  import android.graphics.Bitmap
5
6
  import android.graphics.BitmapFactory
@@ -10,7 +11,7 @@ import expo.modules.gaodemap.utils.LatLngParser
10
11
  import com.amap.api.maps.AMap
11
12
  import com.amap.api.maps.model.BitmapDescriptor
12
13
  import com.amap.api.maps.model.BitmapDescriptorFactory
13
- import com.amap.api.maps.model.LatLng
14
+
14
15
  import com.amap.api.maps.model.MultiPointItem
15
16
  import com.amap.api.maps.model.MultiPointOverlay
16
17
  import com.amap.api.maps.model.MultiPointOverlayOptions
@@ -26,6 +27,7 @@ import java.net.URL
26
27
  import kotlin.concurrent.thread
27
28
  import androidx.core.graphics.scale
28
29
 
30
+ @SuppressLint("ViewConstructor")
29
31
  class MultiPointView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
30
32
 
31
33
  private val onMultiPointPress by EventDispatcher()