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.
Files changed (41) hide show
  1. package/dist/android/src/index.d.ts +2 -0
  2. package/dist/android/src/index.js +22 -18
  3. package/dist/android/templates/CraftNative.kt.template +17 -0
  4. package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
  5. package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
  6. package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
  7. package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
  8. package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
  9. package/dist/android/templates/CraftNativeServices.kt.template +25 -0
  10. package/dist/android/templates/CraftNativeShell.kt.template +41 -9
  11. package/dist/android/templates/MainActivityNative.kt.template +369 -69
  12. package/dist/android/templates/build.gradle.kts.project.template +1 -1
  13. package/dist/api/mobile.d.ts +37 -1
  14. package/dist/browser.js +9 -9
  15. package/dist/cli.js +90 -673
  16. package/dist/components/index.d.ts +50 -9
  17. package/dist/index.cjs +394 -438
  18. package/dist/index.d.cts +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +446 -493
  21. package/dist/ios/src/index.d.ts +2 -0
  22. package/dist/ios/src/index.js +54 -53
  23. package/dist/ios/templates/CraftApp.swift +44 -3
  24. package/dist/ios/templates/CraftHealth.swift +217 -0
  25. package/dist/ios/templates/CraftHeartRate.swift +6 -3
  26. package/dist/ios/templates/CraftHybrid.swift +30 -22
  27. package/dist/ios/templates/CraftLiveActivities.swift +1 -1
  28. package/dist/ios/templates/CraftNativeActions.swift +87 -8
  29. package/dist/ios/templates/CraftNativeFiles.swift +15 -3
  30. package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
  31. package/dist/ios/templates/CraftNativeMutation.swift +36 -0
  32. package/dist/ios/templates/CraftNativeScreen.swift +826 -69
  33. package/dist/ios/templates/CraftNativeServices.swift +4 -2
  34. package/dist/ios/templates/CraftNativeUI.swift +10 -2
  35. package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
  36. package/dist/ios/templates/testing/HostApp.swift +9 -2
  37. package/dist/macos/templates/CraftNativeMutation.swift +36 -0
  38. package/dist/macos/templates/native_layout.zig +226 -2
  39. package/dist/mobile-testing.js +2 -2
  40. package/dist/mobile.js +101 -63
  41. 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
- syncNativeFonts,
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
- ANDROID_MIN_SDK
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(object : OnScrollListener() {
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(recycleItem)
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 -> runCatching { craftNativeColor(value) }.getOrNull()?.let { nextPinColors[id] = it } }
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
- return "m[$markerKey]p[$polylineKey]"
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 = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
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