craft-native 0.0.247 → 0.0.249
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/dist/android/src/index.d.ts +2 -0
- package/dist/android/src/index.js +22 -18
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
- package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
- package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
- package/dist/android/templates/CraftNativeServices.kt.template +25 -0
- package/dist/android/templates/CraftNativeShell.kt.template +41 -9
- package/dist/android/templates/MainActivityNative.kt.template +369 -69
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/api/mobile.d.ts +37 -1
- package/dist/browser.js +9 -9
- package/dist/cli.js +90 -673
- package/dist/components/index.d.ts +50 -9
- package/dist/index.cjs +394 -438
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +446 -493
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +54 -53
- package/dist/ios/templates/CraftApp.swift +44 -3
- package/dist/ios/templates/CraftHealth.swift +217 -0
- package/dist/ios/templates/CraftHeartRate.swift +6 -3
- package/dist/ios/templates/CraftHybrid.swift +30 -22
- package/dist/ios/templates/CraftLiveActivities.swift +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +87 -8
- package/dist/ios/templates/CraftNativeFiles.swift +15 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
- package/dist/ios/templates/CraftNativeMutation.swift +36 -0
- package/dist/ios/templates/CraftNativeScreen.swift +826 -69
- package/dist/ios/templates/CraftNativeServices.swift +4 -2
- package/dist/ios/templates/CraftNativeUI.swift +10 -2
- package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
- package/dist/ios/templates/testing/HostApp.swift +9 -2
- package/dist/macos/templates/CraftNativeMutation.swift +36 -0
- package/dist/macos/templates/native_layout.zig +226 -2
- package/dist/mobile-testing.js +2 -2
- package/dist/mobile.js +101 -63
- package/package.json +1 -1
|
@@ -70,6 +70,8 @@ export interface CraftAndroidConfig {
|
|
|
70
70
|
*/
|
|
71
71
|
enableHeartRate?: boolean;
|
|
72
72
|
enableHealthConnect?: boolean;
|
|
73
|
+
/** Google Wallet save-link handoff for native screens. */
|
|
74
|
+
enableWallet?: boolean;
|
|
73
75
|
/**
|
|
74
76
|
* The Health Connect data the app reads and writes, by the shared type
|
|
75
77
|
* names (`steps`, `sleep`, `workouts`...). The manifest declares only
|
|
@@ -18,6 +18,8 @@ import com.google.android.gms.maps.model.LatLng
|
|
|
18
18
|
import com.google.android.gms.maps.model.LatLngBounds
|
|
19
19
|
import com.google.android.gms.maps.model.Marker
|
|
20
20
|
import com.google.android.gms.maps.model.MarkerOptions
|
|
21
|
+
import com.google.android.gms.maps.model.Polygon
|
|
22
|
+
import com.google.android.gms.maps.model.PolygonOptions
|
|
21
23
|
import com.google.android.gms.maps.model.Polyline
|
|
22
24
|
import com.google.android.gms.maps.model.PolylineOptions`,
|
|
23
25
|
dependency: "com.google.android.gms:play-services-maps:18.2.0"
|
|
@@ -37,6 +39,7 @@ import org.maplibre.android.maps.OnMapReadyCallback
|
|
|
37
39
|
import org.maplibre.android.maps.Style
|
|
38
40
|
import org.maplibre.android.style.expressions.Expression
|
|
39
41
|
import org.maplibre.android.style.layers.CircleLayer
|
|
42
|
+
import org.maplibre.android.style.layers.FillLayer
|
|
40
43
|
import org.maplibre.android.style.layers.LineLayer
|
|
41
44
|
import org.maplibre.android.style.layers.SymbolLayer
|
|
42
45
|
import org.maplibre.android.style.layers.PropertyFactory.*
|
|
@@ -500,6 +503,7 @@ var DEFAULT_CONFIG = {
|
|
|
500
503
|
enableBluetooth: false,
|
|
501
504
|
enableHeartRate: false,
|
|
502
505
|
enableHealthConnect: false,
|
|
506
|
+
enableWallet: false,
|
|
503
507
|
enableKeepAwake: false,
|
|
504
508
|
enableDeepLinks: false,
|
|
505
509
|
urlSchemes: [],
|
|
@@ -1382,23 +1386,23 @@ async function run(options) {
|
|
|
1382
1386
|
}
|
|
1383
1387
|
}
|
|
1384
1388
|
export {
|
|
1385
|
-
|
|
1386
|
-
syncAndroidWebAssets,
|
|
1387
|
-
runKotlinFixture,
|
|
1388
|
-
run,
|
|
1389
|
-
resolveRuntimeDir,
|
|
1390
|
-
renderAndroidPermissions,
|
|
1391
|
-
renderAndroidDeepLinks,
|
|
1392
|
-
open,
|
|
1393
|
-
nativeRouteRuntime,
|
|
1394
|
-
nativeRouteBundles,
|
|
1395
|
-
nativeMessages,
|
|
1396
|
-
nativeMapProviders,
|
|
1397
|
-
installRuntime,
|
|
1398
|
-
init,
|
|
1399
|
-
healthConnectPermissions,
|
|
1400
|
-
createAndroidEmulator,
|
|
1401
|
-
build,
|
|
1389
|
+
ANDROID_MIN_SDK,
|
|
1402
1390
|
HEALTH_CONNECT_PERMISSIONS,
|
|
1403
|
-
|
|
1391
|
+
build,
|
|
1392
|
+
createAndroidEmulator,
|
|
1393
|
+
healthConnectPermissions,
|
|
1394
|
+
init,
|
|
1395
|
+
installRuntime,
|
|
1396
|
+
nativeMapProviders,
|
|
1397
|
+
nativeMessages,
|
|
1398
|
+
nativeRouteBundles,
|
|
1399
|
+
nativeRouteRuntime,
|
|
1400
|
+
open,
|
|
1401
|
+
renderAndroidDeepLinks,
|
|
1402
|
+
renderAndroidPermissions,
|
|
1403
|
+
resolveRuntimeDir,
|
|
1404
|
+
run,
|
|
1405
|
+
runKotlinFixture,
|
|
1406
|
+
syncAndroidWebAssets,
|
|
1407
|
+
syncNativeFonts
|
|
1404
1408
|
};
|
|
@@ -360,6 +360,12 @@ object CraftNative {
|
|
|
360
360
|
nodeData: FloatArray,
|
|
361
361
|
treeMeta: FloatArray,
|
|
362
362
|
): FloatArray?
|
|
363
|
+
private external fun nativeLayoutTreeV5(
|
|
364
|
+
width: Float,
|
|
365
|
+
height: Float,
|
|
366
|
+
nodeData: FloatArray,
|
|
367
|
+
treeMeta: FloatArray,
|
|
368
|
+
): FloatArray?
|
|
363
369
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
364
370
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
365
371
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
@@ -660,6 +666,17 @@ object CraftNative {
|
|
|
660
666
|
}
|
|
661
667
|
}
|
|
662
668
|
|
|
669
|
+
/** Resolve a tree with shared auto rows and explicit equal grid rows. */
|
|
670
|
+
fun layoutTreeV5(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
671
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
672
|
+
return try {
|
|
673
|
+
nativeLayoutTreeV5(width, height, nodeData, treeMeta)
|
|
674
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
675
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
676
|
+
null
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
663
680
|
/**
|
|
664
681
|
* The clipboard's text, or null to use the Kotlin implementation.
|
|
665
682
|
*
|
|
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.PagerSnapHelper
|
|
|
12
12
|
import androidx.recyclerview.widget.RecyclerView
|
|
13
13
|
import org.json.JSONObject
|
|
14
14
|
import kotlin.math.ceil
|
|
15
|
+
import kotlin.math.max
|
|
15
16
|
|
|
16
17
|
internal typealias CraftNativeRenderItem = (node: JSONObject, identity: String, previous: View?) -> View
|
|
17
18
|
|
|
@@ -96,6 +97,7 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
96
97
|
|
|
97
98
|
private var renderItem: CraftNativeRenderItem? = null
|
|
98
99
|
private var recycleItem: ((String) -> Unit)? = null
|
|
100
|
+
private var removeItem: ((String) -> Unit)? = null
|
|
99
101
|
private var endReached: (() -> Unit)? = null
|
|
100
102
|
private var threshold = 0.1
|
|
101
103
|
private var endReachedSignature: String? = null
|
|
@@ -120,6 +122,49 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
120
122
|
var onMomentumScrollBegin: (() -> Unit)? = null
|
|
121
123
|
var onMomentumScrollEnd: (() -> Unit)? = null
|
|
122
124
|
var onLayoutChanged: (() -> Unit)? = null
|
|
125
|
+
var onContentSizeChanged: (() -> Unit)? = null
|
|
126
|
+
private var lastIntrinsicHeight = Int.MIN_VALUE
|
|
127
|
+
private var lastReportedContentHeight = Int.MIN_VALUE
|
|
128
|
+
|
|
129
|
+
private val scrollListener = object : OnScrollListener() {
|
|
130
|
+
private var dragging = false
|
|
131
|
+
private var momentum = false
|
|
132
|
+
|
|
133
|
+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
134
|
+
onScrollEvent?.invoke()
|
|
135
|
+
onLayoutChanged?.invoke()
|
|
136
|
+
evaluateEndReached()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
140
|
+
when (newState) {
|
|
141
|
+
RecyclerView.SCROLL_STATE_DRAGGING -> if (!dragging) {
|
|
142
|
+
dragging = true
|
|
143
|
+
onScrollBeginDrag?.invoke()
|
|
144
|
+
}
|
|
145
|
+
RecyclerView.SCROLL_STATE_SETTLING -> {
|
|
146
|
+
if (dragging) {
|
|
147
|
+
dragging = false
|
|
148
|
+
onScrollEndDrag?.invoke()
|
|
149
|
+
}
|
|
150
|
+
if (!momentum) {
|
|
151
|
+
momentum = true
|
|
152
|
+
onMomentumScrollBegin?.invoke()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
RecyclerView.SCROLL_STATE_IDLE -> {
|
|
156
|
+
if (dragging) {
|
|
157
|
+
dragging = false
|
|
158
|
+
onScrollEndDrag?.invoke()
|
|
159
|
+
}
|
|
160
|
+
if (momentum) {
|
|
161
|
+
momentum = false
|
|
162
|
+
onMomentumScrollEnd?.invoke()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
123
168
|
|
|
124
169
|
private fun holderLayoutParams() = LayoutParams(
|
|
125
170
|
if (horizontal) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT,
|
|
@@ -132,7 +177,11 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
132
177
|
}
|
|
133
178
|
|
|
134
179
|
fun setScrollEnabled(enabled: Boolean) {
|
|
180
|
+
if (scrollEnabled == enabled) return
|
|
135
181
|
scrollEnabled = enabled
|
|
182
|
+
lastIntrinsicHeight = Int.MIN_VALUE
|
|
183
|
+
lastReportedContentHeight = Int.MIN_VALUE
|
|
184
|
+
requestLayout()
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
val isScrollEnabledForTest: Boolean
|
|
@@ -151,6 +200,26 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
151
200
|
val isScrollTargetPendingForTest: Boolean
|
|
152
201
|
get() = pendingScrollTarget != null
|
|
153
202
|
|
|
203
|
+
/** Stable identities currently mounted, in their adapter order. */
|
|
204
|
+
val visibleItemIdentities: List<String>
|
|
205
|
+
get() = (0 until childCount)
|
|
206
|
+
.mapNotNull { index ->
|
|
207
|
+
val child = getChildAt(index)
|
|
208
|
+
val position = getChildAdapterPosition(child)
|
|
209
|
+
val order = if (position == RecyclerView.NO_POSITION) {
|
|
210
|
+
// ListAdapter can briefly report NO_POSITION while an
|
|
211
|
+
// async diff is dispatching. Keep the mounted holder in
|
|
212
|
+
// the snapshot instead of making the entire list appear
|
|
213
|
+
// empty to host-side reconciliation tests.
|
|
214
|
+
Int.MAX_VALUE - childCount + index
|
|
215
|
+
} else {
|
|
216
|
+
position
|
|
217
|
+
}
|
|
218
|
+
(getChildViewHolder(child) as? Holder)?.identity?.let { order to it }
|
|
219
|
+
}
|
|
220
|
+
.sortedBy { it.first }
|
|
221
|
+
.map { it.second }
|
|
222
|
+
|
|
154
223
|
override fun onInterceptTouchEvent(event: android.view.MotionEvent): Boolean {
|
|
155
224
|
if (!scrollEnabled) return false
|
|
156
225
|
return super.onInterceptTouchEvent(event)
|
|
@@ -161,6 +230,29 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
161
230
|
return super.onTouchEvent(event)
|
|
162
231
|
}
|
|
163
232
|
|
|
233
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
234
|
+
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
|
235
|
+
if (!scrollEnabled && !horizontal && heightMode != MeasureSpec.EXACTLY) {
|
|
236
|
+
// A non-scrolling FlatList is content-sized on iOS. Let the
|
|
237
|
+
// RecyclerView measure its full adapter range instead of keeping
|
|
238
|
+
// the viewport height, which otherwise clips the list when it is
|
|
239
|
+
// nested in an outer native ScrollView.
|
|
240
|
+
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
|
|
241
|
+
val contentHeight = max(measuredHeight, computeVerticalScrollRange())
|
|
242
|
+
val resolvedHeight = resolveSize(contentHeight, heightMeasureSpec)
|
|
243
|
+
val contentChanged = resolvedHeight != lastIntrinsicHeight
|
|
244
|
+
lastIntrinsicHeight = resolvedHeight
|
|
245
|
+
setMeasuredDimension(
|
|
246
|
+
resolveSize(measuredWidth, widthMeasureSpec),
|
|
247
|
+
resolvedHeight,
|
|
248
|
+
)
|
|
249
|
+
if (contentChanged) reportContentSizeChanged()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
lastIntrinsicHeight = Int.MIN_VALUE
|
|
253
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
254
|
+
}
|
|
255
|
+
|
|
164
256
|
fun setBounceOptions(enabled: Boolean, vertical: Boolean, horizontal: Boolean) {
|
|
165
257
|
bounces = enabled
|
|
166
258
|
alwaysBounceVertical = vertical
|
|
@@ -182,51 +274,21 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
182
274
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
183
275
|
super.onLayout(changed, left, top, right, bottom)
|
|
184
276
|
scrollToPendingTarget(0)
|
|
277
|
+
if (!scrollEnabled && !horizontal) reportContentSizeChanged()
|
|
185
278
|
onLayoutChanged?.invoke()
|
|
186
279
|
}
|
|
187
280
|
|
|
281
|
+
private fun reportContentSizeChanged() {
|
|
282
|
+
val height = measuredHeight
|
|
283
|
+
if (height <= 0 || height == lastReportedContentHeight) return
|
|
284
|
+
lastReportedContentHeight = height
|
|
285
|
+
post { onContentSizeChanged?.invoke() }
|
|
286
|
+
}
|
|
287
|
+
|
|
188
288
|
init {
|
|
189
289
|
adapter = listAdapter
|
|
190
290
|
itemAnimator = null
|
|
191
|
-
addOnScrollListener(
|
|
192
|
-
private var dragging = false
|
|
193
|
-
private var momentum = false
|
|
194
|
-
|
|
195
|
-
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
196
|
-
onScrollEvent?.invoke()
|
|
197
|
-
onLayoutChanged?.invoke()
|
|
198
|
-
evaluateEndReached()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
202
|
-
when (newState) {
|
|
203
|
-
RecyclerView.SCROLL_STATE_DRAGGING -> if (!dragging) {
|
|
204
|
-
dragging = true
|
|
205
|
-
onScrollBeginDrag?.invoke()
|
|
206
|
-
}
|
|
207
|
-
RecyclerView.SCROLL_STATE_SETTLING -> {
|
|
208
|
-
if (dragging) {
|
|
209
|
-
dragging = false
|
|
210
|
-
onScrollEndDrag?.invoke()
|
|
211
|
-
}
|
|
212
|
-
if (!momentum) {
|
|
213
|
-
momentum = true
|
|
214
|
-
onMomentumScrollBegin?.invoke()
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
RecyclerView.SCROLL_STATE_IDLE -> {
|
|
218
|
-
if (dragging) {
|
|
219
|
-
dragging = false
|
|
220
|
-
onScrollEndDrag?.invoke()
|
|
221
|
-
}
|
|
222
|
-
if (momentum) {
|
|
223
|
-
momentum = false
|
|
224
|
-
onMomentumScrollEnd?.invoke()
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
})
|
|
291
|
+
addOnScrollListener(scrollListener)
|
|
230
292
|
}
|
|
231
293
|
|
|
232
294
|
fun setKeyboardDismissMode(mode: String, dismissKeyboard: () -> Unit) {
|
|
@@ -291,10 +353,12 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
291
353
|
endReachedThreshold: Double,
|
|
292
354
|
renderItem: CraftNativeRenderItem,
|
|
293
355
|
recycleItem: (String) -> Unit,
|
|
356
|
+
removeItem: (String) -> Unit,
|
|
294
357
|
endReached: (() -> Unit)?,
|
|
295
358
|
) {
|
|
296
359
|
this.renderItem = renderItem
|
|
297
360
|
this.recycleItem = recycleItem
|
|
361
|
+
this.removeItem = removeItem
|
|
298
362
|
this.endReached = endReached
|
|
299
363
|
threshold = endReachedThreshold.takeIf { it.isFinite() }?.coerceAtLeast(0.0) ?: 0.1
|
|
300
364
|
|
|
@@ -321,7 +385,7 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
321
385
|
|
|
322
386
|
val oldIds = listAdapter.currentList.mapTo(mutableSetOf()) { it.identity }
|
|
323
387
|
val nextIds = next.mapTo(mutableSetOf()) { it.identity }
|
|
324
|
-
(oldIds - nextIds).forEach(
|
|
388
|
+
(oldIds - nextIds).forEach(removeItem)
|
|
325
389
|
val dataItems = next.filterNot { it.chrome }
|
|
326
390
|
val contentSignature = dataItems.joinToString("") {
|
|
327
391
|
"${it.identity.length}:${it.identity}${it.signature.length}:${it.signature}"
|
|
@@ -386,6 +450,7 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
386
450
|
}
|
|
387
451
|
renderItem = null
|
|
388
452
|
recycleItem = null
|
|
453
|
+
removeItem = null
|
|
389
454
|
endReached = null
|
|
390
455
|
setPagingEnabled(false)
|
|
391
456
|
scrollTargetKey = null
|
|
@@ -397,6 +462,16 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
397
462
|
onMomentumScrollBegin = null
|
|
398
463
|
onMomentumScrollEnd = null
|
|
399
464
|
onLayoutChanged = null
|
|
465
|
+
onContentSizeChanged = null
|
|
466
|
+
lastIntrinsicHeight = Int.MIN_VALUE
|
|
467
|
+
lastReportedContentHeight = Int.MIN_VALUE
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Releases callbacks owned by a permanently removed native list node. */
|
|
471
|
+
fun resetForTeardown() {
|
|
472
|
+
discardAll()
|
|
473
|
+
removeOnScrollListener(scrollListener)
|
|
474
|
+
setOnTouchListener(null)
|
|
400
475
|
}
|
|
401
476
|
|
|
402
477
|
fun refreshThemeDefaults() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The Android host for the small, host-neutral native trail map contract. */
|
|
2
2
|
private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
3
|
+
private data class PolygonValue(val points: List<LatLng>, val fillColor: Int, val strokeColor: Int, val strokeWidth: Float)
|
|
3
4
|
private val mapView = MapView(context)
|
|
4
5
|
private val scaleView = LinearLayout(context).apply {
|
|
5
6
|
orientation = LinearLayout.VERTICAL
|
|
@@ -17,6 +18,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
17
18
|
private var pendingChildren = JSONArray()
|
|
18
19
|
private val markers = mutableMapOf<String, Marker>()
|
|
19
20
|
private val polylines = mutableMapOf<String, Polyline>()
|
|
21
|
+
private val polygons = mutableMapOf<String, Polygon>()
|
|
20
22
|
private val markerHandlers = mutableMapOf<String, String>()
|
|
21
23
|
private var onMarkerTap: ((String) -> Unit)? = null
|
|
22
24
|
var onMapPress: ((LatLng) -> Unit)? = null
|
|
@@ -40,10 +42,23 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
40
42
|
})
|
|
41
43
|
scaleView.visibility = View.GONE
|
|
42
44
|
addView(tileAttribution, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.END).apply { rightMargin = (8 * resources.displayMetrics.density).roundToInt(); bottomMargin = (8 * resources.displayMetrics.density).roundToInt() })
|
|
45
|
+
refreshThemeDefaults()
|
|
43
46
|
mapView.onCreate(null)
|
|
44
47
|
mapView.getMapAsync(this)
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
fun refreshThemeDefaults() {
|
|
51
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
52
|
+
val foreground = if (dark) Color.rgb(245, 245, 245) else Color.rgb(37, 51, 33)
|
|
53
|
+
val surface = if (dark) Color.argb(230, 38, 38, 38) else Color.argb(230, 255, 255, 255)
|
|
54
|
+
scaleView.background = GradientDrawable().apply { setColor(surface); cornerRadius = 4f }
|
|
55
|
+
scaleLabel.setTextColor(foreground)
|
|
56
|
+
scaleLine.setBackgroundColor(foreground)
|
|
57
|
+
tileAttribution.setTextColor(foreground)
|
|
58
|
+
tileAttribution.setBackgroundColor(surface)
|
|
59
|
+
applyPending(refreshCamera = false)
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
override fun onMapReady(map: GoogleMap) {
|
|
48
63
|
if (destroyed) return
|
|
49
64
|
googleMap = map
|
|
@@ -85,7 +100,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
85
100
|
|
|
86
101
|
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
87
102
|
|
|
88
|
-
private fun applyPending() {
|
|
103
|
+
private fun applyPending(refreshCamera: Boolean = true) {
|
|
89
104
|
val map = googleMap ?: return
|
|
90
105
|
applyMapPadding()
|
|
91
106
|
map.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
@@ -109,6 +124,31 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
109
124
|
val nextPinColors = mutableMapOf<String, Int>()
|
|
110
125
|
val nextHandlers = mutableMapOf<String, String>()
|
|
111
126
|
val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
|
|
127
|
+
val nextPolygons = mutableMapOf<String, PolygonValue>()
|
|
128
|
+
val dataMarkers = pendingProps.optJSONArray("markers")
|
|
129
|
+
for (index in 0 until (dataMarkers?.length() ?: 0)) {
|
|
130
|
+
val overlay = dataMarkers?.optJSONObject(index) ?: continue
|
|
131
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
132
|
+
val point = coordinates(overlay.optJSONArray("geometry")).firstOrNull() ?: continue
|
|
133
|
+
nextMarkers[id] = point to (overlay.opt("title") as? String)
|
|
134
|
+
(overlay.opt("pinColor") as? String)?.let { nextPinColors[id] = color(it) }
|
|
135
|
+
(overlay.opt("bearing") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.let { nextBearings[id] = (it % 360).toFloat() }
|
|
136
|
+
}
|
|
137
|
+
val dataPolylines = pendingProps.optJSONArray("polylines")
|
|
138
|
+
for (index in 0 until (dataPolylines?.length() ?: 0)) {
|
|
139
|
+
val overlay = dataPolylines?.optJSONObject(index) ?: continue
|
|
140
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
141
|
+
val points = coordinates(overlay.optJSONArray("geometry"))
|
|
142
|
+
if (points.size <= 1) continue
|
|
143
|
+
val authoredWidth = when (val rawWidth = overlay.opt("strokeWidth")) {
|
|
144
|
+
null, JSONObject.NULL -> 3.0
|
|
145
|
+
is Number -> rawWidth.toDouble()
|
|
146
|
+
else -> 3.0
|
|
147
|
+
}
|
|
148
|
+
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
149
|
+
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
150
|
+
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(overlay.opt("strokeColor")).orEmpty()), width)
|
|
151
|
+
}
|
|
112
152
|
for (index in 0 until pendingChildren.length()) {
|
|
113
153
|
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
114
154
|
val type = child.optString("type").lowercase()
|
|
@@ -121,7 +161,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
121
161
|
if (type == "mapmarker" || type == "marker") {
|
|
122
162
|
points.firstOrNull()?.let { point ->
|
|
123
163
|
nextMarkers[id] = point to (props.opt("title") as? String)
|
|
124
|
-
(props.opt("pinColor") as? String)?.let { value ->
|
|
164
|
+
(props.opt("pinColor") as? String)?.let { value -> nextPinColors[id] = color(value) }
|
|
125
165
|
(props.opt("bearing") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.let { nextBearings[id] = (it % 360).toFloat() }
|
|
126
166
|
listOf(events.opt("onPress"), events.opt("onMarkerPress"))
|
|
127
167
|
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
@@ -147,6 +187,18 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
147
187
|
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
148
188
|
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
149
189
|
}
|
|
190
|
+
} else if (type == "mappolygon" || type == "polygon") {
|
|
191
|
+
if (points.size > 2) {
|
|
192
|
+
val rawWidth = props.opt("strokeWidth")
|
|
193
|
+
val authoredWidth = when (rawWidth) {
|
|
194
|
+
null, JSONObject.NULL -> 2.0
|
|
195
|
+
is Number -> rawWidth.toDouble()
|
|
196
|
+
else -> 2.0
|
|
197
|
+
}
|
|
198
|
+
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
199
|
+
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
200
|
+
nextPolygons[id] = PolygonValue(points, color(craftNativeOptionalString(props.opt("fillColor")).orEmpty(), Color.argb(72, 33, 150, 83)), color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
201
|
+
}
|
|
150
202
|
}
|
|
151
203
|
}
|
|
152
204
|
markerBearings = nextBearings
|
|
@@ -181,6 +233,22 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
181
233
|
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
182
234
|
}
|
|
183
235
|
}
|
|
236
|
+
polygons.keys.filter { nextPolygons[it] == null }.forEach { id -> polygons.remove(id)?.remove() }
|
|
237
|
+
for ((id, value) in nextPolygons) {
|
|
238
|
+
val polygon = polygons[id]
|
|
239
|
+
if (polygon != null) {
|
|
240
|
+
polygon.points = value.points
|
|
241
|
+
polygon.fillColor = value.fillColor
|
|
242
|
+
polygon.strokeColor = value.strokeColor
|
|
243
|
+
polygon.strokeWidth = value.strokeWidth
|
|
244
|
+
} else {
|
|
245
|
+
map.addPolygon(PolygonOptions().addAll(value.points).fillColor(value.fillColor).strokeColor(value.strokeColor).strokeWidth(value.strokeWidth))?.let { polygons[id] = it }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!refreshCamera) {
|
|
249
|
+
updateScale()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
184
252
|
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
185
253
|
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
186
254
|
if (explicitRegion != null) {
|
|
@@ -196,7 +264,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
196
264
|
initialCameraKey = key
|
|
197
265
|
}
|
|
198
266
|
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
199
|
-
val key = "fit:${overlayKey(nextMarkers, nextPolylines)}:${pendingProps.optJSONObject("edgePadding")}"
|
|
267
|
+
val key = "fit:${overlayKey(nextMarkers, nextPolylines, nextPolygons)}:${pendingProps.optJSONObject("edgePadding")}"
|
|
200
268
|
if (initialCameraKey != key) {
|
|
201
269
|
if (fitToBounds()) initialCameraKey = key
|
|
202
270
|
}
|
|
@@ -323,6 +391,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
323
391
|
private fun overlayKey(
|
|
324
392
|
markers: Map<String, Pair<LatLng, String?>>,
|
|
325
393
|
polylines: Map<String, Triple<List<LatLng>, Int, Float>>,
|
|
394
|
+
polygons: Map<String, PolygonValue>,
|
|
326
395
|
): String {
|
|
327
396
|
val markerKey = markers.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
328
397
|
"$id:${value.first.latitude},${value.first.longitude}"
|
|
@@ -330,7 +399,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
330
399
|
val polylineKey = polylines.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
331
400
|
"$id:" + value.first.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
332
401
|
}
|
|
333
|
-
|
|
402
|
+
val polygonKey = polygons.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
403
|
+
"$id:" + value.points.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
404
|
+
}
|
|
405
|
+
return "m[$markerKey]p[$polylineKey]g[$polygonKey]"
|
|
334
406
|
}
|
|
335
407
|
|
|
336
408
|
private fun fitToBounds(): Boolean {
|
|
@@ -340,6 +412,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
340
412
|
var count = 0
|
|
341
413
|
markers.values.forEach { builder.include(it.position); count++ }
|
|
342
414
|
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
415
|
+
polygons.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
416
|
+
polygons.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
343
417
|
if (count == 0) return false
|
|
344
418
|
post {
|
|
345
419
|
if (width > 0 && height > 0) {
|
|
@@ -388,6 +462,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
388
462
|
markers.clear()
|
|
389
463
|
polylines.values.forEach { it.remove() }
|
|
390
464
|
polylines.clear()
|
|
465
|
+
polygons.values.forEach { it.remove() }
|
|
466
|
+
polygons.clear()
|
|
391
467
|
mapView.onDestroy()
|
|
392
468
|
googleMap = null
|
|
393
469
|
}
|
|
@@ -435,6 +511,17 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
435
511
|
private fun finiteNumber(value: Any?): Double? =
|
|
436
512
|
(value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
437
513
|
|
|
438
|
-
private fun color(value: String): Int
|
|
514
|
+
private fun color(value: String): Int {
|
|
515
|
+
return color(value, Color.BLUE)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private fun color(value: String, fallback: Int): Int {
|
|
519
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
520
|
+
return when (value.trim().lowercase()) {
|
|
521
|
+
"label" -> if (dark) Color.WHITE else Color.BLACK
|
|
522
|
+
"secondarylabel", "secondary-label" -> if (dark) Color.LTGRAY else Color.DKGRAY
|
|
523
|
+
else -> runCatching { craftNativeColor(value) }.getOrDefault(fallback)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
439
526
|
}
|
|
440
527
|
|