expo-gaode-map-navigation 1.1.5-next.1 → 1.1.5-next.3
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/android/build.gradle +10 -0
- package/android/src/main/cpp/CMakeLists.txt +24 -0
- package/android/src/main/cpp/cluster_jni.cpp +848 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
- package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
- package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
- package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
- package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
- package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
- package/build/ExpoGaodeMapNaviView.d.ts +7 -7
- package/build/ExpoGaodeMapNaviView.js +8 -8
- package/build/ExpoGaodeMapNavigationModule.d.ts +2 -1
- package/build/index.d.ts +35 -33
- package/build/index.js +70 -106
- package/build/map/ExpoGaodeMapModule.d.ts +2 -201
- package/build/map/ExpoGaodeMapModule.js +584 -14
- package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
- package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
- package/build/map/ExpoGaodeMapView.js +66 -58
- package/build/map/components/FoldableMapView.d.ts +38 -0
- package/build/map/components/FoldableMapView.js +209 -0
- package/build/map/components/MapContext.d.ts +12 -0
- package/build/map/components/MapContext.js +54 -0
- package/build/map/components/MapUI.d.ts +18 -0
- package/build/map/components/MapUI.js +29 -0
- package/build/map/components/overlays/Circle.js +34 -3
- package/build/map/components/overlays/Cluster.d.ts +3 -1
- package/build/map/components/overlays/Cluster.js +31 -2
- package/build/map/components/overlays/HeatMap.d.ts +3 -1
- package/build/map/components/overlays/HeatMap.js +33 -3
- package/build/map/components/overlays/Marker.d.ts +1 -1
- package/build/map/components/overlays/Marker.js +37 -32
- package/build/map/components/overlays/MultiPoint.js +1 -1
- package/build/map/components/overlays/Polygon.js +30 -3
- package/build/map/components/overlays/Polyline.js +36 -3
- package/build/map/index.d.ts +25 -5
- package/build/map/index.js +59 -18
- package/build/map/types/common.types.d.ts +40 -0
- package/build/map/types/common.types.js +0 -4
- package/build/map/types/index.d.ts +3 -2
- package/build/map/types/map-view.types.d.ts +108 -3
- package/build/map/types/native-module.types.d.ts +363 -0
- package/build/map/types/native-module.types.js +5 -0
- package/build/map/types/offline.types.d.ts +132 -0
- package/build/map/types/offline.types.js +5 -0
- package/build/map/types/overlays.types.d.ts +137 -24
- package/build/map/utils/ErrorHandler.d.ts +110 -0
- package/build/map/utils/ErrorHandler.js +421 -0
- package/build/map/utils/GeoUtils.d.ts +20 -0
- package/build/map/utils/GeoUtils.js +76 -0
- package/build/map/utils/OfflineMapManager.d.ts +148 -0
- package/build/map/utils/OfflineMapManager.js +217 -0
- package/build/map/utils/PermissionUtils.d.ts +91 -0
- package/build/map/utils/PermissionUtils.js +255 -0
- package/build/map/utils/PlatformDetector.d.ts +102 -0
- package/build/map/utils/PlatformDetector.js +186 -0
- package/build/types/index.d.ts +1 -0
- package/build/types/index.js +1 -0
- package/build/types/native-module.types.d.ts +69 -0
- package/build/types/native-module.types.js +2 -0
- package/build/types/naviview.types.d.ts +1 -1
- package/expo-module.config.json +12 -10
- package/ios/ExpoGaodeMapNavigation.podspec +9 -0
- package/ios/map/ExpoGaodeMapModule.swift +485 -75
- package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
- package/ios/map/ExpoGaodeMapView.swift +611 -62
- package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
- package/ios/map/MapPreloadManager.swift +348 -0
- package/ios/map/cpp/ClusterEngine.cpp +110 -0
- package/ios/map/cpp/ClusterEngine.hpp +20 -0
- package/ios/map/cpp/ColorParser.cpp +135 -0
- package/ios/map/cpp/ColorParser.hpp +14 -0
- package/ios/map/cpp/GeometryEngine.cpp +574 -0
- package/ios/map/cpp/GeometryEngine.hpp +159 -0
- package/ios/map/cpp/QuadTree.cpp +92 -0
- package/ios/map/cpp/QuadTree.hpp +42 -0
- package/ios/map/cpp/README.md +55 -0
- package/ios/map/cpp/tests/benchmark_js.js +41 -0
- package/ios/map/cpp/tests/run.sh +17 -0
- package/ios/map/cpp/tests/test_main.cpp +276 -0
- package/ios/map/managers/UIManager.swift +72 -1
- package/ios/map/modules/LocationManager.swift +114 -165
- package/ios/map/overlays/CircleView.swift +16 -32
- package/ios/map/overlays/CircleViewModule.swift +12 -12
- package/ios/map/overlays/ClusterAnnotation.swift +32 -0
- package/ios/map/overlays/ClusterView.swift +331 -45
- package/ios/map/overlays/ClusterViewModule.swift +20 -6
- package/ios/map/overlays/HeatMapView.swift +135 -32
- package/ios/map/overlays/HeatMapViewModule.swift +20 -8
- package/ios/map/overlays/MarkerView.swift +613 -130
- package/ios/map/overlays/MarkerViewModule.swift +38 -18
- package/ios/map/overlays/MultiPointView.swift +168 -10
- package/ios/map/overlays/MultiPointViewModule.swift +27 -5
- package/ios/map/overlays/PolygonView.swift +62 -23
- package/ios/map/overlays/PolygonViewModule.swift +18 -12
- package/ios/map/overlays/PolylineView.swift +21 -13
- package/ios/map/overlays/PolylineViewModule.swift +18 -12
- package/ios/map/utils/ClusterNative.h +96 -0
- package/ios/map/utils/ClusterNative.mm +377 -0
- package/ios/map/utils/ColorParser.swift +12 -1
- package/ios/map/utils/CppBridging.mm +13 -0
- package/ios/map/utils/GeometryUtils.swift +34 -0
- package/ios/map/utils/LatLngParser.swift +87 -0
- package/ios/map/utils/PermissionManager.swift +135 -6
- package/package.json +1 -1
- package/build/map/ExpoGaodeMap.types.d.ts +0 -41
- package/build/map/ExpoGaodeMap.types.js +0 -24
- package/build/map/utils/EventManager.d.ts +0 -10
- package/build/map/utils/EventManager.js +0 -26
- package/build/map/utils/ModuleLoader.d.ts +0 -73
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
247
|
+
pendingPosition?.let { pos ->
|
|
248
|
+
marker?.position = pos
|
|
206
249
|
pendingPosition = null
|
|
207
250
|
pendingLatitude = null
|
|
208
251
|
pendingLongitude = null
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
968
|
+
} catch (_: Exception) {
|
|
969
|
+
// 忽略异常
|
|
656
970
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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(
|
|
1011
|
+
fixChildLayoutParams(it)
|
|
679
1012
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
}
|
|
1035
|
+
}
|
|
720
1036
|
}
|
|
721
|
-
|
|
1037
|
+
|
|
722
1038
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
}
|