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.
- package/README.md +20 -14
- package/android/build.gradle +8 -4
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +7 -8
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +150 -27
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +24 -14
- package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +38 -41
- package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +18 -17
- package/android/src/main/java/expo/modules/gaodemap/overlays/CircleView.kt +3 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +6 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +124 -10
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +2 -2
- package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +10 -9
- package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +7 -11
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +3 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/PolygonView.kt +2 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/PolylineView.kt +1 -0
- package/android/src/main/java/expo/modules/gaodemap/search/ExpoGaodeMapSearchModule.kt +751 -0
- package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +5 -5
- package/android/src/main/java/expo/modules/gaodemap/utils/PermissionHelper.kt +13 -16
- package/build/ExpoGaodeMapOfflineModule.d.ts +5 -0
- package/build/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
- package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
- package/build/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/components/overlays/HeatMap.js +21 -2
- package/build/components/overlays/HeatMap.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/search/ExpoGaodeMapSearch.types.d.ts +340 -0
- package/build/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
- package/build/search/ExpoGaodeMapSearch.types.js +19 -0
- package/build/search/ExpoGaodeMapSearch.types.js.map +1 -0
- package/build/search/ExpoGaodeMapSearchModule.d.ts +74 -0
- package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
- package/build/search/ExpoGaodeMapSearchModule.js +47 -0
- package/build/search/ExpoGaodeMapSearchModule.js.map +1 -0
- package/build/search/index.d.ts +156 -0
- package/build/search/index.d.ts.map +1 -0
- package/build/search/index.js +171 -0
- package/build/search/index.js.map +1 -0
- package/build/types/map-view.types.d.ts +4 -2
- package/build/types/map-view.types.d.ts.map +1 -1
- package/build/types/map-view.types.js.map +1 -1
- package/build/utils/OfflineMapManager.d.ts +4 -0
- package/build/utils/OfflineMapManager.d.ts.map +1 -1
- package/build/utils/OfflineMapManager.js +6 -0
- package/build/utils/OfflineMapManager.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/ExpoGaodeMap.podspec +2 -2
- package/ios/ExpoGaodeMapOfflineModule.swift +60 -0
- package/ios/ExpoGaodeMapSearchModule.swift +773 -0
- package/ios/modules/LocationManager.swift +9 -3
- package/ios/overlays/PolylineView.swift +6 -12
- package/package.json +1 -1
- 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()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
if (keepCurrentVersion) {
|
|
196
|
+
putString(KEY_PRIVACY_VERSION, privacyVersion)
|
|
197
|
+
} else {
|
|
198
|
+
privacyVersion = null
|
|
199
|
+
remove(KEY_PRIVACY_VERSION)
|
|
200
|
+
}
|
|
200
201
|
|
|
201
|
-
|
|
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
|
-
|
|
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.
|
|
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<
|
|
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(
|
|
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
|
-
|
|
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
|
|
168
|
-
.data(
|
|
194
|
+
val builder = HeatmapTileProvider.Builder()
|
|
195
|
+
.data(latLngSnapshot)
|
|
169
196
|
.radius(radiusValue)
|
|
170
|
-
|
|
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(
|
|
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") {
|
|
31
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
341
|
+
if (!topEdgeHasPixel && Color.alpha(bitmap[x, 0]) != 0) {
|
|
341
342
|
topEdgeHasPixel = true
|
|
342
343
|
}
|
|
343
|
-
if (!bottomEdgeHasPixel && Color.alpha(bitmap
|
|
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
|
|
353
|
+
if (!leftEdgeHasPixel && Color.alpha(bitmap[0, y]) != 0) {
|
|
353
354
|
leftEdgeHasPixel = true
|
|
354
355
|
}
|
|
355
|
-
if (!rightEdgeHasPixel && Color.alpha(bitmap
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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()
|