craft-native 0.0.172 → 0.0.174
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 +4 -1
- package/dist/android/templates/AndroidManifest.xml.template +2 -0
- package/dist/android/templates/CraftNativeShell.kt.template +50 -0
- package/dist/android/templates/MainActivityNative.kt.template +441 -27
- package/dist/android/templates/build.gradle.kts.app.template +3 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +11 -0
- package/dist/ios/templates/CraftNativeActions.swift +174 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +443 -38
- package/dist/macos/src/index.d.ts +16 -0
- package/dist/macos/src/index.js +75 -0
- package/dist/macos/templates/CraftMacApplication.swift +342 -0
- package/dist/macos/templates/CraftMacRenderer.swift +481 -0
- package/dist/macos/templates/CraftMacRuntime.swift +266 -0
- package/dist/macos/templates/CraftNativeMutation.swift +390 -0
- package/dist/macos/templates/native_layout.zig +1651 -0
- package/package.json +9 -1
|
@@ -86,6 +86,17 @@ import androidx.javascriptengine.Message
|
|
|
86
86
|
import androidx.javascriptengine.MessagePort
|
|
87
87
|
import androidx.javascriptengine.MessagePortClient
|
|
88
88
|
import com.craft.runtime.CraftNative
|
|
89
|
+
import com.google.android.gms.maps.CameraUpdateFactory
|
|
90
|
+
import com.google.android.gms.maps.GoogleMap
|
|
91
|
+
import com.google.android.gms.maps.MapView
|
|
92
|
+
import com.google.android.gms.maps.OnMapReadyCallback
|
|
93
|
+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
94
|
+
import com.google.android.gms.maps.model.LatLng
|
|
95
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
96
|
+
import com.google.android.gms.maps.model.Marker
|
|
97
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
98
|
+
import com.google.android.gms.maps.model.Polyline
|
|
99
|
+
import com.google.android.gms.maps.model.PolylineOptions
|
|
89
100
|
import org.json.JSONArray
|
|
90
101
|
import org.json.JSONObject
|
|
91
102
|
import java.net.URL
|
|
@@ -459,6 +470,145 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
459
470
|
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
460
471
|
}
|
|
461
472
|
|
|
473
|
+
/** The Android host for the small, host-neutral native trail map contract. */
|
|
474
|
+
private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
475
|
+
private val mapView = MapView(context)
|
|
476
|
+
private var googleMap: GoogleMap? = null
|
|
477
|
+
private var pendingProps = JSONObject()
|
|
478
|
+
private var pendingChildren = JSONArray()
|
|
479
|
+
private val markers = mutableMapOf<String, Marker>()
|
|
480
|
+
private val polylines = mutableMapOf<String, Polyline>()
|
|
481
|
+
private val markerHandlers = mutableMapOf<String, String>()
|
|
482
|
+
private var onMarkerTap: ((String) -> Unit)? = null
|
|
483
|
+
|
|
484
|
+
init {
|
|
485
|
+
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
486
|
+
mapView.onCreate(null)
|
|
487
|
+
mapView.getMapAsync(this)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
override fun onMapReady(map: GoogleMap) {
|
|
491
|
+
googleMap = map
|
|
492
|
+
map.setOnMarkerClickListener { marker ->
|
|
493
|
+
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
494
|
+
onMarkerTap?.invoke(id)
|
|
495
|
+
false
|
|
496
|
+
}
|
|
497
|
+
applyPending()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fun configure(props: JSONObject, children: JSONArray, markerHandler: (String) -> Unit) {
|
|
501
|
+
pendingProps = props
|
|
502
|
+
pendingChildren = children
|
|
503
|
+
onMarkerTap = markerHandler
|
|
504
|
+
applyPending()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
508
|
+
|
|
509
|
+
private fun applyPending() {
|
|
510
|
+
val map = googleMap ?: return
|
|
511
|
+
map.uiSettings.isScrollGesturesEnabled = pendingProps.optBoolean("scrollEnabled", true)
|
|
512
|
+
map.uiSettings.isZoomGesturesEnabled = pendingProps.optBoolean("zoomEnabled", true)
|
|
513
|
+
map.uiSettings.isRotateGesturesEnabled = pendingProps.optBoolean("rotateEnabled", false)
|
|
514
|
+
map.mapType = when (pendingProps.optString("mapType").lowercase()) {
|
|
515
|
+
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
516
|
+
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
517
|
+
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
518
|
+
else -> GoogleMap.MAP_TYPE_NORMAL
|
|
519
|
+
}
|
|
520
|
+
val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
|
|
521
|
+
val nextHandlers = mutableMapOf<String, String>()
|
|
522
|
+
val nextPolylines = mutableMapOf<String, Pair<List<LatLng>, Int>>()
|
|
523
|
+
for (index in 0 until pendingChildren.length()) {
|
|
524
|
+
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
525
|
+
val type = child.optString("type").lowercase()
|
|
526
|
+
val props = child.optJSONObject("props") ?: JSONObject()
|
|
527
|
+
val events = child.optJSONObject("events") ?: JSONObject()
|
|
528
|
+
val id = props.opt("id")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
529
|
+
?: child.optString("id").takeIf { it.isNotBlank() }
|
|
530
|
+
?: "overlay-$index"
|
|
531
|
+
val points = coordinates(props.optJSONArray("geometry"))
|
|
532
|
+
if (type == "mapmarker" || type == "marker") {
|
|
533
|
+
points.firstOrNull()?.let { point ->
|
|
534
|
+
nextMarkers[id] = point to props.optString("title").takeIf { it.isNotBlank() }
|
|
535
|
+
events.optString("onPress").ifBlank { events.optString("onMarkerPress") }
|
|
536
|
+
.takeIf { it.isNotBlank() }?.let { nextHandlers[id] = it }
|
|
537
|
+
}
|
|
538
|
+
} else if (type == "mappolyline" || type == "polyline") {
|
|
539
|
+
if (points.size > 1) nextPolylines[id] = points to color(props.optString("strokeColor"))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
|
|
543
|
+
for ((id, value) in nextMarkers) {
|
|
544
|
+
val marker = markers[id]
|
|
545
|
+
if (marker != null) {
|
|
546
|
+
marker.position = value.first
|
|
547
|
+
marker.title = value.second
|
|
548
|
+
} else {
|
|
549
|
+
map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(BitmapDescriptorFactory.defaultMarker()))?.let { markers[id] = it }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
markerHandlers.clear()
|
|
553
|
+
markerHandlers.putAll(nextHandlers)
|
|
554
|
+
polylines.values.forEach { it.remove() }
|
|
555
|
+
polylines.clear()
|
|
556
|
+
for ((id, value) in nextPolylines) {
|
|
557
|
+
polylines[id] = map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(3f))
|
|
558
|
+
}
|
|
559
|
+
if (pendingProps.optBoolean("fitToBounds", false)) fitToBounds()
|
|
560
|
+
region(pendingProps.optJSONObject("region") ?: pendingProps.optJSONObject("initialRegion"))?.let { bounds ->
|
|
561
|
+
post {
|
|
562
|
+
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 24))
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private fun fitToBounds() {
|
|
568
|
+
val map = googleMap ?: return
|
|
569
|
+
val builder = LatLngBounds.Builder()
|
|
570
|
+
var count = 0
|
|
571
|
+
markers.values.forEach { builder.include(it.position); count++ }
|
|
572
|
+
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
573
|
+
if (count == 0) return
|
|
574
|
+
post {
|
|
575
|
+
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 24))
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
override fun onAttachedToWindow() {
|
|
580
|
+
super.onAttachedToWindow()
|
|
581
|
+
mapView.onResume()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
override fun onDetachedFromWindow() {
|
|
585
|
+
mapView.onPause()
|
|
586
|
+
super.onDetachedFromWindow()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
590
|
+
if (geometry == null) return emptyList()
|
|
591
|
+
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
592
|
+
val latitude = geometry.optDouble(index, Double.NaN)
|
|
593
|
+
val longitude = geometry.optDouble(index + 1, Double.NaN)
|
|
594
|
+
if (latitude.isFinite() && longitude.isFinite()) LatLng(latitude, longitude) else null
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private fun region(value: JSONObject?): LatLngBounds? {
|
|
599
|
+
val center = value?.optJSONObject("center") ?: return null
|
|
600
|
+
val span = value.optJSONObject("span") ?: return null
|
|
601
|
+
val latitude = center.optDouble("latitude", Double.NaN)
|
|
602
|
+
val longitude = center.optDouble("longitude", Double.NaN)
|
|
603
|
+
val latitudeDelta = span.optDouble("latitudeDelta", span.optDouble("latitude_delta", Double.NaN))
|
|
604
|
+
val longitudeDelta = span.optDouble("longitudeDelta", span.optDouble("longitude_delta", Double.NaN))
|
|
605
|
+
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite()) return null
|
|
606
|
+
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private fun color(value: String): Int = runCatching { Color.parseColor(value) }.getOrDefault(Color.BLUE)
|
|
610
|
+
}
|
|
611
|
+
|
|
462
612
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
463
613
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
464
614
|
var onLayoutChanged: (() -> Unit)? = null
|
|
@@ -474,6 +624,46 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
474
624
|
private var gridAutoRows: CraftNativeLength? = null
|
|
475
625
|
private var reverse = false
|
|
476
626
|
private var wrapReverse = false
|
|
627
|
+
private var authoredPadding = intArrayOf(0, 0, 0, 0)
|
|
628
|
+
private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
|
|
629
|
+
private var applyingPadding = false
|
|
630
|
+
private var safeAreaEdges = setOf("top", "right", "bottom", "left")
|
|
631
|
+
|
|
632
|
+
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
|
633
|
+
if (!applyingPadding) authoredPadding = intArrayOf(left, top, right, bottom)
|
|
634
|
+
applyingPadding = true
|
|
635
|
+
super.setPadding(
|
|
636
|
+
left + safeAreaPadding[0],
|
|
637
|
+
top + safeAreaPadding[1],
|
|
638
|
+
right + safeAreaPadding[2],
|
|
639
|
+
bottom + safeAreaPadding[3],
|
|
640
|
+
)
|
|
641
|
+
applyingPadding = false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
fun setSafeAreaEdges(raw: JSONArray?) {
|
|
645
|
+
safeAreaEdges = if (raw == null) {
|
|
646
|
+
setOf("top", "right", "bottom", "left")
|
|
647
|
+
} else {
|
|
648
|
+
(0 until raw.length()).mapNotNull { raw.optString(it).takeIf { edge -> edge in setOf("top", "right", "bottom", "left") } }.toSet()
|
|
649
|
+
}
|
|
650
|
+
applySafeAreaPadding()
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
fun setWindowSafeInsets(left: Int, top: Int, right: Int, bottom: Int) {
|
|
654
|
+
val insets = intArrayOf(left, top, right, bottom)
|
|
655
|
+
safeAreaPadding = intArrayOf(
|
|
656
|
+
if ("left" in safeAreaEdges) insets[0] else 0,
|
|
657
|
+
if ("top" in safeAreaEdges) insets[1] else 0,
|
|
658
|
+
if ("right" in safeAreaEdges) insets[2] else 0,
|
|
659
|
+
if ("bottom" in safeAreaEdges) insets[3] else 0,
|
|
660
|
+
)
|
|
661
|
+
applySafeAreaPadding()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private fun applySafeAreaPadding() {
|
|
665
|
+
setPadding(authoredPadding[0], authoredPadding[1], authoredPadding[2], authoredPadding[3])
|
|
666
|
+
}
|
|
477
667
|
|
|
478
668
|
/**
|
|
479
669
|
* What a child measures at a pair of specs, remembered for the frame.
|
|
@@ -643,7 +833,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
643
833
|
if (index > 0 && lineIds != null && lineIds[index] != lineIds[index - 1]) {
|
|
644
834
|
lines += line
|
|
645
835
|
line = Line()
|
|
646
|
-
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain) {
|
|
836
|
+
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain + 0.5f * (line.entries.size + 1)) {
|
|
837
|
+
// Each size is rounded to whole pixels, up to half a pixel
|
|
838
|
+
// over: two 60-point items fill a 120-point row exactly
|
|
839
|
+
// in points, and at 2.625 pixels per point (157.5 + 157.5
|
|
840
|
+
// in 315) rounding alone must not wrap the second.
|
|
647
841
|
lines += line
|
|
648
842
|
line = Line()
|
|
649
843
|
}
|
|
@@ -1343,6 +1537,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1343
1537
|
if (handler == null) isRefreshing = false
|
|
1344
1538
|
}
|
|
1345
1539
|
|
|
1540
|
+
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
1541
|
+
val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
|
|
1542
|
+
if (tint != null) setColorSchemeColors(craftNativeColor(tint))
|
|
1543
|
+
else setColorSchemeColors(Color.WHITE)
|
|
1544
|
+
val background = props.optString("progressBackgroundColor").takeIf { it.isNotBlank() }
|
|
1545
|
+
if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
|
|
1546
|
+
else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
|
|
1547
|
+
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
1548
|
+
if (offset.isFinite()) setProgressViewOffset(true, 0, (offset * density).roundToInt())
|
|
1549
|
+
else setProgressViewOffset(false, 0, 0)
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1346
1552
|
override fun onDetachedFromWindow() {
|
|
1347
1553
|
refreshHandler = null
|
|
1348
1554
|
isRefreshing = false
|
|
@@ -1783,12 +1989,15 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1783
1989
|
|
|
1784
1990
|
private fun changed() {
|
|
1785
1991
|
if (suppressChanges) return
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
picker.minute
|
|
1791
|
-
|
|
1992
|
+
// A date-only picker has no time half to snap, but its change is
|
|
1993
|
+
// still the screen's to hear.
|
|
1994
|
+
timePicker?.let { picker ->
|
|
1995
|
+
val snapped = ((picker.minute + minuteInterval / 2) / minuteInterval * minuteInterval).coerceAtMost(59)
|
|
1996
|
+
if (snapped != picker.minute) {
|
|
1997
|
+
suppressChanges = true
|
|
1998
|
+
picker.minute = snapped
|
|
1999
|
+
suppressChanges = false
|
|
2000
|
+
}
|
|
1792
2001
|
}
|
|
1793
2002
|
onValueChanged?.invoke()
|
|
1794
2003
|
}
|
|
@@ -2182,6 +2391,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2182
2391
|
) {
|
|
2183
2392
|
/** What the screen set with `craft.navigation.setOptions`, merged. */
|
|
2184
2393
|
var options = JSONObject()
|
|
2394
|
+
/** Overrides from a native `<StatusBar>` node, reset on each render. */
|
|
2395
|
+
var statusBarStyle: String? = null
|
|
2396
|
+
var statusBarHidden: Boolean? = null
|
|
2185
2397
|
/** Shown before: its next showing is a `SCREEN_FOCUS`. */
|
|
2186
2398
|
var shown = false
|
|
2187
2399
|
/** The app path it was opened for (`/m/workout/42`), to find it again. */
|
|
@@ -2209,6 +2421,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2209
2421
|
val nodeViews = mutableMapOf<String, View>()
|
|
2210
2422
|
val nodePaths = mutableMapOf<String, String>()
|
|
2211
2423
|
val handlers = mutableMapOf<View, String>()
|
|
2424
|
+
val linkTargets = mutableMapOf<View, String>()
|
|
2212
2425
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
2213
2426
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
2214
2427
|
val longPressHandlers = mutableMapOf<View, String>()
|
|
@@ -2279,6 +2492,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2279
2492
|
private val tabStacks = mutableListOf<MutableList<Screen>>()
|
|
2280
2493
|
private var currentTab = 0
|
|
2281
2494
|
private var visibleScreen: Screen? = null
|
|
2495
|
+
private var tabSwipeTarget: Screen? = null
|
|
2496
|
+
private var tabSwipeTargetIndex = -1
|
|
2497
|
+
private var tabSwipeDirection = 0
|
|
2282
2498
|
/** The app script's isolate (`nativeApp`), when the app has one. */
|
|
2283
2499
|
private var appScreen: Screen? = null
|
|
2284
2500
|
private val urlSchemes = mutableSetOf<String>()
|
|
@@ -2298,6 +2514,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2298
2514
|
private var initialDeepLink: Uri? = null
|
|
2299
2515
|
private var initialDeepLinkClaimed = false
|
|
2300
2516
|
private var defaultTextColor = Color.WHITE
|
|
2517
|
+
private var windowSafeInsets = intArrayOf(0, 0, 0, 0)
|
|
2301
2518
|
private var useMessagePorts = false
|
|
2302
2519
|
private val imageExecutor = Executors.newFixedThreadPool(2)
|
|
2303
2520
|
/** Lays out each isolate's prelude (the synchronous stores) off the main thread. */
|
|
@@ -2330,8 +2547,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2330
2547
|
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
2331
2548
|
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
2332
2549
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
2550
|
+
windowSafeInsets = intArrayOf(safe.left, safe.top, safe.right, safe.bottom)
|
|
2333
2551
|
keyboardInset = max(0, ime.bottom - safe.bottom)
|
|
2334
|
-
|
|
2552
|
+
updateSafeAreaViews()
|
|
2335
2553
|
updateKeyboardAvoidingViews()
|
|
2336
2554
|
view.requestLayout()
|
|
2337
2555
|
view.post { screens.lastOrNull()?.takeIf { it.active }?.let(::emitLayoutEvents) }
|
|
@@ -2443,6 +2661,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2443
2661
|
haptic()
|
|
2444
2662
|
if (index == currentTab) popToRoot() else selectTab(index)
|
|
2445
2663
|
}
|
|
2664
|
+
bars.onTabSwipeStart = { dx, x, y -> beginTabSwipe(dx, x, y) }
|
|
2665
|
+
bars.onTabSwipeMove = { dx -> updateTabSwipe(dx) }
|
|
2666
|
+
bars.onTabSwipeEnd = { dx, cancelled -> finishTabSwipe(dx, cancelled) }
|
|
2446
2667
|
root.addView(bars)
|
|
2447
2668
|
chrome = bars
|
|
2448
2669
|
tabStacks.clear()
|
|
@@ -2466,12 +2687,97 @@ class MainActivity : AppCompatActivity() {
|
|
|
2466
2687
|
currentTab = index
|
|
2467
2688
|
screens = tabStacks[index]
|
|
2468
2689
|
chrome?.selectTab(index)
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2690
|
+
ensureTabRoot(index)?.let(::show)
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
/** Mount a tab's root without stealing focus, for an interactive swipe. */
|
|
2694
|
+
private fun ensureTabRoot(index: Int): Screen? {
|
|
2695
|
+
if (index !in tabStacks.indices) return null
|
|
2696
|
+
tabStacks[index].lastOrNull()?.let { return it }
|
|
2697
|
+
val previous = screens
|
|
2698
|
+
screens = tabStacks[index]
|
|
2699
|
+
val match = routes.tabs.getOrNull(index)?.let { routes.match(it.id) }
|
|
2700
|
+
val screen = if (match != null) openScreen(match.screen, match.params, showImmediately = false) else openScreen(null, showImmediately = false)
|
|
2701
|
+
screen?.path = match?.path
|
|
2702
|
+
screens = previous
|
|
2703
|
+
return screen
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/** Start an interactive horizontal transition to the adjacent tab. */
|
|
2707
|
+
private fun beginTabSwipe(dx: Float, x: Float, y: Float): Boolean {
|
|
2708
|
+
if (tabSwipeTarget != null || screens.size != 1 || chrome?.tabSwipeEnabled != true) return false
|
|
2709
|
+
val direction = if (dx < 0) 1 else -1
|
|
2710
|
+
val targetIndex = currentTab + direction
|
|
2711
|
+
val current = visibleScreen ?: return false
|
|
2712
|
+
val target = ensureTabRoot(targetIndex) ?: return false
|
|
2713
|
+
val host = chrome?.content ?: return false
|
|
2714
|
+
if (target === current || host.width <= 0) return false
|
|
2715
|
+
if (horizontalScrollCanConsume(current.view, host, x, y, dx)) return false
|
|
2716
|
+
(target.view.parent as? ViewGroup)?.removeView(target.view)
|
|
2717
|
+
host.addView(target.view, FrameLayout.LayoutParams(-1, -1))
|
|
2718
|
+
target.view.translationX = if (direction > 0) host.width.toFloat() else -host.width.toFloat()
|
|
2719
|
+
current.view.translationX = 0f
|
|
2720
|
+
host.bringChildToFront(target.view)
|
|
2721
|
+
tabSwipeTarget = target
|
|
2722
|
+
tabSwipeTargetIndex = targetIndex
|
|
2723
|
+
tabSwipeDirection = direction
|
|
2724
|
+
chrome?.selectTab(targetIndex)
|
|
2725
|
+
return true
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
private fun updateTabSwipe(dx: Float) {
|
|
2729
|
+
val current = visibleScreen ?: return
|
|
2730
|
+
val target = tabSwipeTarget ?: return
|
|
2731
|
+
val width = (chrome?.content?.width ?: return).toFloat().coerceAtLeast(1f)
|
|
2732
|
+
val offset = dx.coerceIn(-width, width)
|
|
2733
|
+
target.view.translationX = (if (tabSwipeDirection > 0) width else -width) + offset
|
|
2734
|
+
current.view.translationX = offset
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
private fun finishTabSwipe(dx: Float, cancelled: Boolean) {
|
|
2738
|
+
val current = visibleScreen ?: return
|
|
2739
|
+
val target = tabSwipeTarget ?: return
|
|
2740
|
+
val host = chrome?.content ?: return
|
|
2741
|
+
val width = host.width.toFloat().coerceAtLeast(1f)
|
|
2742
|
+
val complete = !cancelled && kotlin.math.abs(dx) >= width * 0.28f
|
|
2743
|
+
val endOffset = if (complete) {
|
|
2744
|
+
if (tabSwipeDirection > 0) -width else width
|
|
2745
|
+
} else 0f
|
|
2746
|
+
val targetOffset = if (complete) 0f else if (tabSwipeDirection > 0) width else -width
|
|
2747
|
+
current.view.animate().translationX(endOffset).setDuration(180).start()
|
|
2748
|
+
target.view.animate().translationX(targetOffset).setDuration(180).withEndAction {
|
|
2749
|
+
target.view.animate().setListener(null)
|
|
2750
|
+
if (complete) {
|
|
2751
|
+
current.view.translationX = 0f
|
|
2752
|
+
currentTab = tabSwipeTargetIndex
|
|
2753
|
+
screens = tabStacks[currentTab]
|
|
2754
|
+
tabSwipeTarget = null
|
|
2755
|
+
tabSwipeTargetIndex = -1
|
|
2756
|
+
tabSwipeDirection = 0
|
|
2757
|
+
haptic()
|
|
2758
|
+
show(target)
|
|
2759
|
+
} else {
|
|
2760
|
+
host.removeView(target.view)
|
|
2761
|
+
current.view.translationX = 0f
|
|
2762
|
+
chrome?.selectTab(currentTab)
|
|
2763
|
+
tabSwipeTarget = null
|
|
2764
|
+
tabSwipeTargetIndex = -1
|
|
2765
|
+
tabSwipeDirection = 0
|
|
2766
|
+
}
|
|
2767
|
+
}.start()
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
private fun horizontalScrollCanConsume(view: View, host: ViewGroup, x: Float, y: Float, dx: Float): Boolean {
|
|
2771
|
+
val localX = x - view.left
|
|
2772
|
+
val localY = y - host.top - view.top
|
|
2773
|
+
if (view !is ViewGroup || localX < 0 || localY < 0 || localX > view.width || localY > view.height) return false
|
|
2774
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
2775
|
+
val child = view.getChildAt(index)
|
|
2776
|
+
if (!child.isShown || localX < child.left || localX > child.right || localY < child.top || localY > child.bottom) continue
|
|
2777
|
+
if (child is android.widget.HorizontalScrollView && child.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
2778
|
+
if (horizontalScrollCanConsume(child, host, localX - child.left, localY - child.top, dx)) return true
|
|
2474
2779
|
}
|
|
2780
|
+
return false
|
|
2475
2781
|
}
|
|
2476
2782
|
|
|
2477
2783
|
/**
|
|
@@ -2523,7 +2829,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2523
2829
|
services.perform("Browser", "open", JSONArray().put(url), {})
|
|
2524
2830
|
}
|
|
2525
2831
|
|
|
2526
|
-
private fun openScreen(route: String?, params: JSONObject = JSONObject(), headless: Boolean = false): Screen? {
|
|
2832
|
+
private fun openScreen(route: String?, params: JSONObject = JSONObject(), headless: Boolean = false, showImmediately: Boolean = true): Screen? {
|
|
2527
2833
|
val engine = sandbox ?: return null
|
|
2528
2834
|
val isolate = engine.createIsolate()
|
|
2529
2835
|
val view = CraftNativeFlexLayout(this).apply {
|
|
@@ -2560,7 +2866,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2560
2866
|
}
|
|
2561
2867
|
if (!headless) {
|
|
2562
2868
|
screens.add(activeScreen)
|
|
2563
|
-
show(activeScreen)
|
|
2869
|
+
if (showImmediately) show(activeScreen)
|
|
2564
2870
|
}
|
|
2565
2871
|
val routePrelude = """
|
|
2566
2872
|
globalThis.__stxNativeRoute = ${route?.let { JSONObject.quote(it) } ?: "undefined"};
|
|
@@ -2844,6 +3150,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
2844
3150
|
else -> null
|
|
2845
3151
|
}
|
|
2846
3152
|
}.forEach { it.refreshThemeDefaults() }
|
|
3153
|
+
updateSafeAreaViews()
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
private fun updateSafeAreaViews() {
|
|
3157
|
+
if (!::root.isInitialized) return
|
|
3158
|
+
val active = visibleScreen ?: screens.lastOrNull { it.active }
|
|
3159
|
+
val rootNode = active?.mutations?.node("root")
|
|
3160
|
+
val safeRoot = rootNode?.let { canonicalType(it.optString("type")) == "SafeAreaView" } == true
|
|
3161
|
+
root.setPadding(
|
|
3162
|
+
if (safeRoot) 0 else windowSafeInsets[0],
|
|
3163
|
+
if (safeRoot) 0 else windowSafeInsets[1],
|
|
3164
|
+
if (safeRoot) 0 else windowSafeInsets[2],
|
|
3165
|
+
if (safeRoot) 0 else windowSafeInsets[3],
|
|
3166
|
+
)
|
|
3167
|
+
val tree = active?.view?.getChildAt(0) as? CraftNativeFlexLayout
|
|
3168
|
+
if (safeRoot) tree?.setWindowSafeInsets(windowSafeInsets[0], windowSafeInsets[1], windowSafeInsets[2], windowSafeInsets[3])
|
|
3169
|
+
else tree?.setWindowSafeInsets(0, 0, 0, 0)
|
|
2847
3170
|
}
|
|
2848
3171
|
|
|
2849
3172
|
private fun refreshThemeDefaults() {
|
|
@@ -3061,6 +3384,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3061
3384
|
|
|
3062
3385
|
private fun renderCommitted(screen: Screen, document: JSONObject) {
|
|
3063
3386
|
// Keep live controls (especially the focused EditText) across renders.
|
|
3387
|
+
screen.statusBarStyle = null
|
|
3388
|
+
screen.statusBarHidden = null
|
|
3064
3389
|
val next = mutableMapOf<String, View>()
|
|
3065
3390
|
screen.nodeViews.clear()
|
|
3066
3391
|
screen.nodePaths.clear()
|
|
@@ -3075,10 +3400,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
3075
3400
|
screen.controls.putAll(next)
|
|
3076
3401
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
3077
3402
|
screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
3403
|
+
updateSafeAreaViews()
|
|
3078
3404
|
// A full render can change a view's custom layout style without
|
|
3079
3405
|
// changing the root bounds. Ensure native @layout observers still see
|
|
3080
3406
|
// the settled child frame after the next traversal.
|
|
3081
3407
|
scheduleLayoutEvents(screen)
|
|
3408
|
+
if (visibleScreen === screen) applyStatusBar(screen)
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
private fun applyStatusBar(screen: Screen) {
|
|
3412
|
+
val controller = WindowCompat.getInsetsController(window, window.decorView)
|
|
3413
|
+
val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
3414
|
+
controller.isAppearanceLightStatusBars = when (screen.statusBarStyle?.lowercase()) {
|
|
3415
|
+
"light-content", "light" -> false
|
|
3416
|
+
"dark-content", "dark" -> true
|
|
3417
|
+
else -> !night
|
|
3418
|
+
}
|
|
3419
|
+
if (screen.statusBarHidden == true) controller.hide(WindowInsetsCompat.Type.statusBars())
|
|
3420
|
+
else controller.show(WindowInsetsCompat.Type.statusBars())
|
|
3082
3421
|
}
|
|
3083
3422
|
|
|
3084
3423
|
private fun clearRenderedTree(screen: Screen) {
|
|
@@ -3088,6 +3427,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3088
3427
|
screen.nodeViews.clear()
|
|
3089
3428
|
screen.nodePaths.clear()
|
|
3090
3429
|
screen.handlers.clear()
|
|
3430
|
+
screen.linkTargets.clear()
|
|
3091
3431
|
screen.flatListRows.clear()
|
|
3092
3432
|
screen.flatListOwners.clear()
|
|
3093
3433
|
screen.flatListOwnerByView.clear()
|
|
@@ -3124,6 +3464,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3124
3464
|
}
|
|
3125
3465
|
configureText(this, childText(children), style)
|
|
3126
3466
|
}
|
|
3467
|
+
"StatusBar" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3468
|
+
screen.statusBarStyle = props.optString("barStyle").takeIf { it.isNotBlank() }
|
|
3469
|
+
screen.statusBarHidden = if (props.has("hidden")) props.optBoolean("hidden") else null
|
|
3470
|
+
visibility = View.GONE
|
|
3471
|
+
}
|
|
3472
|
+
"RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3473
|
+
visibility = View.GONE
|
|
3474
|
+
}
|
|
3127
3475
|
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
3128
3476
|
includeFontPadding = false
|
|
3129
3477
|
configureText(this, props.optString("title").ifBlank { childText(children) }, style)
|
|
@@ -3133,9 +3481,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
3133
3481
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3134
3482
|
isClickable = true
|
|
3135
3483
|
isFocusable = true
|
|
3484
|
+
val target = listOf(props.optString("href"), props.optString("to"))
|
|
3485
|
+
.firstOrNull { it.isNotBlank() }
|
|
3486
|
+
?.trim()
|
|
3487
|
+
?.takeIf { !props.optBoolean("disabled", false) }
|
|
3488
|
+
if (target != null) screen.linkTargets[this] = target else screen.linkTargets.remove(this)
|
|
3136
3489
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
3137
3490
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3138
3491
|
setOnClickListener {
|
|
3492
|
+
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3139
3493
|
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
3140
3494
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3141
3495
|
}
|
|
@@ -3540,7 +3894,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3540
3894
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
3541
3895
|
val onError = events.optString("onError")
|
|
3542
3896
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
3543
|
-
updateImage(screen, this, props.opt("source"))
|
|
3897
|
+
updateImage(screen, this, props.opt("source"), props.opt("defaultSource"))
|
|
3544
3898
|
}
|
|
3545
3899
|
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
3546
3900
|
image.scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
@@ -3557,7 +3911,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3557
3911
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
3558
3912
|
val onError = events.optString("onError")
|
|
3559
3913
|
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
3560
|
-
updateImage(screen, image, props.opt("source"))
|
|
3914
|
+
updateImage(screen, image, props.opt("source"), props.opt("defaultSource"))
|
|
3561
3915
|
configureContainer(content, style)
|
|
3562
3916
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
3563
3917
|
}
|
|
@@ -3571,6 +3925,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3571
3925
|
controls = props.optBoolean("controls", false),
|
|
3572
3926
|
)
|
|
3573
3927
|
}
|
|
3928
|
+
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
3929
|
+
configure(props, children) { markerId ->
|
|
3930
|
+
markerHandler(markerId)?.let { handler ->
|
|
3931
|
+
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("id", markerId)))
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3574
3935
|
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
3575
3936
|
drawing = node
|
|
3576
3937
|
}
|
|
@@ -3590,10 +3951,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
3590
3951
|
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
3591
3952
|
)
|
|
3592
3953
|
setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
3593
|
-
val
|
|
3954
|
+
val refreshNode = (0 until children.length()).asSequence()
|
|
3955
|
+
.mapNotNull { children.optJSONObject(it) }
|
|
3956
|
+
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
3957
|
+
val refreshProps = refreshNode?.optJSONObject("props") ?: JSONObject()
|
|
3958
|
+
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
3959
|
+
val refreshHandlerName = refreshEvents.optString("onRefresh").takeIf { it.isNotBlank() }
|
|
3960
|
+
?: events.optString("onRefresh").takeIf { it.isNotBlank() }
|
|
3961
|
+
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
3594
3962
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
3595
3963
|
}
|
|
3596
|
-
setRefreshHandler(refreshHandler, props.optBoolean("refreshing", false))
|
|
3964
|
+
setRefreshHandler(refreshHandler, if (refreshNode == null) props.optBoolean("refreshing", false) else refreshProps.optBoolean("refreshing", false))
|
|
3965
|
+
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
3597
3966
|
val onScroll = events.optString("onScroll")
|
|
3598
3967
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
3599
3968
|
val onBegin = events.optString("onScrollBeginDrag")
|
|
@@ -3647,7 +4016,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
3647
4016
|
}
|
|
3648
4017
|
}
|
|
3649
4018
|
configureContainer(content, contentStyle)
|
|
3650
|
-
|
|
4019
|
+
val contentChildren = JSONArray()
|
|
4020
|
+
for (index in 0 until children.length()) {
|
|
4021
|
+
val child = children.opt(index)
|
|
4022
|
+
if (child !is JSONObject || canonicalType(child.optString("type")) != "RefreshControl") contentChildren.put(child)
|
|
4023
|
+
}
|
|
4024
|
+
reconcileChildren(screen, content, contentChildren, path, next, contentStyle, previousControls)
|
|
3651
4025
|
}
|
|
3652
4026
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
3653
4027
|
val nativeList = list
|
|
@@ -3665,6 +4039,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3665
4039
|
}
|
|
3666
4040
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3667
4041
|
configureContainer(this, style)
|
|
4042
|
+
if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
|
|
3668
4043
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
3669
4044
|
}
|
|
3670
4045
|
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
@@ -3686,15 +4061,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
3686
4061
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
3687
4062
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
3688
4063
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
3689
|
-
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView") {
|
|
4064
|
+
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView" && type != "StatusBar" && type != "RefreshControl" && type != "Map") {
|
|
3690
4065
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
3691
4066
|
val disabled = props.optBoolean("disabled", false)
|
|
4067
|
+
val target = listOf(props.optString("href"), props.optString("to"))
|
|
4068
|
+
.firstOrNull { it.isNotBlank() }
|
|
4069
|
+
?.trim()
|
|
4070
|
+
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
4071
|
+
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
3692
4072
|
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null
|
|
3693
|
-
if ((handler.isNotBlank() || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
4073
|
+
if ((handler.isNotBlank() || target != null || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
3694
4074
|
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
3695
4075
|
control.isClickable = true
|
|
3696
4076
|
if (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable")) {
|
|
3697
4077
|
control.setOnClickListener {
|
|
4078
|
+
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3698
4079
|
val current = screen.handlers[it] ?: return@setOnClickListener
|
|
3699
4080
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3700
4081
|
}
|
|
@@ -3715,6 +4096,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3715
4096
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3716
4097
|
}
|
|
3717
4098
|
if (event.actionMasked == android.view.MotionEvent.ACTION_UP) {
|
|
4099
|
+
screen.linkTargets[view]?.let { target -> navigateOpen(target); return@setOnTouchListener true }
|
|
3718
4100
|
val current = screen.handlers[view] ?: return@setOnTouchListener true
|
|
3719
4101
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3720
4102
|
}
|
|
@@ -3726,6 +4108,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3726
4108
|
} else {
|
|
3727
4109
|
control.setOnTouchListener(null)
|
|
3728
4110
|
control.setOnClickListener {
|
|
4111
|
+
screen.linkTargets[control]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3729
4112
|
val current = screen.handlers[control] ?: return@setOnClickListener
|
|
3730
4113
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3731
4114
|
}
|
|
@@ -3833,10 +4216,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3833
4216
|
"switch" -> "Switch"
|
|
3834
4217
|
"slider" -> "Slider"
|
|
3835
4218
|
"activityindicator" -> "ActivityIndicator"
|
|
4219
|
+
"statusbar" -> "StatusBar"
|
|
4220
|
+
"refreshcontrol" -> "RefreshControl"
|
|
3836
4221
|
"image" -> "Image"
|
|
3837
4222
|
"imagebackground" -> "ImageBackground"
|
|
3838
4223
|
"keyboardavoidingview" -> "KeyboardAvoidingView"
|
|
3839
4224
|
"video" -> "Video"
|
|
4225
|
+
"map" -> "Map"
|
|
3840
4226
|
"icon" -> "Icon"
|
|
3841
4227
|
"scrollview" -> "ScrollView"
|
|
3842
4228
|
"sectionlist" -> "SectionList"
|
|
@@ -4274,6 +4660,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
4274
4660
|
|
|
4275
4661
|
private fun applyTextDecoration(view: TextView, style: JSONObject) {
|
|
4276
4662
|
val line = style.optString("textDecorationLine")
|
|
4663
|
+
if (view is EditText) {
|
|
4664
|
+
// A field's own text, spanned in place: replacing it would move
|
|
4665
|
+
// the caret to the start and tell the screen the text changed.
|
|
4666
|
+
val editable = view.text ?: return
|
|
4667
|
+
editable.getSpans(0, editable.length, CraftTextDecorationSpan::class.java).forEach(editable::removeSpan)
|
|
4668
|
+
if (line.isBlank() || line == "none" || editable.isEmpty()) return
|
|
4669
|
+
val fieldColor = style.optString("textDecorationColor").takeIf { it.isNotBlank() }?.let { color(it, Color.TRANSPARENT) }
|
|
4670
|
+
editable.setSpan(
|
|
4671
|
+
CraftTextDecorationSpan(line, style.optString("textDecorationStyle").ifBlank { "solid" }, fieldColor),
|
|
4672
|
+
0,
|
|
4673
|
+
editable.length,
|
|
4674
|
+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
4675
|
+
)
|
|
4676
|
+
return
|
|
4677
|
+
}
|
|
4277
4678
|
val text = view.text?.toString() ?: return
|
|
4278
4679
|
if (line.isBlank() || line == "none" || text.isEmpty()) {
|
|
4279
4680
|
if (view.text is Spannable) view.text = text
|
|
@@ -4311,12 +4712,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
4311
4712
|
host == "::1" || host.endsWith(".localhost")
|
|
4312
4713
|
}
|
|
4313
4714
|
|
|
4314
|
-
private fun
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4715
|
+
private fun imageUri(source: Any?): String = when (source) {
|
|
4716
|
+
is String -> source
|
|
4717
|
+
is JSONObject -> source.optString("uri")
|
|
4718
|
+
else -> ""
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
private fun immediateImage(uri: String): Bitmap? {
|
|
4722
|
+
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
4723
|
+
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
4724
|
+
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
4319
4725
|
}
|
|
4726
|
+
if (!uri.contains(':')) return runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
4727
|
+
return null
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
private fun updateImage(screen: Screen, view: ImageView, source: Any?, defaultSource: Any? = null) {
|
|
4731
|
+
val uri = imageUri(source)
|
|
4320
4732
|
if (uri.isBlank()) {
|
|
4321
4733
|
if (imageSources[view] == "") return
|
|
4322
4734
|
imageJobs.remove(view)?.cancel(true)
|
|
@@ -4331,6 +4743,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4331
4743
|
imageSources[view] = uri
|
|
4332
4744
|
imageErrors.remove(view)
|
|
4333
4745
|
view.setImageDrawable(null)
|
|
4746
|
+
immediateImage(imageUri(defaultSource))?.let(view::setImageBitmap)
|
|
4334
4747
|
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
4335
4748
|
|
|
4336
4749
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
@@ -4847,6 +5260,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4847
5260
|
if (!preservingInputDrafts) identity?.let(screen.drafts::remove)
|
|
4848
5261
|
}
|
|
4849
5262
|
released.forEach(defaultBackgrounds::remove)
|
|
5263
|
+
screen.linkTargets.keys.removeAll(released)
|
|
4850
5264
|
screen.handlers.keys.removeAll(released)
|
|
4851
5265
|
screen.pressInHandlers.keys.removeAll(released)
|
|
4852
5266
|
screen.pressOutHandlers.keys.removeAll(released)
|