craft-native 0.0.246 → 0.0.248
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 +11 -0
- package/dist/android/src/index.js +33 -21
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +136 -1
- package/dist/android/templates/CraftHeartRate.kt.template +938 -0
- package/dist/android/templates/CraftHeartRateCore.kt.template +485 -0
- 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 +59 -1
- package/dist/android/templates/CraftNativeShell.kt.template +54 -14
- 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 +39 -3
- 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 +37 -2
- package/dist/ios/templates/CraftHealth.swift +217 -0
- package/dist/ios/templates/CraftHeartRate.swift +6 -3
- package/dist/ios/templates/CraftHybrid.swift +18 -19
- 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 +813 -65
- 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 +21 -3
- package/dist/mobile.js +101 -63
- package/dist/testing/media-cache/media-cache.kt +4 -2
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -57,11 +57,24 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
57
57
|
rightMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
58
58
|
bottomMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
59
59
|
})
|
|
60
|
+
refreshThemeDefaults()
|
|
60
61
|
mapView.onCreate(null)
|
|
61
62
|
mapView.addOnDidFinishLoadingMapListener { if (!destroyed && initialCameraKey == null) applyPending() }
|
|
62
63
|
mapView.getMapAsync(this)
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
fun refreshThemeDefaults() {
|
|
67
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
68
|
+
val foreground = if (dark) Color.rgb(245, 245, 245) else Color.rgb(37, 51, 33)
|
|
69
|
+
val surface = if (dark) Color.argb(230, 38, 38, 38) else Color.argb(230, 255, 255, 255)
|
|
70
|
+
attribution.setTextColor(foreground)
|
|
71
|
+
attribution.setBackgroundColor(surface)
|
|
72
|
+
scale.setBackgroundColor(surface)
|
|
73
|
+
scaleLabel.setTextColor(foreground)
|
|
74
|
+
scaleLine.setBackgroundColor(foreground)
|
|
75
|
+
readyStyle?.let { updateOverlays(it) }
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
override fun onMapReady(nativeMap: MapLibreMap) {
|
|
66
79
|
if (destroyed) return
|
|
67
80
|
map = nativeMap
|
|
@@ -109,6 +122,14 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
109
122
|
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
110
123
|
|
|
111
124
|
private fun applyPending() {
|
|
125
|
+
// MapLibre may deliver style and render callbacks from its worker
|
|
126
|
+
// thread. All map, style, and GeoJSON source mutations must happen on
|
|
127
|
+
// Android's UI thread, so marshal the whole reconciliation before
|
|
128
|
+
// touching the native map object.
|
|
129
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
130
|
+
post { applyPending() }
|
|
131
|
+
return
|
|
132
|
+
}
|
|
112
133
|
val nativeMap = map ?: return
|
|
113
134
|
if (destroyed) return
|
|
114
135
|
nativeMap.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
@@ -200,6 +221,12 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
200
221
|
style.addLayer(LineLayer("craft-map-routes", "craft-map-overlays")
|
|
201
222
|
.withFilter(Expression.eq(Expression.get("kind"), "line"))
|
|
202
223
|
.withProperties(lineColor(Expression.toColor(Expression.get("color"))), lineWidth(Expression.get("width")), lineCap("round"), lineJoin("round")))
|
|
224
|
+
style.addLayer(FillLayer("craft-map-polygons", "craft-map-overlays")
|
|
225
|
+
.withFilter(Expression.eq(Expression.get("kind"), "polygon"))
|
|
226
|
+
.withProperties(fillColor(Expression.toColor(Expression.get("fillColor")))))
|
|
227
|
+
style.addLayer(LineLayer("craft-map-polygon-strokes", "craft-map-overlays")
|
|
228
|
+
.withFilter(Expression.eq(Expression.get("kind"), "polygon"))
|
|
229
|
+
.withProperties(lineColor(Expression.toColor(Expression.get("strokeColor"))), lineWidth(Expression.get("strokeWidth")), lineCap("round"), lineJoin("round")))
|
|
203
230
|
style.addLayer(CircleLayer("craft-map-markers", "craft-map-overlays")
|
|
204
231
|
.withFilter(Expression.eq(Expression.get("kind"), "marker"))
|
|
205
232
|
.withProperties(circleColor(Expression.toColor(Expression.get("color"))), circleRadius(7f), circleStrokeColor(Color.WHITE), circleStrokeWidth(2f)))
|
|
@@ -212,6 +239,42 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
212
239
|
val features = JSONArray()
|
|
213
240
|
val pointsForFit = mutableListOf<LatLng>()
|
|
214
241
|
markerHandlers.clear()
|
|
242
|
+
val dataMarkers = pendingProps.optJSONArray("markers")
|
|
243
|
+
for (index in 0 until (dataMarkers?.length() ?: 0)) {
|
|
244
|
+
val overlay = dataMarkers?.optJSONObject(index) ?: continue
|
|
245
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
246
|
+
val point = coordinates(overlay.optJSONArray("geometry")).firstOrNull() ?: continue
|
|
247
|
+
val bearing = finiteNumber(overlay.opt("bearing"))
|
|
248
|
+
val properties = JSONObject()
|
|
249
|
+
.put("craftOverlayId", id)
|
|
250
|
+
.put("kind", if (bearing == null) "marker" else "direction")
|
|
251
|
+
.put("craftMarkerId", id)
|
|
252
|
+
.put("bearing", (bearing ?: 0.0) % 360)
|
|
253
|
+
.put("color", cssColor(overlay.opt("pinColor"), Color.rgb(36, 114, 75)))
|
|
254
|
+
val geometry = JSONObject().put("type", "Point").put("coordinates", JSONArray().put(point.longitude).put(point.latitude))
|
|
255
|
+
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
256
|
+
pointsForFit.add(point)
|
|
257
|
+
}
|
|
258
|
+
val dataPolylines = pendingProps.optJSONArray("polylines")
|
|
259
|
+
for (index in 0 until (dataPolylines?.length() ?: 0)) {
|
|
260
|
+
val overlay = dataPolylines?.optJSONObject(index) ?: continue
|
|
261
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
262
|
+
val points = coordinates(overlay.optJSONArray("geometry"))
|
|
263
|
+
if (points.size <= 1) continue
|
|
264
|
+
val rawWidth = overlay.opt("strokeWidth")
|
|
265
|
+
val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 3.0 else finiteNumber(rawWidth) ?: continue
|
|
266
|
+
val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
|
|
267
|
+
val properties = JSONObject()
|
|
268
|
+
.put("craftOverlayId", id)
|
|
269
|
+
.put("kind", "line")
|
|
270
|
+
.put("color", cssColor(overlay.opt("strokeColor"), Color.BLUE))
|
|
271
|
+
.put("width", width)
|
|
272
|
+
val geometry = JSONObject().put("type", "LineString").put("coordinates", JSONArray().apply {
|
|
273
|
+
points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) }
|
|
274
|
+
})
|
|
275
|
+
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
276
|
+
pointsForFit.addAll(points)
|
|
277
|
+
}
|
|
215
278
|
for (index in 0 until pendingChildren.length()) {
|
|
216
279
|
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
217
280
|
val type = child.optString("type").lowercase()
|
|
@@ -231,11 +294,25 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
231
294
|
pointsForFit.add(point)
|
|
232
295
|
} else if ((type == "mappolyline" || type == "polyline") && points.size > 1) {
|
|
233
296
|
val authoredWidth = props.opt("strokeWidth")
|
|
234
|
-
val
|
|
235
|
-
|
|
297
|
+
val logicalWidth = if (authoredWidth == null || authoredWidth == JSONObject.NULL) 3.0 else finiteNumber(authoredWidth) ?: continue
|
|
298
|
+
// The shared contract is expressed in logical points. MapLibre's
|
|
299
|
+
// style line-width is in physical pixels, just like the Google
|
|
300
|
+
// Maps polyline width, so convert only at this host boundary.
|
|
301
|
+
val width = (logicalWidth * resources.displayMetrics.density)
|
|
302
|
+
.takeIf { it.isFinite() && it > 0 } ?: continue
|
|
236
303
|
properties.put("kind", "line").put("color", cssColor(props.opt("strokeColor"), Color.BLUE)).put("width", width)
|
|
237
304
|
geometry.put("type", "LineString").put("coordinates", JSONArray().apply { points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } })
|
|
238
305
|
pointsForFit.addAll(points)
|
|
306
|
+
} else if ((type == "mappolygon" || type == "polygon") && points.size > 2) {
|
|
307
|
+
val rawWidth = props.opt("strokeWidth")
|
|
308
|
+
val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 2.0 else finiteNumber(rawWidth) ?: continue
|
|
309
|
+
val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
|
|
310
|
+
val fill = cssColor(props.opt("fillColor"), Color.argb(72, 33, 150, 83))
|
|
311
|
+
val stroke = cssColor(props.opt("strokeColor"), Color.rgb(33, 150, 83))
|
|
312
|
+
properties.put("kind", "polygon").put("fillColor", fill).put("strokeColor", stroke).put("strokeWidth", width)
|
|
313
|
+
val ring = points + points.first()
|
|
314
|
+
geometry.put("type", "Polygon").put("coordinates", JSONArray().put(JSONArray().apply { ring.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } }))
|
|
315
|
+
pointsForFit.addAll(points)
|
|
239
316
|
} else continue
|
|
240
317
|
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
241
318
|
}
|
|
@@ -244,10 +321,20 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
244
321
|
}
|
|
245
322
|
|
|
246
323
|
private fun cssColor(value: Any?, fallback: Int): String {
|
|
247
|
-
val color = (value as? String)?.let {
|
|
324
|
+
val color = (value as? String)?.let { mapColor(it, fallback) } ?: fallback
|
|
248
325
|
return "rgba(${Color.red(color)},${Color.green(color)},${Color.blue(color)},${Color.alpha(color) / 255.0})"
|
|
249
326
|
}
|
|
250
327
|
|
|
328
|
+
private fun mapColor(raw: String, fallback: Int): Int {
|
|
329
|
+
val value = raw.trim().lowercase()
|
|
330
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
331
|
+
return when (value) {
|
|
332
|
+
"label" -> if (dark) Color.WHITE else Color.BLACK
|
|
333
|
+
"secondarylabel", "secondary-label" -> if (dark) Color.LTGRAY else Color.DKGRAY
|
|
334
|
+
else -> runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
private fun directionBitmap(): Bitmap {
|
|
252
339
|
val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
|
|
253
340
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
@@ -111,6 +111,14 @@ class CraftNativeMutationDocument {
|
|
|
111
111
|
moveChild(operation, candidateNodes)
|
|
112
112
|
operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
113
113
|
}
|
|
114
|
+
"reparentChild" -> {
|
|
115
|
+
val previousParent = reparentChild(operation, candidateNodes)
|
|
116
|
+
operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
117
|
+
previousParent.takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
118
|
+
// A child crossing parents needs the committed tree to
|
|
119
|
+
// be reconciled so the host can move its existing view.
|
|
120
|
+
requiresFullRender = true
|
|
121
|
+
}
|
|
114
122
|
"removeNode" -> {
|
|
115
123
|
val parent = candidateNodes[operation.optString("id")]?.parent
|
|
116
124
|
if (parent == null) requiresFullRender = true
|
|
@@ -152,6 +160,9 @@ class CraftNativeMutationDocument {
|
|
|
152
160
|
|
|
153
161
|
fun node(id: String): JSONObject? = materialize(id, nodes)
|
|
154
162
|
|
|
163
|
+
/** Reserve surviving protocol nodes before reusing an authored keyed slot. */
|
|
164
|
+
fun containsNode(id: String): Boolean = nodes.containsKey(id)
|
|
165
|
+
|
|
155
166
|
/** The committed parent used to redraw a child that has no native view. */
|
|
156
167
|
fun parentId(id: String): String? = nodes[id]?.parent
|
|
157
168
|
|
|
@@ -225,6 +236,27 @@ class CraftNativeMutationDocument {
|
|
|
225
236
|
parent.children.add(insertionIndex(operation, parent.children.size), childId)
|
|
226
237
|
}
|
|
227
238
|
|
|
239
|
+
private fun reparentChild(operation: JSONObject, nodes: MutableMap<String, Node>): String {
|
|
240
|
+
val parentId = requiredId(operation, "parentId", "reparentChild")
|
|
241
|
+
val childId = requiredId(operation, "childId", "reparentChild")
|
|
242
|
+
val parent = nodes[parentId] ?: fail("UNKNOWN_PARENT", "node $parentId does not exist")
|
|
243
|
+
val child = nodes[childId] ?: fail("UNKNOWN_NODE", "node $childId does not exist")
|
|
244
|
+
val previousParentId = child.parent
|
|
245
|
+
?: fail("NOT_ATTACHED", "node $childId is not attached to a parent")
|
|
246
|
+
val previousParent = nodes[previousParentId]
|
|
247
|
+
?: fail("INVALID_TREE", "node $childId has a missing parent")
|
|
248
|
+
if (!acceptsChildren(parent.type)) fail("INVALID_PARENT", "node $parentId cannot contain child nodes")
|
|
249
|
+
if (parentId == childId || parentId in descendants(childId, nodes)) {
|
|
250
|
+
fail("CYCLE", "reparenting node $childId would create a cycle")
|
|
251
|
+
}
|
|
252
|
+
if (parentId == previousParentId) fail("SAME_PARENT", "node $childId is already a child of $parentId")
|
|
253
|
+
val index = insertionIndex(operation, parent.children.size)
|
|
254
|
+
previousParent.children.remove(childId)
|
|
255
|
+
parent.children.add(index, childId)
|
|
256
|
+
child.parent = parentId
|
|
257
|
+
return previousParentId
|
|
258
|
+
}
|
|
259
|
+
|
|
228
260
|
private fun removeNode(
|
|
229
261
|
operation: JSONObject,
|
|
230
262
|
nodes: MutableMap<String, Node>,
|
|
@@ -22,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
|
|
|
22
22
|
if (handler == null) isRefreshing = false
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
fun resetForTeardown() {
|
|
26
|
+
refreshHandler = null
|
|
27
|
+
isRefreshing = false
|
|
28
|
+
isEnabled = false
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
fun setRefreshAppearance(props: JSONObject, fallback: Int) {
|
|
26
32
|
val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
|
|
27
33
|
val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
|
|
@@ -29,8 +35,7 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
override fun onDetachedFromWindow() {
|
|
32
|
-
|
|
33
|
-
isRefreshing = false
|
|
38
|
+
resetForTeardown()
|
|
34
39
|
super.onDetachedFromWindow()
|
|
35
40
|
}
|
|
36
41
|
}
|