craft-native 0.0.128 → 0.0.133
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/templates/CraftNativeCapabilities.kt.template +103 -1
- package/dist/android/templates/CraftNativeMutation.kt.template +12 -3
- package/dist/android/templates/MainActivityNative.kt.template +262 -57
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +51 -7
- package/dist/ios/templates/CraftHybrid.swift +3 -6
- package/dist/ios/templates/CraftNativeActions.swift +5 -1
- package/dist/ios/templates/CraftNativeMutation.swift +10 -3
- package/dist/ios/templates/CraftNativeScreen.swift +178 -11
- package/package.json +2 -2
|
@@ -48,6 +48,7 @@ import android.widget.Switch
|
|
|
48
48
|
import android.widget.TextView
|
|
49
49
|
import androidx.appcompat.app.AppCompatActivity
|
|
50
50
|
import androidx.appcompat.app.AppCompatDelegate
|
|
51
|
+
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
51
52
|
import androidx.core.content.ContextCompat
|
|
52
53
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
53
54
|
import androidx.core.view.WindowInsetsCompat
|
|
@@ -72,13 +73,31 @@ import kotlin.math.roundToInt
|
|
|
72
73
|
|
|
73
74
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
74
75
|
|
|
76
|
+
private data class CraftNativeLength(
|
|
77
|
+
val points: Int? = null,
|
|
78
|
+
val fraction: Double? = null,
|
|
79
|
+
) {
|
|
80
|
+
fun resolve(base: Int?): Int? = points ?: fraction?.let { value -> base?.let { max(0, (it * value).roundToInt()) } }
|
|
81
|
+
|
|
82
|
+
companion object {
|
|
83
|
+
fun from(value: Any?, density: Float): CraftNativeLength? {
|
|
84
|
+
if (value == null || value == JSONObject.NULL) return null
|
|
85
|
+
if (value is Number) return CraftNativeLength(points = (value.toDouble() * density).roundToInt())
|
|
86
|
+
val text = value.toString().trim()
|
|
87
|
+
if (text.endsWith('%')) return text.dropLast(1).toDoubleOrNull()?.let { CraftNativeLength(fraction = it / 100) }
|
|
88
|
+
if (text.endsWith("px")) return text.dropLast(2).toDoubleOrNull()?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
89
|
+
return text.toDoubleOrNull()?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
private data class CraftNativeLayoutStyle(
|
|
76
|
-
val width:
|
|
77
|
-
val height:
|
|
78
|
-
val minWidth:
|
|
79
|
-
val maxWidth:
|
|
80
|
-
val minHeight:
|
|
81
|
-
val maxHeight:
|
|
95
|
+
val width: CraftNativeLength? = null,
|
|
96
|
+
val height: CraftNativeLength? = null,
|
|
97
|
+
val minWidth: CraftNativeLength? = null,
|
|
98
|
+
val maxWidth: CraftNativeLength? = null,
|
|
99
|
+
val minHeight: CraftNativeLength? = null,
|
|
100
|
+
val maxHeight: CraftNativeLength? = null,
|
|
82
101
|
val position: String = "relative",
|
|
83
102
|
val top: Int? = null,
|
|
84
103
|
val right: Int? = null,
|
|
@@ -86,19 +105,25 @@ private data class CraftNativeLayoutStyle(
|
|
|
86
105
|
val left: Int? = null,
|
|
87
106
|
val alignSelf: String? = null,
|
|
88
107
|
val flexGrow: Double = 0.0,
|
|
89
|
-
val flexShrink: Double =
|
|
90
|
-
val flexBasis:
|
|
108
|
+
val flexShrink: Double? = null,
|
|
109
|
+
val flexBasis: CraftNativeLength? = null,
|
|
91
110
|
) {
|
|
92
111
|
companion object {
|
|
93
112
|
fun from(raw: JSONObject, density: Float): CraftNativeLayoutStyle {
|
|
113
|
+
fun length(name: String): CraftNativeLength? = CraftNativeLength.from(raw.opt(name), density)
|
|
94
114
|
fun number(name: String): Int? {
|
|
95
115
|
if (!raw.has(name) || raw.isNull(name)) return null
|
|
96
|
-
return raw.optDouble(name, Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).
|
|
116
|
+
return raw.optDouble(name, Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).roundToInt() }
|
|
97
117
|
}
|
|
118
|
+
fun decimal(name: String): Double? {
|
|
119
|
+
if (!raw.has(name) || raw.isNull(name)) return null
|
|
120
|
+
return raw.optDouble(name, Double.NaN).takeUnless { it.isNaN() }
|
|
121
|
+
}
|
|
122
|
+
val flex = decimal("flex")
|
|
98
123
|
return CraftNativeLayoutStyle(
|
|
99
|
-
width =
|
|
100
|
-
minWidth =
|
|
101
|
-
minHeight =
|
|
124
|
+
width = length("width"), height = length("height"),
|
|
125
|
+
minWidth = length("minWidth"), maxWidth = length("maxWidth"),
|
|
126
|
+
minHeight = length("minHeight"), maxHeight = length("maxHeight"),
|
|
102
127
|
position = raw.opt("position")
|
|
103
128
|
?.takeUnless { it == JSONObject.NULL }
|
|
104
129
|
?.toString()
|
|
@@ -108,10 +133,11 @@ private data class CraftNativeLayoutStyle(
|
|
|
108
133
|
alignSelf = raw.opt("alignSelf")
|
|
109
134
|
?.takeUnless { it == JSONObject.NULL }
|
|
110
135
|
?.toString()
|
|
111
|
-
?.takeIf { it.isNotBlank() },
|
|
112
|
-
flexGrow =
|
|
113
|
-
flexShrink =
|
|
114
|
-
flexBasis =
|
|
136
|
+
?.takeIf { it.isNotBlank() && it != "auto" },
|
|
137
|
+
flexGrow = decimal("flexGrow") ?: flex?.coerceAtLeast(0.0) ?: 0.0,
|
|
138
|
+
flexShrink = decimal("flexShrink") ?: flex?.let { if (it != 0.0) 1.0 else 0.0 },
|
|
139
|
+
flexBasis = length("flexBasis")
|
|
140
|
+
?: flex?.takeIf { it > 0.0 }?.let { CraftNativeLength(points = 0) },
|
|
115
141
|
)
|
|
116
142
|
}
|
|
117
143
|
}
|
|
@@ -134,6 +160,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
134
160
|
private val styles = mutableMapOf<View, CraftNativeLayoutStyle>()
|
|
135
161
|
private var wrap = false
|
|
136
162
|
private var alignItems = "stretch"
|
|
163
|
+
private var alignContent = "flex-start"
|
|
137
164
|
private var justifyContent = "flex-start"
|
|
138
165
|
private var rowGap = 0
|
|
139
166
|
private var columnGap = 0
|
|
@@ -145,6 +172,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
145
172
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
146
173
|
wrap = style.optString("flexWrap") == "wrap" || style.optBoolean("flexWrap", false)
|
|
147
174
|
alignItems = style.optString("alignItems", "stretch")
|
|
175
|
+
alignContent = style.optString("alignContent", "flex-start")
|
|
148
176
|
justifyContent = style.optString("justifyContent", "flex-start")
|
|
149
177
|
val gap = (style.optDouble("gap", 0.0) * density).toInt()
|
|
150
178
|
rowGap = (style.optDouble("rowGap", gap.toDouble() / density.toDouble()) * density).toInt()
|
|
@@ -294,25 +322,30 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
294
322
|
}
|
|
295
323
|
|
|
296
324
|
private fun measureChildWidth(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
297
|
-
val
|
|
298
|
-
val
|
|
299
|
-
|
|
300
|
-
|
|
325
|
+
val base = max(0, available - margins)
|
|
326
|
+
val flexBasis = if (orientation == HORIZONTAL) style.flexBasis?.resolve(base) else null
|
|
327
|
+
// The STX style is authoritative. A reused View may still carry a
|
|
328
|
+
// previous fixed LayoutParams width after a reactive width is removed;
|
|
329
|
+
// using that value here prevents intrinsic sizing from being restored.
|
|
330
|
+
val explicit = style.width?.resolve(base) ?: flexBasis
|
|
331
|
+
val target = explicit ?: base
|
|
301
332
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
302
|
-
else View.MeasureSpec.makeMeasureSpec(
|
|
333
|
+
else View.MeasureSpec.makeMeasureSpec(base, View.MeasureSpec.AT_MOST)
|
|
303
334
|
child.measure(spec, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
|
|
304
|
-
return clamp(if (explicit != null) target else child.measuredWidth, style.minWidth, style.maxWidth)
|
|
335
|
+
return clamp(if (explicit != null) target else child.measuredWidth, style.minWidth?.resolve(base), style.maxWidth?.resolve(base))
|
|
305
336
|
}
|
|
306
337
|
|
|
307
338
|
private fun measureChildHeight(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
308
|
-
val
|
|
309
|
-
val
|
|
310
|
-
|
|
311
|
-
|
|
339
|
+
val base = max(0, available - margins)
|
|
340
|
+
val flexBasis = if (orientation == VERTICAL) style.flexBasis?.resolve(base) else null
|
|
341
|
+
// Match width measurement: do not let stale native LayoutParams keep
|
|
342
|
+
// a removed reactive height fixed.
|
|
343
|
+
val explicit = style.height?.resolve(base) ?: flexBasis
|
|
344
|
+
val target = explicit ?: base
|
|
312
345
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
313
|
-
else View.MeasureSpec.makeMeasureSpec(
|
|
346
|
+
else View.MeasureSpec.makeMeasureSpec(base, View.MeasureSpec.AT_MOST)
|
|
314
347
|
child.measure(child.measuredWidth.let { View.MeasureSpec.makeMeasureSpec(max(0, it), View.MeasureSpec.EXACTLY) }, spec)
|
|
315
|
-
return clamp(if (explicit != null) target else child.measuredHeight, style.minHeight, style.maxHeight)
|
|
348
|
+
return clamp(if (explicit != null) target else child.measuredHeight, style.minHeight?.resolve(base), style.maxHeight?.resolve(base))
|
|
316
349
|
}
|
|
317
350
|
|
|
318
351
|
private fun clamp(value: Int, lower: Int?, upper: Int?): Int {
|
|
@@ -322,6 +355,12 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
322
355
|
return result
|
|
323
356
|
}
|
|
324
357
|
|
|
358
|
+
private fun defaultFlexShrink(view: View): Double = when {
|
|
359
|
+
view is ScrollView || view is HorizontalScrollView -> 1.0
|
|
360
|
+
view is TextView && view !is Button && view !is EditText && orientation == HORIZONTAL -> 1.0
|
|
361
|
+
else -> 0.0
|
|
362
|
+
}
|
|
363
|
+
|
|
325
364
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
326
365
|
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
327
366
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -404,18 +443,38 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
404
443
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
405
444
|
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
406
445
|
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
407
|
-
|
|
446
|
+
val availableCross = if (orientation == HORIZONTAL) contentHeight else contentWidth
|
|
447
|
+
val totalCross = lines.sumOf { it.cross } + max(0, lines.size - 1) * crossGap
|
|
448
|
+
val crossFree = max(0, availableCross - totalCross)
|
|
449
|
+
var crossLeading = 0
|
|
450
|
+
var crossBetween = crossGap
|
|
451
|
+
if (wrap && lines.size > 1) {
|
|
452
|
+
when (alignContent) {
|
|
453
|
+
"center" -> crossLeading = crossFree / 2
|
|
454
|
+
"flex-end" -> crossLeading = crossFree
|
|
455
|
+
"space-between" -> crossBetween = crossFree / (lines.size - 1) + crossGap
|
|
456
|
+
"space-around" -> {
|
|
457
|
+
crossBetween = crossFree / lines.size + crossGap
|
|
458
|
+
crossLeading = crossFree / (lines.size * 2)
|
|
459
|
+
}
|
|
460
|
+
"stretch" -> {
|
|
461
|
+
val extra = crossFree / lines.size
|
|
462
|
+
lines.forEach { it.cross += extra }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + crossLeading
|
|
408
467
|
for (line in lines) {
|
|
409
468
|
val rawFree = availableMain - line.main
|
|
410
|
-
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink }
|
|
469
|
+
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
411
470
|
val totalFactor = factors.sum()
|
|
412
471
|
val distributedMain = line.entries.indices.sumOf { index ->
|
|
413
472
|
val base = if (orientation == HORIZONTAL) line.entries[index].width else line.entries[index].height
|
|
414
473
|
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
415
474
|
val constrained = if (orientation == HORIZONTAL) {
|
|
416
|
-
clamp(primary, line.entries[index].style.minWidth, line.entries[index].style.maxWidth)
|
|
475
|
+
clamp(primary, line.entries[index].style.minWidth?.resolve(contentWidth), line.entries[index].style.maxWidth?.resolve(contentWidth))
|
|
417
476
|
} else {
|
|
418
|
-
clamp(primary, line.entries[index].style.minHeight, line.entries[index].style.maxHeight)
|
|
477
|
+
clamp(primary, line.entries[index].style.minHeight?.resolve(contentHeight), line.entries[index].style.maxHeight?.resolve(contentHeight))
|
|
419
478
|
}
|
|
420
479
|
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
421
480
|
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
@@ -447,16 +506,16 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
447
506
|
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
448
507
|
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factors[index] / totalFactor).toInt()) else primary
|
|
449
508
|
val distributedPrimary = if (orientation == HORIZONTAL) {
|
|
450
|
-
clamp(distributed, entry.style.minWidth, entry.style.maxWidth)
|
|
509
|
+
clamp(distributed, entry.style.minWidth?.resolve(contentWidth), entry.style.maxWidth?.resolve(contentWidth))
|
|
451
510
|
} else {
|
|
452
|
-
clamp(distributed, entry.style.minHeight, entry.style.maxHeight)
|
|
511
|
+
clamp(distributed, entry.style.minHeight?.resolve(contentHeight), entry.style.maxHeight?.resolve(contentHeight))
|
|
453
512
|
}
|
|
454
513
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
455
514
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
456
515
|
entry.view.layout(x, y, x + w, y + h)
|
|
457
516
|
mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
458
517
|
}
|
|
459
|
-
crossOffset += line.cross +
|
|
518
|
+
crossOffset += line.cross + crossBetween
|
|
460
519
|
}
|
|
461
520
|
for (entry in absolute) {
|
|
462
521
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
@@ -479,7 +538,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
479
538
|
}
|
|
480
539
|
}
|
|
481
540
|
|
|
482
|
-
private class CraftNativeScrollView(context: Context) :
|
|
541
|
+
private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
|
|
483
542
|
val content = CraftNativeFlexLayout(context)
|
|
484
543
|
private val vertical = ScrollView(context)
|
|
485
544
|
private val horizontal = HorizontalScrollView(context)
|
|
@@ -489,15 +548,31 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
489
548
|
private var alwaysBounceVertical = true
|
|
490
549
|
private var alwaysBounceHorizontal = true
|
|
491
550
|
private var keyboardShouldPersistTaps = "never"
|
|
551
|
+
private var refreshHandler: (() -> Unit)? = null
|
|
492
552
|
|
|
493
553
|
init {
|
|
494
554
|
vertical.isNestedScrollingEnabled = true
|
|
495
555
|
horizontal.isNestedScrollingEnabled = true
|
|
496
556
|
vertical.isFillViewport = true
|
|
497
557
|
horizontal.isFillViewport = true
|
|
558
|
+
setOnRefreshListener { refreshHandler?.invoke() }
|
|
559
|
+
isEnabled = false
|
|
498
560
|
setHorizontal(false)
|
|
499
561
|
}
|
|
500
562
|
|
|
563
|
+
fun setRefreshHandler(handler: (() -> Unit)?, refreshing: Boolean) {
|
|
564
|
+
refreshHandler = handler
|
|
565
|
+
isEnabled = handler != null
|
|
566
|
+
isRefreshing = handler != null && refreshing
|
|
567
|
+
if (handler == null) isRefreshing = false
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
override fun onDetachedFromWindow() {
|
|
571
|
+
refreshHandler = null
|
|
572
|
+
isRefreshing = false
|
|
573
|
+
super.onDetachedFromWindow()
|
|
574
|
+
}
|
|
575
|
+
|
|
501
576
|
fun setHorizontal(value: Boolean) {
|
|
502
577
|
if (horizontalMode == value) return
|
|
503
578
|
horizontalMode = value
|
|
@@ -661,6 +736,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
661
736
|
val content = CraftNativeFlexLayout(context)
|
|
662
737
|
private val blocker = View(context)
|
|
663
738
|
private var renderedVisible = false
|
|
739
|
+
private var animationGeneration = 0
|
|
664
740
|
var onRequestClose: (() -> Unit)? = null
|
|
665
741
|
|
|
666
742
|
var transparent: Boolean = false
|
|
@@ -679,22 +755,59 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
679
755
|
}
|
|
680
756
|
|
|
681
757
|
fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
|
|
682
|
-
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE)
|
|
758
|
+
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) {
|
|
759
|
+
if (visible && alpha != opacity) alpha = opacity
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
val wasVisible = renderedVisible && visibility == View.VISIBLE
|
|
683
763
|
renderedVisible = visible
|
|
764
|
+
animationGeneration += 1
|
|
765
|
+
val generation = animationGeneration
|
|
766
|
+
animate().cancel()
|
|
684
767
|
if (!visible) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
768
|
+
if (animationType == "slide" && wasVisible) {
|
|
769
|
+
val distance = max(height, (parent as? View)?.height ?: resources.displayMetrics.heightPixels).toFloat()
|
|
770
|
+
animate().translationY(distance).alpha(0f).setDuration(240L).withEndAction {
|
|
771
|
+
if (animationGeneration != generation) return@withEndAction
|
|
772
|
+
visibility = View.GONE
|
|
773
|
+
translationY = 0f
|
|
774
|
+
alpha = 0f
|
|
775
|
+
}.start()
|
|
776
|
+
} else {
|
|
777
|
+
alpha = 0f
|
|
778
|
+
translationY = 0f
|
|
779
|
+
visibility = View.GONE
|
|
780
|
+
}
|
|
688
781
|
return
|
|
689
782
|
}
|
|
690
783
|
visibility = View.VISIBLE
|
|
691
784
|
if (animationType == "none") {
|
|
785
|
+
translationY = 0f
|
|
786
|
+
alpha = opacity
|
|
787
|
+
} else if (animationType == "slide") {
|
|
692
788
|
alpha = opacity
|
|
789
|
+
translationY = max(height, (parent as? View)?.height ?: resources.displayMetrics.heightPixels).toFloat()
|
|
790
|
+
post {
|
|
791
|
+
if (animationGeneration != generation || visibility != View.VISIBLE) return@post
|
|
792
|
+
val distance = max(height, (parent as? View)?.height ?: resources.displayMetrics.heightPixels).toFloat()
|
|
793
|
+
translationY = distance
|
|
794
|
+
animate().translationY(0f).setDuration(240L).start()
|
|
795
|
+
}
|
|
693
796
|
} else {
|
|
797
|
+
translationY = 0f
|
|
694
798
|
alpha = 0f
|
|
695
|
-
animate().alpha(opacity).setDuration(
|
|
799
|
+
animate().alpha(opacity).setDuration(180L).start()
|
|
696
800
|
}
|
|
697
801
|
}
|
|
802
|
+
|
|
803
|
+
fun resetForTeardown() {
|
|
804
|
+
animationGeneration += 1
|
|
805
|
+
animate().cancel()
|
|
806
|
+
visibility = View.GONE
|
|
807
|
+
translationY = 0f
|
|
808
|
+
alpha = 0f
|
|
809
|
+
onRequestClose = null
|
|
810
|
+
}
|
|
698
811
|
}
|
|
699
812
|
|
|
700
813
|
private data class LayoutBounds(
|
|
@@ -752,9 +865,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
752
865
|
var active = true
|
|
753
866
|
|
|
754
867
|
fun close() {
|
|
868
|
+
if (!active) return
|
|
755
869
|
active = false
|
|
756
|
-
|
|
757
|
-
|
|
870
|
+
val finish = {
|
|
871
|
+
port?.close()
|
|
872
|
+
isolate.close()
|
|
873
|
+
}
|
|
874
|
+
val teardown = runCatching {
|
|
875
|
+
isolate.evaluateJavaScriptAsync(
|
|
876
|
+
"globalThis.__stxNativeUnmount && globalThis.__stxNativeUnmount()"
|
|
877
|
+
)
|
|
878
|
+
}.getOrNull()
|
|
879
|
+
if (teardown == null) {
|
|
880
|
+
finish()
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
teardown.addListener({
|
|
884
|
+
runCatching { teardown.get() }
|
|
885
|
+
finish()
|
|
886
|
+
}, java.util.concurrent.Executor { command -> command.run() })
|
|
758
887
|
}
|
|
759
888
|
}
|
|
760
889
|
|
|
@@ -922,6 +1051,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
922
1051
|
capabilityTimeoutMs: $CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS,
|
|
923
1052
|
capabilities: ${capabilities.names()},
|
|
924
1053
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
1054
|
+
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
925
1055
|
postMessage: function(value) { port.postMessage(value); },
|
|
926
1056
|
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
927
1057
|
};
|
|
@@ -935,6 +1065,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
935
1065
|
capabilityTimeoutMs: $CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS,
|
|
936
1066
|
capabilities: ${capabilities.names()},
|
|
937
1067
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
1068
|
+
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
938
1069
|
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
939
1070
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
940
1071
|
};
|
|
@@ -991,6 +1122,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
991
1122
|
refreshInputDefaultsForTraitChange()
|
|
992
1123
|
screens.lastOrNull()?.let { screen ->
|
|
993
1124
|
refreshScreenTheme(screen)
|
|
1125
|
+
if (screen.active) {
|
|
1126
|
+
send(screen, "APPEARANCE", JSONObject().put("colorScheme", colorScheme()))
|
|
1127
|
+
}
|
|
994
1128
|
}
|
|
995
1129
|
}
|
|
996
1130
|
|
|
@@ -1011,6 +1145,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1011
1145
|
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
1012
1146
|
}
|
|
1013
1147
|
|
|
1148
|
+
private fun colorScheme(): String = if (
|
|
1149
|
+
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
1150
|
+
) "dark" else "light"
|
|
1151
|
+
|
|
1014
1152
|
private fun refreshInputDefaultsForTraitChange() {
|
|
1015
1153
|
if (inputHintDefaults.isEmpty() && inputHighlightDefaults.isEmpty()) return
|
|
1016
1154
|
val probe = EditText(this)
|
|
@@ -1102,10 +1240,30 @@ class MainActivity : AppCompatActivity() {
|
|
|
1102
1240
|
val document = screen.mutations.node(target) ?: return false
|
|
1103
1241
|
if (!screen.controls.containsKey("id:$target")) return false
|
|
1104
1242
|
makeView(screen, document, target, screen.controls)
|
|
1243
|
+
val updated = screen.controls["id:$target"] ?: return false
|
|
1244
|
+
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
1105
1245
|
}
|
|
1106
1246
|
return true
|
|
1107
1247
|
}
|
|
1108
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* Targeted mutations bypass the parent's child reconciliation. Rebind the
|
|
1251
|
+
* updated node's layout style explicitly so removing a width/height (or
|
|
1252
|
+
* changing flex participation) invalidates the native parent's metrics.
|
|
1253
|
+
*/
|
|
1254
|
+
private fun updateParentLayoutStyle(view: View, style: JSONObject): Boolean {
|
|
1255
|
+
var parent = view.parent
|
|
1256
|
+
while (parent != null) {
|
|
1257
|
+
if (parent is CraftNativeFlexLayout) {
|
|
1258
|
+
parent.setChildStyle(view, style, resources.displayMetrics.density)
|
|
1259
|
+
parent.requestLayout()
|
|
1260
|
+
return true
|
|
1261
|
+
}
|
|
1262
|
+
parent = parent.parent
|
|
1263
|
+
}
|
|
1264
|
+
return false
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1109
1267
|
private fun renderCommitted(screen: Screen, document: JSONObject) {
|
|
1110
1268
|
// Keep live controls (especially the focused EditText) across renders.
|
|
1111
1269
|
val next = mutableMapOf<String, View>()
|
|
@@ -1154,6 +1312,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1154
1312
|
val reusableInput = previous as? EditText
|
|
1155
1313
|
val control = when (type) {
|
|
1156
1314
|
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
|
|
1315
|
+
includeFontPadding = false
|
|
1157
1316
|
maxLines = props.optInt("numberOfLines", 0).takeIf { it > 0 } ?: Int.MAX_VALUE
|
|
1158
1317
|
ellipsize = when (props.optString("ellipsizeMode")) {
|
|
1159
1318
|
"head" -> TextUtils.TruncateAt.START
|
|
@@ -1164,6 +1323,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1164
1323
|
configureText(this, childText(children), style)
|
|
1165
1324
|
}
|
|
1166
1325
|
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
1326
|
+
includeFontPadding = false
|
|
1167
1327
|
configureText(this, props.optString("title").ifBlank { childText(children) }, style)
|
|
1168
1328
|
val linkColor = props.optString("color").takeIf { it.isNotBlank() } ?: style.optString("color")
|
|
1169
1329
|
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
@@ -1180,6 +1340,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1180
1340
|
}
|
|
1181
1341
|
"Button" -> (previous as? Button ?: Button(this)).apply {
|
|
1182
1342
|
val title = props.optString("title").ifBlank { childText(children) }
|
|
1343
|
+
isAllCaps = false
|
|
1183
1344
|
configureText(this, title, style)
|
|
1184
1345
|
props.optString("color").takeIf { it.isNotBlank() }?.let { setTextColor(color(it, defaultTextColor)) }
|
|
1185
1346
|
isEnabled = !props.optBoolean("disabled", false)
|
|
@@ -1287,17 +1448,27 @@ class MainActivity : AppCompatActivity() {
|
|
|
1287
1448
|
val value = optionProps?.optString("value").takeIf { !it.isNullOrBlank() } ?: label
|
|
1288
1449
|
value to label
|
|
1289
1450
|
}
|
|
1451
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1452
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1453
|
+
// Detach the old listener before replacing the adapter or selection.
|
|
1454
|
+
// Both operations can synchronously dispatch an Android selection
|
|
1455
|
+
// callback during reconciliation, but neither is user input.
|
|
1456
|
+
onItemSelectedListener = null
|
|
1290
1457
|
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
1291
1458
|
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
1292
1459
|
}
|
|
1293
1460
|
val requested = props.optString("selectedValue").ifBlank { props.optString("value") }
|
|
1294
1461
|
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
1295
1462
|
isEnabled = !props.optBoolean("disabled", false)
|
|
1296
|
-
|
|
1297
|
-
|
|
1463
|
+
// Mounting a controlled picker must not look like a user
|
|
1464
|
+
// change. Android may dispatch the initial adapter selection
|
|
1465
|
+
// during the first layout pass; iOS only reports a choice
|
|
1466
|
+
// after the picker is interacted with.
|
|
1467
|
+
var suppressInitialSelection = true
|
|
1298
1468
|
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
1299
1469
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
1300
1470
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
1471
|
+
if (suppressInitialSelection) return
|
|
1301
1472
|
val current = screen.handlers[this@apply] ?: return
|
|
1302
1473
|
val option = options.getOrNull(position) ?: return
|
|
1303
1474
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()
|
|
@@ -1305,12 +1476,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
1305
1476
|
}
|
|
1306
1477
|
}
|
|
1307
1478
|
setSelection(selected, false)
|
|
1479
|
+
post { suppressInitialSelection = false }
|
|
1308
1480
|
}
|
|
1309
1481
|
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
1310
1482
|
transparent = props.optBoolean("transparent", false)
|
|
1311
1483
|
content.configure(style, resources.displayMetrics.density)
|
|
1312
1484
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
1313
|
-
val visible = props.optBoolean("visible", false)
|
|
1485
|
+
val visible = props.optBoolean("visible", false) && style.optString("display") != "none"
|
|
1314
1486
|
val wasVisible = screen.modalVisibility[this]
|
|
1315
1487
|
screen.modalVisibility[this] = visible
|
|
1316
1488
|
val requestClose = events.optString("onRequestClose")
|
|
@@ -1332,10 +1504,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
1332
1504
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
1333
1505
|
setOnCheckedChangeListener(null)
|
|
1334
1506
|
val requestedValue = props.opt("value")
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1507
|
+
val requestedChecked = props.opt("checked")
|
|
1508
|
+
isChecked = when {
|
|
1509
|
+
requestedValue != null && requestedValue != JSONObject.NULL -> props.optBoolean("value")
|
|
1510
|
+
requestedChecked == null || requestedChecked == JSONObject.NULL -> false
|
|
1511
|
+
requestedChecked is Boolean -> requestedChecked
|
|
1512
|
+
requestedChecked is Number -> requestedChecked.toDouble() != 0.0
|
|
1513
|
+
requestedChecked is String -> requestedChecked.isBlank() || requestedChecked.equals("true", ignoreCase = true)
|
|
1514
|
+
else -> props.optBoolean("checked", false)
|
|
1339
1515
|
}
|
|
1340
1516
|
isEnabled = !props.optBoolean("disabled", false)
|
|
1341
1517
|
props.optJSONObject("trackColor")?.let { track ->
|
|
@@ -1453,6 +1629,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1453
1629
|
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
1454
1630
|
)
|
|
1455
1631
|
setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
1632
|
+
val refreshHandler = events.optString("onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1633
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
1634
|
+
}
|
|
1635
|
+
setRefreshHandler(refreshHandler, props.optBoolean("refreshing", false))
|
|
1456
1636
|
val onScroll = events.optString("onScroll")
|
|
1457
1637
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
1458
1638
|
val onBegin = events.optString("onScrollBeginDrag")
|
|
@@ -1543,7 +1723,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1543
1723
|
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
1544
1724
|
control.isClickable = true
|
|
1545
1725
|
if (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable")) {
|
|
1546
|
-
control.setOnClickListener
|
|
1726
|
+
control.setOnClickListener {
|
|
1727
|
+
val current = screen.handlers[it] ?: return@setOnClickListener
|
|
1728
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1729
|
+
}
|
|
1547
1730
|
control.setOnTouchListener { view, event ->
|
|
1548
1731
|
when (event.actionMasked) {
|
|
1549
1732
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
@@ -1584,7 +1767,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1584
1767
|
}
|
|
1585
1768
|
control.isEnabled = !disabled
|
|
1586
1769
|
}
|
|
1587
|
-
val
|
|
1770
|
+
val longPressDisabled = props.optBoolean("disabled", false) ||
|
|
1771
|
+
props.optJSONObject("accessibilityState")?.optBoolean("disabled", false) == true
|
|
1772
|
+
val longPressHandler = events.optString("onLongPress").takeUnless { longPressDisabled } ?: ""
|
|
1588
1773
|
if (longPressHandler.isNotBlank()) {
|
|
1589
1774
|
screen.longPressHandlers[control] = longPressHandler
|
|
1590
1775
|
control.setOnLongClickListener {
|
|
@@ -1613,6 +1798,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
1613
1798
|
}
|
|
1614
1799
|
next[identity] = control
|
|
1615
1800
|
applyViewStyle(control, style)
|
|
1801
|
+
if (type == "ActivityIndicator") {
|
|
1802
|
+
val animating = props.optBoolean("animating", true)
|
|
1803
|
+
val hidesWhenStopped = props.optBoolean("hidesWhenStopped", true)
|
|
1804
|
+
control.visibility = if (style.optString("display") == "none" || (!animating && hidesWhenStopped)) {
|
|
1805
|
+
View.GONE
|
|
1806
|
+
} else {
|
|
1807
|
+
View.VISIBLE
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1616
1810
|
if (type == "Modal" && !props.optBoolean("visible", false)) control.visibility = View.GONE
|
|
1617
1811
|
val width = style.optDouble("width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1618
1812
|
val height = style.optDouble("height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
@@ -1672,7 +1866,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1672
1866
|
val child = children.opt(index)
|
|
1673
1867
|
val childView = when (child) {
|
|
1674
1868
|
is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
|
|
1675
|
-
is String -> TextView(this).apply {
|
|
1869
|
+
is String -> TextView(this).apply {
|
|
1870
|
+
includeFontPadding = false
|
|
1871
|
+
text = child
|
|
1872
|
+
setTextColor(defaultTextColor)
|
|
1873
|
+
}
|
|
1676
1874
|
else -> null
|
|
1677
1875
|
}
|
|
1678
1876
|
if (childView != null) desired.add(childView)
|
|
@@ -1790,8 +1988,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
1790
1988
|
view.clipToOutline = style.optString("overflow") == "hidden"
|
|
1791
1989
|
if (view is ViewGroup) view.clipChildren = style.optString("overflow") == "hidden"
|
|
1792
1990
|
if (view is ImageView) {
|
|
1793
|
-
style.
|
|
1794
|
-
|
|
1991
|
+
val tint = style.opt("tintColor") as? String
|
|
1992
|
+
if (tint.isNullOrBlank() || tint == "null" || tint == "undefined") {
|
|
1993
|
+
view.clearColorFilter()
|
|
1994
|
+
} else {
|
|
1995
|
+
view.setColorFilter(color(tint, Color.TRANSPARENT))
|
|
1996
|
+
}
|
|
1795
1997
|
}
|
|
1796
1998
|
}
|
|
1797
1999
|
|
|
@@ -1857,8 +2059,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1857
2059
|
else -> ""
|
|
1858
2060
|
}
|
|
1859
2061
|
if (uri.isBlank()) {
|
|
2062
|
+
if (imageSources[view] == "") return
|
|
1860
2063
|
imageJobs.remove(view)?.cancel(true)
|
|
1861
|
-
imageSources
|
|
2064
|
+
imageSources[view] = ""
|
|
1862
2065
|
imageErrors[view] = "Image source is missing"
|
|
1863
2066
|
view.setImageDrawable(null)
|
|
1864
2067
|
imageFailure(screen, view, uri, "Image source is missing")
|
|
@@ -1961,7 +2164,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1961
2164
|
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
1962
2165
|
val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
|
|
1963
2166
|
val state = props.optJSONObject("accessibilityState")
|
|
1964
|
-
val disabled = state?.optBoolean("disabled", false) == true
|
|
2167
|
+
val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
|
|
1965
2168
|
if (type !in setOf("Button", "Link", "TextInput", "Picker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
1966
2169
|
view.isEnabled = !disabled
|
|
1967
2170
|
} else if (disabled) {
|
|
@@ -2236,6 +2439,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
2236
2439
|
released.filterIsInstance<CraftNativeFlatList>().filterNot { nestedLists.contains(it) }.forEach {
|
|
2237
2440
|
releaseFlatList(screen, it, preservingInputDrafts)
|
|
2238
2441
|
}
|
|
2442
|
+
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
2443
|
+
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|
|
2239
2444
|
releaseImages(released)
|
|
2240
2445
|
released.filterIsInstance<EditText>().forEach {
|
|
2241
2446
|
inputHintDefaults.remove(it)
|
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.133";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED