craft-native 0.0.173 → 0.0.175
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 +454 -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 +5 -0
- package/dist/ios/templates/CraftNativeActions.swift +174 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +454 -38
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
3
|
import android.animation.ValueAnimator
|
|
4
|
+
import android.Manifest
|
|
4
5
|
import android.content.ClipData
|
|
5
6
|
import android.content.ClipboardManager
|
|
6
7
|
import android.content.Context
|
|
8
|
+
import android.content.pm.PackageManager
|
|
7
9
|
import android.content.res.ColorStateList
|
|
8
10
|
import android.content.res.Configuration
|
|
9
11
|
import android.database.ContentObserver
|
|
@@ -86,6 +88,17 @@ import androidx.javascriptengine.Message
|
|
|
86
88
|
import androidx.javascriptengine.MessagePort
|
|
87
89
|
import androidx.javascriptengine.MessagePortClient
|
|
88
90
|
import com.craft.runtime.CraftNative
|
|
91
|
+
import com.google.android.gms.maps.CameraUpdateFactory
|
|
92
|
+
import com.google.android.gms.maps.GoogleMap
|
|
93
|
+
import com.google.android.gms.maps.MapView
|
|
94
|
+
import com.google.android.gms.maps.OnMapReadyCallback
|
|
95
|
+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
96
|
+
import com.google.android.gms.maps.model.LatLng
|
|
97
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
98
|
+
import com.google.android.gms.maps.model.Marker
|
|
99
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
100
|
+
import com.google.android.gms.maps.model.Polyline
|
|
101
|
+
import com.google.android.gms.maps.model.PolylineOptions
|
|
89
102
|
import org.json.JSONArray
|
|
90
103
|
import org.json.JSONObject
|
|
91
104
|
import java.net.URL
|
|
@@ -459,6 +472,156 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
459
472
|
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
460
473
|
}
|
|
461
474
|
|
|
475
|
+
/** The Android host for the small, host-neutral native trail map contract. */
|
|
476
|
+
private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
477
|
+
private val mapView = MapView(context)
|
|
478
|
+
private var googleMap: GoogleMap? = null
|
|
479
|
+
private var pendingProps = JSONObject()
|
|
480
|
+
private var pendingChildren = JSONArray()
|
|
481
|
+
private val markers = mutableMapOf<String, Marker>()
|
|
482
|
+
private val polylines = mutableMapOf<String, Polyline>()
|
|
483
|
+
private val markerHandlers = mutableMapOf<String, String>()
|
|
484
|
+
private var onMarkerTap: ((String) -> Unit)? = null
|
|
485
|
+
|
|
486
|
+
init {
|
|
487
|
+
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
488
|
+
mapView.onCreate(null)
|
|
489
|
+
mapView.getMapAsync(this)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
override fun onMapReady(map: GoogleMap) {
|
|
493
|
+
googleMap = map
|
|
494
|
+
map.setOnMarkerClickListener { marker ->
|
|
495
|
+
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
496
|
+
onMarkerTap?.invoke(id)
|
|
497
|
+
false
|
|
498
|
+
}
|
|
499
|
+
applyPending()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fun configure(props: JSONObject, children: JSONArray, markerHandler: (String) -> Unit) {
|
|
503
|
+
pendingProps = props
|
|
504
|
+
pendingChildren = children
|
|
505
|
+
onMarkerTap = markerHandler
|
|
506
|
+
applyPending()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
510
|
+
|
|
511
|
+
private fun applyPending() {
|
|
512
|
+
val map = googleMap ?: return
|
|
513
|
+
map.uiSettings.isScrollGesturesEnabled = pendingProps.optBoolean("scrollEnabled", true)
|
|
514
|
+
map.uiSettings.isZoomGesturesEnabled = pendingProps.optBoolean("zoomEnabled", true)
|
|
515
|
+
map.uiSettings.isRotateGesturesEnabled = pendingProps.optBoolean("rotateEnabled", false)
|
|
516
|
+
map.mapType = when (pendingProps.optString("mapType").lowercase()) {
|
|
517
|
+
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
518
|
+
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
519
|
+
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
520
|
+
else -> GoogleMap.MAP_TYPE_NORMAL
|
|
521
|
+
}
|
|
522
|
+
val showUserLocation = pendingProps.optBoolean("showsUserLocation", false) && hasLocationPermission()
|
|
523
|
+
runCatching { map.isMyLocationEnabled = showUserLocation }
|
|
524
|
+
val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
|
|
525
|
+
val nextHandlers = mutableMapOf<String, String>()
|
|
526
|
+
val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
|
|
527
|
+
for (index in 0 until pendingChildren.length()) {
|
|
528
|
+
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
529
|
+
val type = child.optString("type").lowercase()
|
|
530
|
+
val props = child.optJSONObject("props") ?: JSONObject()
|
|
531
|
+
val events = child.optJSONObject("events") ?: JSONObject()
|
|
532
|
+
val id = props.opt("id")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
533
|
+
?: child.optString("id").takeIf { it.isNotBlank() }
|
|
534
|
+
?: "overlay-$index"
|
|
535
|
+
val points = coordinates(props.optJSONArray("geometry"))
|
|
536
|
+
if (type == "mapmarker" || type == "marker") {
|
|
537
|
+
points.firstOrNull()?.let { point ->
|
|
538
|
+
nextMarkers[id] = point to props.optString("title").takeIf { it.isNotBlank() }
|
|
539
|
+
events.optString("onPress").ifBlank { events.optString("onMarkerPress") }
|
|
540
|
+
.takeIf { it.isNotBlank() }?.let { nextHandlers[id] = it }
|
|
541
|
+
}
|
|
542
|
+
} else if (type == "mappolyline" || type == "polyline") {
|
|
543
|
+
if (points.size > 1) {
|
|
544
|
+
val width = props.optDouble("strokeWidth", 3.0).toFloat().takeIf { it.isFinite() && it > 0 } ?: 3f
|
|
545
|
+
nextPolylines[id] = Triple(points, color(props.optString("strokeColor")), width)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
|
|
550
|
+
for ((id, value) in nextMarkers) {
|
|
551
|
+
val marker = markers[id]
|
|
552
|
+
if (marker != null) {
|
|
553
|
+
marker.position = value.first
|
|
554
|
+
marker.title = value.second
|
|
555
|
+
} else {
|
|
556
|
+
map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(BitmapDescriptorFactory.defaultMarker()))?.let { markers[id] = it }
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
markerHandlers.clear()
|
|
560
|
+
markerHandlers.putAll(nextHandlers)
|
|
561
|
+
polylines.values.forEach { it.remove() }
|
|
562
|
+
polylines.clear()
|
|
563
|
+
for ((id, value) in nextPolylines) {
|
|
564
|
+
polylines[id] = map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))
|
|
565
|
+
}
|
|
566
|
+
if (pendingProps.optBoolean("fitToBounds", false)) fitToBounds()
|
|
567
|
+
region(pendingProps.optJSONObject("region") ?: pendingProps.optJSONObject("initialRegion"))?.let { bounds ->
|
|
568
|
+
post {
|
|
569
|
+
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 24))
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private fun fitToBounds() {
|
|
575
|
+
val map = googleMap ?: return
|
|
576
|
+
val builder = LatLngBounds.Builder()
|
|
577
|
+
var count = 0
|
|
578
|
+
markers.values.forEach { builder.include(it.position); count++ }
|
|
579
|
+
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
580
|
+
if (count == 0) return
|
|
581
|
+
post {
|
|
582
|
+
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 24))
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
override fun onAttachedToWindow() {
|
|
587
|
+
super.onAttachedToWindow()
|
|
588
|
+
mapView.onStart()
|
|
589
|
+
mapView.onResume()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
override fun onDetachedFromWindow() {
|
|
593
|
+
mapView.onPause()
|
|
594
|
+
mapView.onStop()
|
|
595
|
+
super.onDetachedFromWindow()
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private fun hasLocationPermission(): Boolean =
|
|
599
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
600
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
601
|
+
|
|
602
|
+
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
603
|
+
if (geometry == null) return emptyList()
|
|
604
|
+
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
605
|
+
val latitude = geometry.optDouble(index, Double.NaN)
|
|
606
|
+
val longitude = geometry.optDouble(index + 1, Double.NaN)
|
|
607
|
+
if (latitude.isFinite() && longitude.isFinite()) LatLng(latitude, longitude) else null
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private fun region(value: JSONObject?): LatLngBounds? {
|
|
612
|
+
val center = value?.optJSONObject("center") ?: return null
|
|
613
|
+
val span = value.optJSONObject("span") ?: return null
|
|
614
|
+
val latitude = center.optDouble("latitude", Double.NaN)
|
|
615
|
+
val longitude = center.optDouble("longitude", Double.NaN)
|
|
616
|
+
val latitudeDelta = span.optDouble("latitudeDelta", span.optDouble("latitude_delta", Double.NaN))
|
|
617
|
+
val longitudeDelta = span.optDouble("longitudeDelta", span.optDouble("longitude_delta", Double.NaN))
|
|
618
|
+
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite()) return null
|
|
619
|
+
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private fun color(value: String): Int = runCatching { Color.parseColor(value) }.getOrDefault(Color.BLUE)
|
|
623
|
+
}
|
|
624
|
+
|
|
462
625
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
463
626
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
464
627
|
var onLayoutChanged: (() -> Unit)? = null
|
|
@@ -474,6 +637,46 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
474
637
|
private var gridAutoRows: CraftNativeLength? = null
|
|
475
638
|
private var reverse = false
|
|
476
639
|
private var wrapReverse = false
|
|
640
|
+
private var authoredPadding = intArrayOf(0, 0, 0, 0)
|
|
641
|
+
private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
|
|
642
|
+
private var applyingPadding = false
|
|
643
|
+
private var safeAreaEdges = setOf("top", "right", "bottom", "left")
|
|
644
|
+
|
|
645
|
+
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
|
646
|
+
if (!applyingPadding) authoredPadding = intArrayOf(left, top, right, bottom)
|
|
647
|
+
applyingPadding = true
|
|
648
|
+
super.setPadding(
|
|
649
|
+
left + safeAreaPadding[0],
|
|
650
|
+
top + safeAreaPadding[1],
|
|
651
|
+
right + safeAreaPadding[2],
|
|
652
|
+
bottom + safeAreaPadding[3],
|
|
653
|
+
)
|
|
654
|
+
applyingPadding = false
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fun setSafeAreaEdges(raw: JSONArray?) {
|
|
658
|
+
safeAreaEdges = if (raw == null) {
|
|
659
|
+
setOf("top", "right", "bottom", "left")
|
|
660
|
+
} else {
|
|
661
|
+
(0 until raw.length()).mapNotNull { raw.optString(it).takeIf { edge -> edge in setOf("top", "right", "bottom", "left") } }.toSet()
|
|
662
|
+
}
|
|
663
|
+
applySafeAreaPadding()
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fun setWindowSafeInsets(left: Int, top: Int, right: Int, bottom: Int) {
|
|
667
|
+
val insets = intArrayOf(left, top, right, bottom)
|
|
668
|
+
safeAreaPadding = intArrayOf(
|
|
669
|
+
if ("left" in safeAreaEdges) insets[0] else 0,
|
|
670
|
+
if ("top" in safeAreaEdges) insets[1] else 0,
|
|
671
|
+
if ("right" in safeAreaEdges) insets[2] else 0,
|
|
672
|
+
if ("bottom" in safeAreaEdges) insets[3] else 0,
|
|
673
|
+
)
|
|
674
|
+
applySafeAreaPadding()
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private fun applySafeAreaPadding() {
|
|
678
|
+
setPadding(authoredPadding[0], authoredPadding[1], authoredPadding[2], authoredPadding[3])
|
|
679
|
+
}
|
|
477
680
|
|
|
478
681
|
/**
|
|
479
682
|
* What a child measures at a pair of specs, remembered for the frame.
|
|
@@ -643,7 +846,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
643
846
|
if (index > 0 && lineIds != null && lineIds[index] != lineIds[index - 1]) {
|
|
644
847
|
lines += line
|
|
645
848
|
line = Line()
|
|
646
|
-
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain) {
|
|
849
|
+
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain + 0.5f * (line.entries.size + 1)) {
|
|
850
|
+
// Each size is rounded to whole pixels, up to half a pixel
|
|
851
|
+
// over: two 60-point items fill a 120-point row exactly
|
|
852
|
+
// in points, and at 2.625 pixels per point (157.5 + 157.5
|
|
853
|
+
// in 315) rounding alone must not wrap the second.
|
|
647
854
|
lines += line
|
|
648
855
|
line = Line()
|
|
649
856
|
}
|
|
@@ -1343,6 +1550,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1343
1550
|
if (handler == null) isRefreshing = false
|
|
1344
1551
|
}
|
|
1345
1552
|
|
|
1553
|
+
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
1554
|
+
val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
|
|
1555
|
+
if (tint != null) setColorSchemeColors(craftNativeColor(tint))
|
|
1556
|
+
else setColorSchemeColors(Color.WHITE)
|
|
1557
|
+
val background = props.optString("progressBackgroundColor").takeIf { it.isNotBlank() }
|
|
1558
|
+
if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
|
|
1559
|
+
else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
|
|
1560
|
+
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
1561
|
+
if (offset.isFinite()) setProgressViewOffset(true, 0, (offset * density).roundToInt())
|
|
1562
|
+
else setProgressViewOffset(false, 0, 0)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1346
1565
|
override fun onDetachedFromWindow() {
|
|
1347
1566
|
refreshHandler = null
|
|
1348
1567
|
isRefreshing = false
|
|
@@ -1783,12 +2002,15 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1783
2002
|
|
|
1784
2003
|
private fun changed() {
|
|
1785
2004
|
if (suppressChanges) return
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
picker.minute
|
|
1791
|
-
|
|
2005
|
+
// A date-only picker has no time half to snap, but its change is
|
|
2006
|
+
// still the screen's to hear.
|
|
2007
|
+
timePicker?.let { picker ->
|
|
2008
|
+
val snapped = ((picker.minute + minuteInterval / 2) / minuteInterval * minuteInterval).coerceAtMost(59)
|
|
2009
|
+
if (snapped != picker.minute) {
|
|
2010
|
+
suppressChanges = true
|
|
2011
|
+
picker.minute = snapped
|
|
2012
|
+
suppressChanges = false
|
|
2013
|
+
}
|
|
1792
2014
|
}
|
|
1793
2015
|
onValueChanged?.invoke()
|
|
1794
2016
|
}
|
|
@@ -2182,6 +2404,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2182
2404
|
) {
|
|
2183
2405
|
/** What the screen set with `craft.navigation.setOptions`, merged. */
|
|
2184
2406
|
var options = JSONObject()
|
|
2407
|
+
/** Overrides from a native `<StatusBar>` node, reset on each render. */
|
|
2408
|
+
var statusBarStyle: String? = null
|
|
2409
|
+
var statusBarHidden: Boolean? = null
|
|
2185
2410
|
/** Shown before: its next showing is a `SCREEN_FOCUS`. */
|
|
2186
2411
|
var shown = false
|
|
2187
2412
|
/** The app path it was opened for (`/m/workout/42`), to find it again. */
|
|
@@ -2209,6 +2434,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2209
2434
|
val nodeViews = mutableMapOf<String, View>()
|
|
2210
2435
|
val nodePaths = mutableMapOf<String, String>()
|
|
2211
2436
|
val handlers = mutableMapOf<View, String>()
|
|
2437
|
+
val linkTargets = mutableMapOf<View, String>()
|
|
2212
2438
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
2213
2439
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
2214
2440
|
val longPressHandlers = mutableMapOf<View, String>()
|
|
@@ -2279,6 +2505,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2279
2505
|
private val tabStacks = mutableListOf<MutableList<Screen>>()
|
|
2280
2506
|
private var currentTab = 0
|
|
2281
2507
|
private var visibleScreen: Screen? = null
|
|
2508
|
+
private var tabSwipeTarget: Screen? = null
|
|
2509
|
+
private var tabSwipeTargetIndex = -1
|
|
2510
|
+
private var tabSwipeDirection = 0
|
|
2282
2511
|
/** The app script's isolate (`nativeApp`), when the app has one. */
|
|
2283
2512
|
private var appScreen: Screen? = null
|
|
2284
2513
|
private val urlSchemes = mutableSetOf<String>()
|
|
@@ -2298,6 +2527,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2298
2527
|
private var initialDeepLink: Uri? = null
|
|
2299
2528
|
private var initialDeepLinkClaimed = false
|
|
2300
2529
|
private var defaultTextColor = Color.WHITE
|
|
2530
|
+
private var windowSafeInsets = intArrayOf(0, 0, 0, 0)
|
|
2301
2531
|
private var useMessagePorts = false
|
|
2302
2532
|
private val imageExecutor = Executors.newFixedThreadPool(2)
|
|
2303
2533
|
/** Lays out each isolate's prelude (the synchronous stores) off the main thread. */
|
|
@@ -2330,8 +2560,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2330
2560
|
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
2331
2561
|
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
2332
2562
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
2563
|
+
windowSafeInsets = intArrayOf(safe.left, safe.top, safe.right, safe.bottom)
|
|
2333
2564
|
keyboardInset = max(0, ime.bottom - safe.bottom)
|
|
2334
|
-
|
|
2565
|
+
updateSafeAreaViews()
|
|
2335
2566
|
updateKeyboardAvoidingViews()
|
|
2336
2567
|
view.requestLayout()
|
|
2337
2568
|
view.post { screens.lastOrNull()?.takeIf { it.active }?.let(::emitLayoutEvents) }
|
|
@@ -2443,6 +2674,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2443
2674
|
haptic()
|
|
2444
2675
|
if (index == currentTab) popToRoot() else selectTab(index)
|
|
2445
2676
|
}
|
|
2677
|
+
bars.onTabSwipeStart = { dx, x, y -> beginTabSwipe(dx, x, y) }
|
|
2678
|
+
bars.onTabSwipeMove = { dx -> updateTabSwipe(dx) }
|
|
2679
|
+
bars.onTabSwipeEnd = { dx, cancelled -> finishTabSwipe(dx, cancelled) }
|
|
2446
2680
|
root.addView(bars)
|
|
2447
2681
|
chrome = bars
|
|
2448
2682
|
tabStacks.clear()
|
|
@@ -2466,12 +2700,97 @@ class MainActivity : AppCompatActivity() {
|
|
|
2466
2700
|
currentTab = index
|
|
2467
2701
|
screens = tabStacks[index]
|
|
2468
2702
|
chrome?.selectTab(index)
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2703
|
+
ensureTabRoot(index)?.let(::show)
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/** Mount a tab's root without stealing focus, for an interactive swipe. */
|
|
2707
|
+
private fun ensureTabRoot(index: Int): Screen? {
|
|
2708
|
+
if (index !in tabStacks.indices) return null
|
|
2709
|
+
tabStacks[index].lastOrNull()?.let { return it }
|
|
2710
|
+
val previous = screens
|
|
2711
|
+
screens = tabStacks[index]
|
|
2712
|
+
val match = routes.tabs.getOrNull(index)?.let { routes.match(it.id) }
|
|
2713
|
+
val screen = if (match != null) openScreen(match.screen, match.params, showImmediately = false) else openScreen(null, showImmediately = false)
|
|
2714
|
+
screen?.path = match?.path
|
|
2715
|
+
screens = previous
|
|
2716
|
+
return screen
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
/** Start an interactive horizontal transition to the adjacent tab. */
|
|
2720
|
+
private fun beginTabSwipe(dx: Float, x: Float, y: Float): Boolean {
|
|
2721
|
+
if (tabSwipeTarget != null || screens.size != 1 || chrome?.tabSwipeEnabled != true) return false
|
|
2722
|
+
val direction = if (dx < 0) 1 else -1
|
|
2723
|
+
val targetIndex = currentTab + direction
|
|
2724
|
+
val current = visibleScreen ?: return false
|
|
2725
|
+
val target = ensureTabRoot(targetIndex) ?: return false
|
|
2726
|
+
val host = chrome?.content ?: return false
|
|
2727
|
+
if (target === current || host.width <= 0) return false
|
|
2728
|
+
if (horizontalScrollCanConsume(current.view, host, x, y, dx)) return false
|
|
2729
|
+
(target.view.parent as? ViewGroup)?.removeView(target.view)
|
|
2730
|
+
host.addView(target.view, FrameLayout.LayoutParams(-1, -1))
|
|
2731
|
+
target.view.translationX = if (direction > 0) host.width.toFloat() else -host.width.toFloat()
|
|
2732
|
+
current.view.translationX = 0f
|
|
2733
|
+
host.bringChildToFront(target.view)
|
|
2734
|
+
tabSwipeTarget = target
|
|
2735
|
+
tabSwipeTargetIndex = targetIndex
|
|
2736
|
+
tabSwipeDirection = direction
|
|
2737
|
+
chrome?.selectTab(targetIndex)
|
|
2738
|
+
return true
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
private fun updateTabSwipe(dx: Float) {
|
|
2742
|
+
val current = visibleScreen ?: return
|
|
2743
|
+
val target = tabSwipeTarget ?: return
|
|
2744
|
+
val width = (chrome?.content?.width ?: return).toFloat().coerceAtLeast(1f)
|
|
2745
|
+
val offset = dx.coerceIn(-width, width)
|
|
2746
|
+
target.view.translationX = (if (tabSwipeDirection > 0) width else -width) + offset
|
|
2747
|
+
current.view.translationX = offset
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
private fun finishTabSwipe(dx: Float, cancelled: Boolean) {
|
|
2751
|
+
val current = visibleScreen ?: return
|
|
2752
|
+
val target = tabSwipeTarget ?: return
|
|
2753
|
+
val host = chrome?.content ?: return
|
|
2754
|
+
val width = host.width.toFloat().coerceAtLeast(1f)
|
|
2755
|
+
val complete = !cancelled && kotlin.math.abs(dx) >= width * 0.28f
|
|
2756
|
+
val endOffset = if (complete) {
|
|
2757
|
+
if (tabSwipeDirection > 0) -width else width
|
|
2758
|
+
} else 0f
|
|
2759
|
+
val targetOffset = if (complete) 0f else if (tabSwipeDirection > 0) width else -width
|
|
2760
|
+
current.view.animate().translationX(endOffset).setDuration(180).start()
|
|
2761
|
+
target.view.animate().translationX(targetOffset).setDuration(180).withEndAction {
|
|
2762
|
+
target.view.animate().setListener(null)
|
|
2763
|
+
if (complete) {
|
|
2764
|
+
current.view.translationX = 0f
|
|
2765
|
+
currentTab = tabSwipeTargetIndex
|
|
2766
|
+
screens = tabStacks[currentTab]
|
|
2767
|
+
tabSwipeTarget = null
|
|
2768
|
+
tabSwipeTargetIndex = -1
|
|
2769
|
+
tabSwipeDirection = 0
|
|
2770
|
+
haptic()
|
|
2771
|
+
show(target)
|
|
2772
|
+
} else {
|
|
2773
|
+
host.removeView(target.view)
|
|
2774
|
+
current.view.translationX = 0f
|
|
2775
|
+
chrome?.selectTab(currentTab)
|
|
2776
|
+
tabSwipeTarget = null
|
|
2777
|
+
tabSwipeTargetIndex = -1
|
|
2778
|
+
tabSwipeDirection = 0
|
|
2779
|
+
}
|
|
2780
|
+
}.start()
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
private fun horizontalScrollCanConsume(view: View, host: ViewGroup, x: Float, y: Float, dx: Float): Boolean {
|
|
2784
|
+
val localX = x - view.left
|
|
2785
|
+
val localY = y - host.top - view.top
|
|
2786
|
+
if (view !is ViewGroup || localX < 0 || localY < 0 || localX > view.width || localY > view.height) return false
|
|
2787
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
2788
|
+
val child = view.getChildAt(index)
|
|
2789
|
+
if (!child.isShown || localX < child.left || localX > child.right || localY < child.top || localY > child.bottom) continue
|
|
2790
|
+
if (child is android.widget.HorizontalScrollView && child.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
2791
|
+
if (horizontalScrollCanConsume(child, host, localX - child.left, localY - child.top, dx)) return true
|
|
2474
2792
|
}
|
|
2793
|
+
return false
|
|
2475
2794
|
}
|
|
2476
2795
|
|
|
2477
2796
|
/**
|
|
@@ -2523,7 +2842,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2523
2842
|
services.perform("Browser", "open", JSONArray().put(url), {})
|
|
2524
2843
|
}
|
|
2525
2844
|
|
|
2526
|
-
private fun openScreen(route: String?, params: JSONObject = JSONObject(), headless: Boolean = false): Screen? {
|
|
2845
|
+
private fun openScreen(route: String?, params: JSONObject = JSONObject(), headless: Boolean = false, showImmediately: Boolean = true): Screen? {
|
|
2527
2846
|
val engine = sandbox ?: return null
|
|
2528
2847
|
val isolate = engine.createIsolate()
|
|
2529
2848
|
val view = CraftNativeFlexLayout(this).apply {
|
|
@@ -2560,7 +2879,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2560
2879
|
}
|
|
2561
2880
|
if (!headless) {
|
|
2562
2881
|
screens.add(activeScreen)
|
|
2563
|
-
show(activeScreen)
|
|
2882
|
+
if (showImmediately) show(activeScreen)
|
|
2564
2883
|
}
|
|
2565
2884
|
val routePrelude = """
|
|
2566
2885
|
globalThis.__stxNativeRoute = ${route?.let { JSONObject.quote(it) } ?: "undefined"};
|
|
@@ -2844,6 +3163,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
2844
3163
|
else -> null
|
|
2845
3164
|
}
|
|
2846
3165
|
}.forEach { it.refreshThemeDefaults() }
|
|
3166
|
+
updateSafeAreaViews()
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
private fun updateSafeAreaViews() {
|
|
3170
|
+
if (!::root.isInitialized) return
|
|
3171
|
+
val active = visibleScreen ?: screens.lastOrNull { it.active }
|
|
3172
|
+
val rootNode = active?.mutations?.node("root")
|
|
3173
|
+
val safeRoot = rootNode?.let { canonicalType(it.optString("type")) == "SafeAreaView" } == true
|
|
3174
|
+
root.setPadding(
|
|
3175
|
+
if (safeRoot) 0 else windowSafeInsets[0],
|
|
3176
|
+
if (safeRoot) 0 else windowSafeInsets[1],
|
|
3177
|
+
if (safeRoot) 0 else windowSafeInsets[2],
|
|
3178
|
+
if (safeRoot) 0 else windowSafeInsets[3],
|
|
3179
|
+
)
|
|
3180
|
+
val tree = active?.view?.getChildAt(0) as? CraftNativeFlexLayout
|
|
3181
|
+
if (safeRoot) tree?.setWindowSafeInsets(windowSafeInsets[0], windowSafeInsets[1], windowSafeInsets[2], windowSafeInsets[3])
|
|
3182
|
+
else tree?.setWindowSafeInsets(0, 0, 0, 0)
|
|
2847
3183
|
}
|
|
2848
3184
|
|
|
2849
3185
|
private fun refreshThemeDefaults() {
|
|
@@ -3061,6 +3397,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3061
3397
|
|
|
3062
3398
|
private fun renderCommitted(screen: Screen, document: JSONObject) {
|
|
3063
3399
|
// Keep live controls (especially the focused EditText) across renders.
|
|
3400
|
+
screen.statusBarStyle = null
|
|
3401
|
+
screen.statusBarHidden = null
|
|
3064
3402
|
val next = mutableMapOf<String, View>()
|
|
3065
3403
|
screen.nodeViews.clear()
|
|
3066
3404
|
screen.nodePaths.clear()
|
|
@@ -3075,10 +3413,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
3075
3413
|
screen.controls.putAll(next)
|
|
3076
3414
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
3077
3415
|
screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
3416
|
+
updateSafeAreaViews()
|
|
3078
3417
|
// A full render can change a view's custom layout style without
|
|
3079
3418
|
// changing the root bounds. Ensure native @layout observers still see
|
|
3080
3419
|
// the settled child frame after the next traversal.
|
|
3081
3420
|
scheduleLayoutEvents(screen)
|
|
3421
|
+
if (visibleScreen === screen) applyStatusBar(screen)
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
private fun applyStatusBar(screen: Screen) {
|
|
3425
|
+
val controller = WindowCompat.getInsetsController(window, window.decorView)
|
|
3426
|
+
val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
3427
|
+
controller.isAppearanceLightStatusBars = when (screen.statusBarStyle?.lowercase()) {
|
|
3428
|
+
"light-content", "light" -> false
|
|
3429
|
+
"dark-content", "dark" -> true
|
|
3430
|
+
else -> !night
|
|
3431
|
+
}
|
|
3432
|
+
if (screen.statusBarHidden == true) controller.hide(WindowInsetsCompat.Type.statusBars())
|
|
3433
|
+
else controller.show(WindowInsetsCompat.Type.statusBars())
|
|
3082
3434
|
}
|
|
3083
3435
|
|
|
3084
3436
|
private fun clearRenderedTree(screen: Screen) {
|
|
@@ -3088,6 +3440,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3088
3440
|
screen.nodeViews.clear()
|
|
3089
3441
|
screen.nodePaths.clear()
|
|
3090
3442
|
screen.handlers.clear()
|
|
3443
|
+
screen.linkTargets.clear()
|
|
3091
3444
|
screen.flatListRows.clear()
|
|
3092
3445
|
screen.flatListOwners.clear()
|
|
3093
3446
|
screen.flatListOwnerByView.clear()
|
|
@@ -3124,6 +3477,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3124
3477
|
}
|
|
3125
3478
|
configureText(this, childText(children), style)
|
|
3126
3479
|
}
|
|
3480
|
+
"StatusBar" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3481
|
+
screen.statusBarStyle = props.optString("barStyle").takeIf { it.isNotBlank() }
|
|
3482
|
+
screen.statusBarHidden = if (props.has("hidden")) props.optBoolean("hidden") else null
|
|
3483
|
+
visibility = View.GONE
|
|
3484
|
+
}
|
|
3485
|
+
"RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3486
|
+
visibility = View.GONE
|
|
3487
|
+
}
|
|
3127
3488
|
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
3128
3489
|
includeFontPadding = false
|
|
3129
3490
|
configureText(this, props.optString("title").ifBlank { childText(children) }, style)
|
|
@@ -3133,9 +3494,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
3133
3494
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3134
3495
|
isClickable = true
|
|
3135
3496
|
isFocusable = true
|
|
3497
|
+
val target = listOf(props.optString("href"), props.optString("to"))
|
|
3498
|
+
.firstOrNull { it.isNotBlank() }
|
|
3499
|
+
?.trim()
|
|
3500
|
+
?.takeIf { !props.optBoolean("disabled", false) }
|
|
3501
|
+
if (target != null) screen.linkTargets[this] = target else screen.linkTargets.remove(this)
|
|
3136
3502
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
3137
3503
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3138
3504
|
setOnClickListener {
|
|
3505
|
+
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3139
3506
|
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
3140
3507
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3141
3508
|
}
|
|
@@ -3540,7 +3907,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3540
3907
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
3541
3908
|
val onError = events.optString("onError")
|
|
3542
3909
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
3543
|
-
updateImage(screen, this, props.opt("source"))
|
|
3910
|
+
updateImage(screen, this, props.opt("source"), props.opt("defaultSource"))
|
|
3544
3911
|
}
|
|
3545
3912
|
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
3546
3913
|
image.scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
@@ -3557,7 +3924,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3557
3924
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
3558
3925
|
val onError = events.optString("onError")
|
|
3559
3926
|
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
3560
|
-
updateImage(screen, image, props.opt("source"))
|
|
3927
|
+
updateImage(screen, image, props.opt("source"), props.opt("defaultSource"))
|
|
3561
3928
|
configureContainer(content, style)
|
|
3562
3929
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
3563
3930
|
}
|
|
@@ -3571,6 +3938,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3571
3938
|
controls = props.optBoolean("controls", false),
|
|
3572
3939
|
)
|
|
3573
3940
|
}
|
|
3941
|
+
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
3942
|
+
configure(props, children) { markerId ->
|
|
3943
|
+
markerHandler(markerId)?.let { handler ->
|
|
3944
|
+
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("id", markerId)))
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3574
3948
|
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
3575
3949
|
drawing = node
|
|
3576
3950
|
}
|
|
@@ -3590,10 +3964,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
3590
3964
|
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
3591
3965
|
)
|
|
3592
3966
|
setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
3593
|
-
val
|
|
3967
|
+
val refreshNode = (0 until children.length()).asSequence()
|
|
3968
|
+
.mapNotNull { children.optJSONObject(it) }
|
|
3969
|
+
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
3970
|
+
val refreshProps = refreshNode?.optJSONObject("props") ?: JSONObject()
|
|
3971
|
+
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
3972
|
+
val refreshHandlerName = refreshEvents.optString("onRefresh").takeIf { it.isNotBlank() }
|
|
3973
|
+
?: events.optString("onRefresh").takeIf { it.isNotBlank() }
|
|
3974
|
+
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
3594
3975
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
3595
3976
|
}
|
|
3596
|
-
setRefreshHandler(refreshHandler, props.optBoolean("refreshing", false))
|
|
3977
|
+
setRefreshHandler(refreshHandler, if (refreshNode == null) props.optBoolean("refreshing", false) else refreshProps.optBoolean("refreshing", false))
|
|
3978
|
+
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
3597
3979
|
val onScroll = events.optString("onScroll")
|
|
3598
3980
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
3599
3981
|
val onBegin = events.optString("onScrollBeginDrag")
|
|
@@ -3647,7 +4029,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
3647
4029
|
}
|
|
3648
4030
|
}
|
|
3649
4031
|
configureContainer(content, contentStyle)
|
|
3650
|
-
|
|
4032
|
+
val contentChildren = JSONArray()
|
|
4033
|
+
for (index in 0 until children.length()) {
|
|
4034
|
+
val child = children.opt(index)
|
|
4035
|
+
if (child !is JSONObject || canonicalType(child.optString("type")) != "RefreshControl") contentChildren.put(child)
|
|
4036
|
+
}
|
|
4037
|
+
reconcileChildren(screen, content, contentChildren, path, next, contentStyle, previousControls)
|
|
3651
4038
|
}
|
|
3652
4039
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
3653
4040
|
val nativeList = list
|
|
@@ -3665,6 +4052,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3665
4052
|
}
|
|
3666
4053
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3667
4054
|
configureContainer(this, style)
|
|
4055
|
+
if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
|
|
3668
4056
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
3669
4057
|
}
|
|
3670
4058
|
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
@@ -3686,15 +4074,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
3686
4074
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
3687
4075
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
3688
4076
|
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") {
|
|
4077
|
+
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
4078
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
3691
4079
|
val disabled = props.optBoolean("disabled", false)
|
|
4080
|
+
val target = listOf(props.optString("href"), props.optString("to"))
|
|
4081
|
+
.firstOrNull { it.isNotBlank() }
|
|
4082
|
+
?.trim()
|
|
4083
|
+
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
4084
|
+
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
3692
4085
|
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null
|
|
3693
|
-
if ((handler.isNotBlank() || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
4086
|
+
if ((handler.isNotBlank() || target != null || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
3694
4087
|
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
3695
4088
|
control.isClickable = true
|
|
3696
4089
|
if (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable")) {
|
|
3697
4090
|
control.setOnClickListener {
|
|
4091
|
+
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3698
4092
|
val current = screen.handlers[it] ?: return@setOnClickListener
|
|
3699
4093
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3700
4094
|
}
|
|
@@ -3715,6 +4109,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3715
4109
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3716
4110
|
}
|
|
3717
4111
|
if (event.actionMasked == android.view.MotionEvent.ACTION_UP) {
|
|
4112
|
+
screen.linkTargets[view]?.let { target -> navigateOpen(target); return@setOnTouchListener true }
|
|
3718
4113
|
val current = screen.handlers[view] ?: return@setOnTouchListener true
|
|
3719
4114
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3720
4115
|
}
|
|
@@ -3726,6 +4121,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3726
4121
|
} else {
|
|
3727
4122
|
control.setOnTouchListener(null)
|
|
3728
4123
|
control.setOnClickListener {
|
|
4124
|
+
screen.linkTargets[control]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
3729
4125
|
val current = screen.handlers[control] ?: return@setOnClickListener
|
|
3730
4126
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
3731
4127
|
}
|
|
@@ -3833,10 +4229,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3833
4229
|
"switch" -> "Switch"
|
|
3834
4230
|
"slider" -> "Slider"
|
|
3835
4231
|
"activityindicator" -> "ActivityIndicator"
|
|
4232
|
+
"statusbar" -> "StatusBar"
|
|
4233
|
+
"refreshcontrol" -> "RefreshControl"
|
|
3836
4234
|
"image" -> "Image"
|
|
3837
4235
|
"imagebackground" -> "ImageBackground"
|
|
3838
4236
|
"keyboardavoidingview" -> "KeyboardAvoidingView"
|
|
3839
4237
|
"video" -> "Video"
|
|
4238
|
+
"map" -> "Map"
|
|
3840
4239
|
"icon" -> "Icon"
|
|
3841
4240
|
"scrollview" -> "ScrollView"
|
|
3842
4241
|
"sectionlist" -> "SectionList"
|
|
@@ -4274,6 +4673,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
4274
4673
|
|
|
4275
4674
|
private fun applyTextDecoration(view: TextView, style: JSONObject) {
|
|
4276
4675
|
val line = style.optString("textDecorationLine")
|
|
4676
|
+
if (view is EditText) {
|
|
4677
|
+
// A field's own text, spanned in place: replacing it would move
|
|
4678
|
+
// the caret to the start and tell the screen the text changed.
|
|
4679
|
+
val editable = view.text ?: return
|
|
4680
|
+
editable.getSpans(0, editable.length, CraftTextDecorationSpan::class.java).forEach(editable::removeSpan)
|
|
4681
|
+
if (line.isBlank() || line == "none" || editable.isEmpty()) return
|
|
4682
|
+
val fieldColor = style.optString("textDecorationColor").takeIf { it.isNotBlank() }?.let { color(it, Color.TRANSPARENT) }
|
|
4683
|
+
editable.setSpan(
|
|
4684
|
+
CraftTextDecorationSpan(line, style.optString("textDecorationStyle").ifBlank { "solid" }, fieldColor),
|
|
4685
|
+
0,
|
|
4686
|
+
editable.length,
|
|
4687
|
+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
4688
|
+
)
|
|
4689
|
+
return
|
|
4690
|
+
}
|
|
4277
4691
|
val text = view.text?.toString() ?: return
|
|
4278
4692
|
if (line.isBlank() || line == "none" || text.isEmpty()) {
|
|
4279
4693
|
if (view.text is Spannable) view.text = text
|
|
@@ -4311,12 +4725,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
4311
4725
|
host == "::1" || host.endsWith(".localhost")
|
|
4312
4726
|
}
|
|
4313
4727
|
|
|
4314
|
-
private fun
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4728
|
+
private fun imageUri(source: Any?): String = when (source) {
|
|
4729
|
+
is String -> source
|
|
4730
|
+
is JSONObject -> source.optString("uri")
|
|
4731
|
+
else -> ""
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
private fun immediateImage(uri: String): Bitmap? {
|
|
4735
|
+
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
4736
|
+
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
4737
|
+
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
4319
4738
|
}
|
|
4739
|
+
if (!uri.contains(':')) return runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
4740
|
+
return null
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
private fun updateImage(screen: Screen, view: ImageView, source: Any?, defaultSource: Any? = null) {
|
|
4744
|
+
val uri = imageUri(source)
|
|
4320
4745
|
if (uri.isBlank()) {
|
|
4321
4746
|
if (imageSources[view] == "") return
|
|
4322
4747
|
imageJobs.remove(view)?.cancel(true)
|
|
@@ -4331,6 +4756,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4331
4756
|
imageSources[view] = uri
|
|
4332
4757
|
imageErrors.remove(view)
|
|
4333
4758
|
view.setImageDrawable(null)
|
|
4759
|
+
immediateImage(imageUri(defaultSource))?.let(view::setImageBitmap)
|
|
4334
4760
|
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
4335
4761
|
|
|
4336
4762
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
@@ -4847,6 +5273,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4847
5273
|
if (!preservingInputDrafts) identity?.let(screen.drafts::remove)
|
|
4848
5274
|
}
|
|
4849
5275
|
released.forEach(defaultBackgrounds::remove)
|
|
5276
|
+
screen.linkTargets.keys.removeAll(released)
|
|
4850
5277
|
screen.handlers.keys.removeAll(released)
|
|
4851
5278
|
screen.pressInHandlers.keys.removeAll(released)
|
|
4852
5279
|
screen.pressOutHandlers.keys.removeAll(released)
|