craft-native 0.0.158 → 0.0.161
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/CraftNative.kt.template +62 -1
- package/dist/android/templates/CraftNativeCapabilities.kt.template +311 -4
- package/dist/android/templates/MainActivityNative.kt.template +638 -55
- package/dist/api/mobile.d.ts +57 -4
- 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 +16 -6
- package/dist/ios/templates/CraftApp.swift +24 -0
- package/dist/ios/templates/CraftHealth.swift +123 -0
- package/dist/ios/templates/CraftHybrid.swift +19 -0
- package/dist/ios/templates/CraftNativeScreen.swift +501 -58
- package/dist/mobile.js +12 -0
- package/package.json +1 -1
|
@@ -45,11 +45,14 @@ import android.view.ViewTreeObserver
|
|
|
45
45
|
import android.widget.AdapterView
|
|
46
46
|
import android.widget.ArrayAdapter
|
|
47
47
|
import android.widget.Button
|
|
48
|
+
import android.widget.CheckBox
|
|
49
|
+
import android.widget.DatePicker
|
|
48
50
|
import android.widget.EditText
|
|
49
51
|
import android.widget.FrameLayout
|
|
50
52
|
import android.widget.HorizontalScrollView
|
|
51
53
|
import android.widget.ImageView
|
|
52
54
|
import android.widget.LinearLayout
|
|
55
|
+
import android.widget.TimePicker
|
|
53
56
|
import android.widget.ScrollView
|
|
54
57
|
import android.widget.SeekBar
|
|
55
58
|
import android.widget.Spinner
|
|
@@ -76,6 +79,9 @@ import com.craft.runtime.CraftNative
|
|
|
76
79
|
import org.json.JSONArray
|
|
77
80
|
import org.json.JSONObject
|
|
78
81
|
import java.net.URL
|
|
82
|
+
import java.text.SimpleDateFormat
|
|
83
|
+
import java.util.Calendar
|
|
84
|
+
import java.util.Locale
|
|
79
85
|
import java.util.concurrent.Executors
|
|
80
86
|
import java.util.concurrent.Future
|
|
81
87
|
import java.util.UUID
|
|
@@ -109,6 +115,7 @@ private data class CraftNativeLength(
|
|
|
109
115
|
private data class CraftNativeLayoutStyle(
|
|
110
116
|
val width: CraftNativeLength? = null,
|
|
111
117
|
val height: CraftNativeLength? = null,
|
|
118
|
+
val aspectRatio: Double? = null,
|
|
112
119
|
val minWidth: CraftNativeLength? = null,
|
|
113
120
|
val maxWidth: CraftNativeLength? = null,
|
|
114
121
|
val minHeight: CraftNativeLength? = null,
|
|
@@ -137,6 +144,7 @@ private data class CraftNativeLayoutStyle(
|
|
|
137
144
|
val flex = decimal("flex")
|
|
138
145
|
return CraftNativeLayoutStyle(
|
|
139
146
|
width = length("width"), height = length("height"),
|
|
147
|
+
aspectRatio = decimal("aspectRatio")?.takeIf { it.isFinite() && it > 0.0 },
|
|
140
148
|
minWidth = length("minWidth"), maxWidth = length("maxWidth"),
|
|
141
149
|
minHeight = length("minHeight"), maxHeight = length("maxHeight"),
|
|
142
150
|
position = raw.opt("position")
|
|
@@ -389,7 +397,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
389
397
|
private var columnGap = 0
|
|
390
398
|
private var grid = false
|
|
391
399
|
private var gridColumns = 1
|
|
392
|
-
private var gridAutoRows:
|
|
400
|
+
private var gridAutoRows: CraftNativeLength? = null
|
|
393
401
|
private var reverse = false
|
|
394
402
|
|
|
395
403
|
fun configure(style: JSONObject, density: Float) {
|
|
@@ -406,7 +414,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
406
414
|
gridColumns = craftNativeGridColumnCount(
|
|
407
415
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
408
416
|
)
|
|
409
|
-
gridAutoRows = style.
|
|
417
|
+
gridAutoRows = CraftNativeLength.from(style.opt("gridAutoRows"), density)
|
|
410
418
|
val padding = (style.optDouble("padding", 0.0) * density).toInt()
|
|
411
419
|
val horizontal = (style.optDouble("paddingHorizontal", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
412
420
|
val vertical = (style.optDouble("paddingVertical", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
@@ -534,7 +542,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
534
542
|
}
|
|
535
543
|
}
|
|
536
544
|
val rowHeights = rows.map { row ->
|
|
537
|
-
max(gridAutoRows ?: 0, row.maxOfOrNull { entry ->
|
|
545
|
+
max(gridAutoRows?.resolve(height - paddingTop - paddingBottom) ?: 0, row.maxOfOrNull { entry ->
|
|
538
546
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
539
547
|
entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
540
548
|
} ?: 0)
|
|
@@ -559,7 +567,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
559
567
|
val childHeight = measureChildHeight(child, style, height, verticalMargins)
|
|
560
568
|
val column = flowIndex % columns
|
|
561
569
|
val row = flowIndex / columns
|
|
562
|
-
while (rowHeights.size <= row) rowHeights += gridAutoRows ?: 0
|
|
570
|
+
while (rowHeights.size <= row) rowHeights += gridAutoRows?.resolve(null) ?: 0
|
|
563
571
|
columnWidths[column] = max(columnWidths[column], childWidth + horizontalMargins)
|
|
564
572
|
rowHeights[row] = max(rowHeights[row], childHeight + verticalMargins)
|
|
565
573
|
flowIndex += 1
|
|
@@ -577,12 +585,30 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
577
585
|
// The STX style is authoritative. A reused View may still carry a
|
|
578
586
|
// previous fixed LayoutParams width after a reactive width is removed;
|
|
579
587
|
// using that value here prevents intrinsic sizing from being restored.
|
|
580
|
-
val
|
|
581
|
-
val
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
588
|
+
val authored = style.width?.resolve(base)
|
|
589
|
+
val explicit = authored ?: style.aspectRatio?.let { ratio -> style.height?.resolve(base)?.times(ratio)?.roundToInt() } ?: flexBasis
|
|
590
|
+
if (explicit != null) {
|
|
591
|
+
child.measure(
|
|
592
|
+
View.MeasureSpec.makeMeasureSpec(explicit, View.MeasureSpec.EXACTLY),
|
|
593
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
594
|
+
)
|
|
595
|
+
return clamp(explicit, style.minWidth?.resolve(base), style.maxWidth?.resolve(base))
|
|
596
|
+
}
|
|
597
|
+
// Measure text and nested flex containers at max-content first. An
|
|
598
|
+
// AT_MOST spec equal to the available room can make Android report the
|
|
599
|
+
// longest-word (min-content) width for a wrapping TextView, collapsing
|
|
600
|
+
// shrink-to-fit bubbles and pills before the parent can clamp them.
|
|
601
|
+
val intrinsicSpec = if (child is TextView && child !is EditText) 1_000_000 else base
|
|
602
|
+
child.measure(
|
|
603
|
+
View.MeasureSpec.makeMeasureSpec(intrinsicSpec, View.MeasureSpec.AT_MOST),
|
|
604
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
605
|
+
)
|
|
606
|
+
val target = clamp(min(base, child.measuredWidth), style.minWidth?.resolve(base), style.maxWidth?.resolve(base))
|
|
607
|
+
child.measure(
|
|
608
|
+
View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.AT_MOST),
|
|
609
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
610
|
+
)
|
|
611
|
+
return target
|
|
586
612
|
}
|
|
587
613
|
|
|
588
614
|
private fun measureChildHeight(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
@@ -590,7 +616,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
590
616
|
val flexBasis = if (orientation == VERTICAL) style.flexBasis?.resolve(base) else null
|
|
591
617
|
// Match width measurement: do not let stale native LayoutParams keep
|
|
592
618
|
// a removed reactive height fixed.
|
|
593
|
-
val
|
|
619
|
+
val authored = style.height?.resolve(base)
|
|
620
|
+
val explicit = authored ?: style.aspectRatio?.let { ratio -> style.width?.resolve(base)?.div(ratio)?.roundToInt() } ?: flexBasis
|
|
594
621
|
val target = explicit ?: base
|
|
595
622
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
596
623
|
else View.MeasureSpec.makeMeasureSpec(base, View.MeasureSpec.AT_MOST)
|
|
@@ -629,6 +656,68 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
629
656
|
else -> 0
|
|
630
657
|
}
|
|
631
658
|
|
|
659
|
+
/** Keep the old-runtime shim on the same bounded-flex water-fill rule as the Zig bridge. */
|
|
660
|
+
private fun fallbackFlexWidths(line: Line, available: Int, gap: Int): IntArray {
|
|
661
|
+
val horizontal = orientation == HORIZONTAL
|
|
662
|
+
val hypothetical = line.entries.sumOf { entry ->
|
|
663
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
664
|
+
val margins = if (horizontal) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
665
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
666
|
+
(if (horizontal) entry.width else entry.height) + margins
|
|
667
|
+
} + max(0, line.entries.size - 1) * gap
|
|
668
|
+
val free = available - hypothetical
|
|
669
|
+
val growTotal = line.entries.sumOf { it.style.flexGrow }
|
|
670
|
+
val shrinkTotal = line.entries.sumOf {
|
|
671
|
+
(it.style.flexShrink ?: defaultFlexShrink(it.view)) * (if (horizontal) it.width else it.height)
|
|
672
|
+
}
|
|
673
|
+
val growing = free > 0 && growTotal > 0
|
|
674
|
+
val shrinking = free < 0 && shrinkTotal > 0
|
|
675
|
+
val widths = IntArray(line.entries.size) { index -> if (horizontal) line.entries[index].width else line.entries[index].height }
|
|
676
|
+
if (!growing && !shrinking) return widths
|
|
677
|
+
|
|
678
|
+
val frozen = BooleanArray(widths.size)
|
|
679
|
+
repeat(widths.size + 1) {
|
|
680
|
+
var fixed = max(0, widths.size - 1) * gap.toDouble()
|
|
681
|
+
var activeBasis = 0.0
|
|
682
|
+
var factor = 0.0
|
|
683
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
684
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
685
|
+
val margins = if (horizontal) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
686
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
687
|
+
fixed += margins
|
|
688
|
+
if (frozen[index]) fixed += widths[index]
|
|
689
|
+
else {
|
|
690
|
+
val basis = if (horizontal) entry.width else entry.height
|
|
691
|
+
activeBasis += basis
|
|
692
|
+
factor += if (growing) entry.style.flexGrow
|
|
693
|
+
else (entry.style.flexShrink ?: defaultFlexShrink(entry.view)) * basis
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (factor <= 0) return@repeat
|
|
697
|
+
val remaining = available - fixed - activeBasis
|
|
698
|
+
var froze = false
|
|
699
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
700
|
+
if (frozen[index]) continue
|
|
701
|
+
val basis = (if (horizontal) entry.width else entry.height).toDouble()
|
|
702
|
+
val share = if (growing) remaining * entry.style.flexGrow / factor
|
|
703
|
+
else -remaining * (entry.style.flexShrink ?: defaultFlexShrink(entry.view)) * basis / factor
|
|
704
|
+
val raw = if (growing) basis + share else basis - share
|
|
705
|
+
val lower = (if (horizontal) entry.style.minWidth else entry.style.minHeight)?.resolve(if (horizontal) width - paddingLeft - paddingRight else height - paddingTop - paddingBottom)
|
|
706
|
+
val upper = (if (horizontal) entry.style.maxWidth else entry.style.maxHeight)?.resolve(if (horizontal) width - paddingLeft - paddingRight else height - paddingTop - paddingBottom)
|
|
707
|
+
var bounded = raw
|
|
708
|
+
if (lower != null) bounded = max(bounded, lower.toDouble())
|
|
709
|
+
if (upper != null) bounded = min(bounded, upper.toDouble())
|
|
710
|
+
widths[index] = bounded.roundToInt().coerceAtLeast(0)
|
|
711
|
+
if (bounded != raw) {
|
|
712
|
+
frozen[index] = true
|
|
713
|
+
froze = true
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (!froze) return@repeat
|
|
717
|
+
}
|
|
718
|
+
return widths
|
|
719
|
+
}
|
|
720
|
+
|
|
632
721
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
633
722
|
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
634
723
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -667,6 +756,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
667
756
|
val contentWidth = width - paddingLeft - paddingRight
|
|
668
757
|
val contentHeight = height - paddingTop - paddingBottom
|
|
669
758
|
val flow = layout.rows.flatten()
|
|
759
|
+
// Older runtimes return null for the optional symbol; the local
|
|
760
|
+
// layout below remains the compatibility path in that case.
|
|
670
761
|
val sharedFrames = CraftNative.grid(
|
|
671
762
|
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap.toFloat(), rowGap.toFloat(),
|
|
672
763
|
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
@@ -684,6 +775,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
684
775
|
val align = it.style.alignSelf ?: alignItems
|
|
685
776
|
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
686
777
|
}.toFloatArray(),
|
|
778
|
+
gridAutoRows?.resolve(contentHeight)?.toFloat(),
|
|
779
|
+
alignContentCode(),
|
|
687
780
|
)
|
|
688
781
|
if (sharedFrames != null) {
|
|
689
782
|
for ((index, entry) in flow.withIndex()) {
|
|
@@ -695,9 +788,26 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
695
788
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
696
789
|
}
|
|
697
790
|
} else {
|
|
698
|
-
|
|
791
|
+
val totalRows = layout.rowHeights.sum()
|
|
792
|
+
val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
|
|
793
|
+
val freeRows = max(0, contentHeight - totalRows - rowGaps)
|
|
794
|
+
val rowCount = max(1, layout.rowHeights.size)
|
|
795
|
+
var rowLeading = 0
|
|
796
|
+
var rowBetween = rowGap
|
|
797
|
+
var rowExtra = 0
|
|
798
|
+
when (alignContentCode()) {
|
|
799
|
+
1 -> rowLeading = freeRows / 2
|
|
800
|
+
2 -> rowLeading = freeRows
|
|
801
|
+
3 -> if (rowCount > 1) rowBetween += freeRows / (rowCount - 1)
|
|
802
|
+
4 -> {
|
|
803
|
+
rowLeading = freeRows / (rowCount * 2)
|
|
804
|
+
rowBetween += freeRows / rowCount
|
|
805
|
+
}
|
|
806
|
+
5 -> rowExtra = freeRows / rowCount
|
|
807
|
+
}
|
|
808
|
+
var rowTop = paddingTop + rowLeading
|
|
699
809
|
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
700
|
-
val rowHeight = layout.rowHeights[rowIndex]
|
|
810
|
+
val rowHeight = layout.rowHeights[rowIndex] + rowExtra
|
|
701
811
|
for ((column, entry) in row.withIndex()) {
|
|
702
812
|
val align = entry.style.alignSelf ?: alignItems
|
|
703
813
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
@@ -721,7 +831,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
721
831
|
}
|
|
722
832
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
723
833
|
}
|
|
724
|
-
rowTop += rowHeight +
|
|
834
|
+
rowTop += rowHeight + rowBetween
|
|
725
835
|
}
|
|
726
836
|
}
|
|
727
837
|
for (entry in layout.absolute) {
|
|
@@ -770,7 +880,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
770
880
|
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + crossLeading
|
|
771
881
|
for ((lineIndex, line) in lines.withIndex()) {
|
|
772
882
|
val lineOffset = sharedCross?.first?.get(lineIndex)?.roundToInt()?.let { (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + it } ?: crossOffset
|
|
773
|
-
val
|
|
883
|
+
val fallbackWidths = fallbackFlexWidths(line, availableMain, gap)
|
|
774
884
|
val sharedPrimary = CraftNative.flexRow(
|
|
775
885
|
availableMain.toFloat(),
|
|
776
886
|
gap.toFloat(),
|
|
@@ -801,16 +911,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
801
911
|
sharedPrimary[index].roundToInt() + margins
|
|
802
912
|
} + max(0, line.entries.size - 1) * gap
|
|
803
913
|
} else {
|
|
804
|
-
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
805
|
-
val totalFactor = factors.sum()
|
|
806
914
|
line.entries.indices.sumOf { index ->
|
|
807
|
-
val
|
|
808
|
-
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
809
|
-
val constrained = if (orientation == HORIZONTAL) {
|
|
810
|
-
clamp(primary, line.entries[index].style.minWidth?.resolve(contentWidth), line.entries[index].style.maxWidth?.resolve(contentWidth))
|
|
811
|
-
} else {
|
|
812
|
-
clamp(primary, line.entries[index].style.minHeight?.resolve(contentHeight), line.entries[index].style.maxHeight?.resolve(contentHeight))
|
|
813
|
-
}
|
|
915
|
+
val constrained = fallbackWidths[index]
|
|
814
916
|
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
815
917
|
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
816
918
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
@@ -851,17 +953,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
851
953
|
?: mainOffset + marginStart
|
|
852
954
|
val x = if (orientation == HORIZONTAL) positionedMain else lineOffset + crossPosition
|
|
853
955
|
val y = if (orientation == HORIZONTAL) lineOffset + crossPosition else positionedMain
|
|
854
|
-
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?:
|
|
855
|
-
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
856
|
-
val factor = if (rawFree >= 0) entry.style.flexGrow else entry.style.flexShrink ?: defaultFlexShrink(entry.view)
|
|
857
|
-
val totalFactor = line.entries.sumOf { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
858
|
-
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factor / totalFactor).toInt()) else primary
|
|
859
|
-
if (orientation == HORIZONTAL) {
|
|
860
|
-
clamp(distributed, entry.style.minWidth?.resolve(contentWidth), entry.style.maxWidth?.resolve(contentWidth))
|
|
861
|
-
} else {
|
|
862
|
-
clamp(distributed, entry.style.minHeight?.resolve(contentHeight), entry.style.maxHeight?.resolve(contentHeight))
|
|
863
|
-
}
|
|
864
|
-
}
|
|
956
|
+
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: fallbackWidths[index]
|
|
865
957
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
866
958
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
867
959
|
entry.view.layout(x, y, x + w, y + h)
|
|
@@ -890,6 +982,67 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
890
982
|
}
|
|
891
983
|
}
|
|
892
984
|
|
|
985
|
+
/** An image-backed flex container with ordinary native children over the image. */
|
|
986
|
+
private class CraftNativeImageBackgroundView(context: Context) : FrameLayout(context) {
|
|
987
|
+
val image = ImageView(context)
|
|
988
|
+
val content = CraftNativeFlexLayout(context)
|
|
989
|
+
|
|
990
|
+
init {
|
|
991
|
+
clipChildren = true
|
|
992
|
+
image.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
993
|
+
image.isClickable = false
|
|
994
|
+
addView(image, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
995
|
+
addView(content, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** A flex container that responds to the activity's IME inset. */
|
|
1000
|
+
private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(context) {
|
|
1001
|
+
val content = CraftNativeFlexLayout(context)
|
|
1002
|
+
private var behavior = "padding"
|
|
1003
|
+
private var keyboardVerticalOffset = 0
|
|
1004
|
+
private var keyboardInset = 0
|
|
1005
|
+
private var basePadding = intArrayOf(0, 0, 0, 0)
|
|
1006
|
+
|
|
1007
|
+
init {
|
|
1008
|
+
addView(content, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
fun configure(props: JSONObject, style: JSONObject, density: Float) {
|
|
1012
|
+
behavior = props.optString("behavior", "padding").takeIf { it in setOf("height", "position", "padding") } ?: "padding"
|
|
1013
|
+
keyboardVerticalOffset = (props.optDouble("keyboardVerticalOffset", 0.0) * density).roundToInt()
|
|
1014
|
+
content.configure(style, density)
|
|
1015
|
+
basePadding = intArrayOf(content.paddingLeft, content.paddingTop, content.paddingRight, content.paddingBottom)
|
|
1016
|
+
applyKeyboardInset()
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
fun setKeyboardInset(inset: Int) {
|
|
1020
|
+
if (keyboardInset == inset) return
|
|
1021
|
+
keyboardInset = max(0, inset)
|
|
1022
|
+
applyKeyboardInset()
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private fun effectiveInset(): Int = max(0, keyboardInset - keyboardVerticalOffset)
|
|
1026
|
+
|
|
1027
|
+
private fun applyKeyboardInset() {
|
|
1028
|
+
val inset = effectiveInset()
|
|
1029
|
+
translationY = if (behavior == "position") -inset.toFloat() else 0f
|
|
1030
|
+
val bottom = if (behavior == "padding") basePadding[3] + inset else basePadding[3]
|
|
1031
|
+
content.setPadding(basePadding[0], basePadding[1], basePadding[2], bottom)
|
|
1032
|
+
requestLayout()
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1036
|
+
val inset = effectiveInset()
|
|
1037
|
+
val adjustedHeight = if (behavior == "height" && inset > 0 && MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
|
|
1038
|
+
MeasureSpec.makeMeasureSpec(max(0, MeasureSpec.getSize(heightMeasureSpec) - inset), MeasureSpec.getMode(heightMeasureSpec))
|
|
1039
|
+
} else {
|
|
1040
|
+
heightMeasureSpec
|
|
1041
|
+
}
|
|
1042
|
+
super.onMeasure(widthMeasureSpec, adjustedHeight)
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
893
1046
|
private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
|
|
894
1047
|
val content = CraftNativeFlexLayout(context)
|
|
895
1048
|
private val vertical = ScrollView(context)
|
|
@@ -939,6 +1092,21 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
939
1092
|
addView(scroller, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
940
1093
|
}
|
|
941
1094
|
|
|
1095
|
+
/**
|
|
1096
|
+
* A targeted mutation can change a child's size without reconciling the
|
|
1097
|
+
* scroll content tree. Invalidate both the custom content layout and the
|
|
1098
|
+
* platform scroller so its measured content range is recomputed too.
|
|
1099
|
+
*/
|
|
1100
|
+
fun invalidateContentLayout() {
|
|
1101
|
+
content.forceLayout()
|
|
1102
|
+
content.requestLayout()
|
|
1103
|
+
val scroller = if (horizontalMode == true) horizontal else vertical
|
|
1104
|
+
scroller.forceLayout()
|
|
1105
|
+
scroller.requestLayout()
|
|
1106
|
+
forceLayout()
|
|
1107
|
+
requestLayout()
|
|
1108
|
+
}
|
|
1109
|
+
|
|
942
1110
|
fun setNativeNestedScrollingEnabled(value: Boolean) {
|
|
943
1111
|
vertical.isNestedScrollingEnabled = value
|
|
944
1112
|
horizontal.isNestedScrollingEnabled = value
|
|
@@ -1084,6 +1252,25 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1084
1252
|
|
|
1085
1253
|
private class CraftNativeFlexSpacer(context: Context) : View(context)
|
|
1086
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* A small, dependency-free native icon surface for Android.
|
|
1257
|
+
*
|
|
1258
|
+
* STX resolves Iconify names to the SF Symbol name that iOS draws. Android
|
|
1259
|
+
* does not ship SF Symbols, so the maintained host uses the corresponding
|
|
1260
|
+
* Unicode glyph for the common cross-platform symbols and a hollow circle for
|
|
1261
|
+
* an unknown symbol. Keeping this as a TextView gives the glyph the same
|
|
1262
|
+
* intrinsic measurement, tint, accessibility, and reconciliation behavior as
|
|
1263
|
+
* the other native leaf controls without adding an icon-font dependency.
|
|
1264
|
+
*/
|
|
1265
|
+
private class CraftNativeIconView(context: Context) : TextView(context) {
|
|
1266
|
+
init {
|
|
1267
|
+
includeFontPadding = false
|
|
1268
|
+
gravity = Gravity.CENTER
|
|
1269
|
+
setPadding(0, 0, 0, 0)
|
|
1270
|
+
isSingleLine = true
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1087
1274
|
private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
1088
1275
|
val content = CraftNativeFlexLayout(context)
|
|
1089
1276
|
private val blocker = View(context)
|
|
@@ -1214,6 +1401,176 @@ private class CraftNativeActivityIndicator(context: Context) :
|
|
|
1214
1401
|
}
|
|
1215
1402
|
}
|
|
1216
1403
|
|
|
1404
|
+
/** Android counterpart to iOS's UIDatePicker-backed native DatePicker. */
|
|
1405
|
+
private class CraftNativeDatePickerView(context: Context) : FrameLayout(context) {
|
|
1406
|
+
private enum class PickerMode { DATE, TIME, DATE_TIME, COUNTDOWN }
|
|
1407
|
+
private val maxCountdownSeconds = 23 * 3600 + 59 * 60 + 59
|
|
1408
|
+
|
|
1409
|
+
private var pickerMode: PickerMode? = null
|
|
1410
|
+
private var datePicker: DatePicker? = null
|
|
1411
|
+
private var timePicker: TimePicker? = null
|
|
1412
|
+
private var minuteInterval = 1
|
|
1413
|
+
private var suppressChanges = true
|
|
1414
|
+
var onValueChanged: (() -> Unit)? = null
|
|
1415
|
+
|
|
1416
|
+
init {
|
|
1417
|
+
isFocusable = true
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
fun configure(props: JSONObject) {
|
|
1421
|
+
val requestedMode = when (props.optString("mode")) {
|
|
1422
|
+
"time" -> PickerMode.TIME
|
|
1423
|
+
"datetime", "dateAndTime", "datetime-local" -> PickerMode.DATE_TIME
|
|
1424
|
+
"countdown" -> PickerMode.COUNTDOWN
|
|
1425
|
+
else -> PickerMode.DATE
|
|
1426
|
+
}
|
|
1427
|
+
if (pickerMode != requestedMode) {
|
|
1428
|
+
pickerMode = requestedMode
|
|
1429
|
+
datePicker = null
|
|
1430
|
+
timePicker = null
|
|
1431
|
+
removeAllViews()
|
|
1432
|
+
val content = LinearLayout(context).apply {
|
|
1433
|
+
orientation = LinearLayout.VERTICAL
|
|
1434
|
+
gravity = Gravity.CENTER
|
|
1435
|
+
}
|
|
1436
|
+
if (requestedMode == PickerMode.DATE || requestedMode == PickerMode.DATE_TIME) {
|
|
1437
|
+
datePicker = DatePicker(context).also { picker ->
|
|
1438
|
+
val today = Calendar.getInstance()
|
|
1439
|
+
picker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) { _, _, _, _ -> changed() }
|
|
1440
|
+
content.addView(picker, LinearLayout.LayoutParams(
|
|
1441
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1442
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1443
|
+
))
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (requestedMode != PickerMode.DATE) {
|
|
1447
|
+
timePicker = TimePicker(context).also { picker ->
|
|
1448
|
+
picker.setIs24HourView(true)
|
|
1449
|
+
picker.setOnTimeChangedListener { _, _, _ -> changed() }
|
|
1450
|
+
content.addView(picker, LinearLayout.LayoutParams(
|
|
1451
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1452
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1453
|
+
))
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
suppressChanges = true
|
|
1460
|
+
minuteInterval = props.optInt("minuteInterval", 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
|
|
1461
|
+
pickerDate(props.opt("minimumDate"))?.let { datePicker?.minDate = it.timeInMillis }
|
|
1462
|
+
pickerDate(props.opt("maximumDate"))?.let { datePicker?.maxDate = it.timeInMillis }
|
|
1463
|
+
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1464
|
+
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
1465
|
+
} else {
|
|
1466
|
+
pickerDate(props.opt("value"))?.let { applyDate(it) }
|
|
1467
|
+
}
|
|
1468
|
+
val enabled = !props.optBoolean("disabled", false)
|
|
1469
|
+
isEnabled = enabled
|
|
1470
|
+
datePicker?.isEnabled = enabled
|
|
1471
|
+
timePicker?.isEnabled = enabled
|
|
1472
|
+
for (index in 0 until childCount) getChildAt(index).isEnabled = enabled
|
|
1473
|
+
suppressChanges = false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
fun event(): JSONObject {
|
|
1477
|
+
if (pickerMode == PickerMode.COUNTDOWN) {
|
|
1478
|
+
val seconds = currentCountdownSeconds()
|
|
1479
|
+
return JSONObject()
|
|
1480
|
+
.put("value", formatCountdown(seconds))
|
|
1481
|
+
.put("iso", "PT${seconds}S")
|
|
1482
|
+
.put("timestamp", seconds * 1000L)
|
|
1483
|
+
}
|
|
1484
|
+
val current = currentDate()
|
|
1485
|
+
val valueFormat = when (pickerMode) {
|
|
1486
|
+
PickerMode.DATE -> "yyyy-MM-dd"
|
|
1487
|
+
PickerMode.TIME -> "HH:mm"
|
|
1488
|
+
else -> "yyyy-MM-dd'T'HH:mm"
|
|
1489
|
+
}
|
|
1490
|
+
val value = SimpleDateFormat(valueFormat, Locale.US).apply { isLenient = false }.format(current.time)
|
|
1491
|
+
val iso = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(current.time)
|
|
1492
|
+
return JSONObject().put("value", value).put("iso", iso).put("timestamp", current.timeInMillis)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private fun changed() {
|
|
1496
|
+
if (suppressChanges) return
|
|
1497
|
+
val picker = timePicker ?: return
|
|
1498
|
+
val snapped = ((picker.minute + minuteInterval / 2) / minuteInterval * minuteInterval).coerceAtMost(59)
|
|
1499
|
+
if (snapped != picker.minute) {
|
|
1500
|
+
suppressChanges = true
|
|
1501
|
+
picker.minute = snapped
|
|
1502
|
+
suppressChanges = false
|
|
1503
|
+
}
|
|
1504
|
+
onValueChanged?.invoke()
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
private fun applyDate(value: Calendar) {
|
|
1508
|
+
datePicker?.updateDate(value.get(Calendar.YEAR), value.get(Calendar.MONTH), value.get(Calendar.DAY_OF_MONTH))
|
|
1509
|
+
timePicker?.hour = value.get(Calendar.HOUR_OF_DAY)
|
|
1510
|
+
timePicker?.minute = value.get(Calendar.MINUTE)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private fun applyCountdown(seconds: Int) {
|
|
1514
|
+
timePicker?.hour = (seconds / 3600).coerceIn(0, 23)
|
|
1515
|
+
timePicker?.minute = (seconds % 3600 / 60).coerceIn(0, 59)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private fun currentCountdownSeconds(): Int = timePicker?.let { it.hour * 3600 + it.minute * 60 } ?: 0
|
|
1519
|
+
|
|
1520
|
+
private fun formatCountdown(seconds: Int): String = "%02d:%02d:%02d".format(
|
|
1521
|
+
Locale.US,
|
|
1522
|
+
seconds / 3600,
|
|
1523
|
+
seconds % 3600 / 60,
|
|
1524
|
+
seconds % 60,
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
private fun currentDate(): Calendar = Calendar.getInstance().also { current ->
|
|
1528
|
+
datePicker?.let {
|
|
1529
|
+
current.set(Calendar.YEAR, it.year)
|
|
1530
|
+
current.set(Calendar.MONTH, it.month)
|
|
1531
|
+
current.set(Calendar.DAY_OF_MONTH, it.dayOfMonth)
|
|
1532
|
+
}
|
|
1533
|
+
timePicker?.let {
|
|
1534
|
+
current.set(Calendar.HOUR_OF_DAY, it.hour)
|
|
1535
|
+
current.set(Calendar.MINUTE, it.minute)
|
|
1536
|
+
current.set(Calendar.SECOND, 0)
|
|
1537
|
+
current.set(Calendar.MILLISECOND, 0)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
private fun pickerDate(value: Any?): Calendar? {
|
|
1542
|
+
if (value is Number && value !is Boolean) return Calendar.getInstance().apply { timeInMillis = value.toLong() }
|
|
1543
|
+
val text = value?.toString()?.trim().orEmpty()
|
|
1544
|
+
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1545
|
+
val formats = listOf(
|
|
1546
|
+
"HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
|
|
1547
|
+
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
|
|
1548
|
+
)
|
|
1549
|
+
formats.firstNotNullOfOrNull { format ->
|
|
1550
|
+
runCatching {
|
|
1551
|
+
SimpleDateFormat(format, Locale.US).apply { isLenient = false }.parse(text)
|
|
1552
|
+
}.getOrNull()
|
|
1553
|
+
}?.let { parsed ->
|
|
1554
|
+
return Calendar.getInstance().apply { time = parsed }
|
|
1555
|
+
}
|
|
1556
|
+
return null
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private fun countdownSeconds(value: Any?): Int? {
|
|
1560
|
+
if (value is Number && value !is Boolean) return value.toDouble().coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1561
|
+
val text = value?.toString()?.trim().orEmpty()
|
|
1562
|
+
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1563
|
+
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
1564
|
+
val parts = text.split(':').mapNotNull { it.toDoubleOrNull() }
|
|
1565
|
+
if (parts.size != 2 && parts.size != 3) return null
|
|
1566
|
+
val hours = parts[0]
|
|
1567
|
+
val minutes = parts[1]
|
|
1568
|
+
val seconds = if (parts.size == 3) parts[2] else 0.0
|
|
1569
|
+
if (hours < 0 || minutes !in 0.0..<60.0 || seconds !in 0.0..<60.0) return null
|
|
1570
|
+
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1217
1574
|
private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
1218
1575
|
private var sourceUri = ""
|
|
1219
1576
|
private var shouldLoop = false
|
|
@@ -1437,6 +1794,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1437
1794
|
val inputIdentities = mutableMapOf<EditText, String>()
|
|
1438
1795
|
val suppressedTextChanges = mutableSetOf<EditText>()
|
|
1439
1796
|
val controls = mutableMapOf<String, View>()
|
|
1797
|
+
// Protocol ids identify mutation targets; authored keys identify the
|
|
1798
|
+
// retained view. Keep both mappings so a targeted update can recover
|
|
1799
|
+
// the original tree path without manufacturing a detached view.
|
|
1800
|
+
val nodeViews = mutableMapOf<String, View>()
|
|
1801
|
+
val nodePaths = mutableMapOf<String, String>()
|
|
1440
1802
|
val handlers = mutableMapOf<View, String>()
|
|
1441
1803
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
1442
1804
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
@@ -1520,6 +1882,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1520
1882
|
private val inputHintDefaults = mutableMapOf<EditText, ColorStateList>()
|
|
1521
1883
|
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
1522
1884
|
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
1885
|
+
private var keyboardInset = 0
|
|
1523
1886
|
|
|
1524
1887
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
1525
1888
|
applyConfiguredAppearance()
|
|
@@ -1531,7 +1894,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1531
1894
|
root = findViewById(R.id.native_root)
|
|
1532
1895
|
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
1533
1896
|
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
1897
|
+
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
1898
|
+
keyboardInset = max(0, ime.bottom - safe.bottom)
|
|
1534
1899
|
view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
|
|
1900
|
+
updateKeyboardAvoidingViews()
|
|
1535
1901
|
view.requestLayout()
|
|
1536
1902
|
view.post { screens.lastOrNull()?.takeIf { it.active }?.let(::emitLayoutEvents) }
|
|
1537
1903
|
insets
|
|
@@ -1926,9 +2292,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1926
2292
|
val target = screen.flatListOwners[id] ?: id
|
|
1927
2293
|
if (!applied.add(target)) continue
|
|
1928
2294
|
val document = screen.mutations.node(target) ?: return false
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2295
|
+
val path = screen.nodePaths[target] ?: return false
|
|
2296
|
+
if (screen.nodeViews[target] == null) return false
|
|
2297
|
+
makeView(screen, document, path, screen.controls)
|
|
2298
|
+
val updated = screen.nodeViews[target] ?: return false
|
|
1932
2299
|
screen.lastLayoutBounds.remove(updated)
|
|
1933
2300
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
1934
2301
|
}
|
|
@@ -1966,6 +2333,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
1966
2333
|
private fun updateParentLayoutStyle(view: View, style: JSONObject): Boolean {
|
|
1967
2334
|
var parent = view.parent
|
|
1968
2335
|
var found = false
|
|
2336
|
+
// A targeted style update can arrive while Android considers the
|
|
2337
|
+
// existing traversal settled. Invalidate the node itself as well as
|
|
2338
|
+
// every ancestor, including the platform ScrollView between the
|
|
2339
|
+
// native content layout and CraftNativeScrollView.
|
|
2340
|
+
view.forceLayout()
|
|
2341
|
+
view.requestLayout()
|
|
1969
2342
|
while (parent != null) {
|
|
1970
2343
|
when (parent) {
|
|
1971
2344
|
is CraftNativeFlexLayout -> {
|
|
@@ -1975,10 +2348,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
1975
2348
|
found = true
|
|
1976
2349
|
}
|
|
1977
2350
|
is CraftNativeScrollView -> {
|
|
1978
|
-
parent.
|
|
1979
|
-
parent.requestLayout()
|
|
2351
|
+
parent.invalidateContentLayout()
|
|
1980
2352
|
}
|
|
1981
2353
|
}
|
|
2354
|
+
(parent as? View)?.let {
|
|
2355
|
+
it.forceLayout()
|
|
2356
|
+
it.requestLayout()
|
|
2357
|
+
}
|
|
1982
2358
|
parent = parent.parent
|
|
1983
2359
|
}
|
|
1984
2360
|
return found
|
|
@@ -1987,6 +2363,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
1987
2363
|
private fun renderCommitted(screen: Screen, document: JSONObject) {
|
|
1988
2364
|
// Keep live controls (especially the focused EditText) across renders.
|
|
1989
2365
|
val next = mutableMapOf<String, View>()
|
|
2366
|
+
screen.nodeViews.clear()
|
|
2367
|
+
screen.nodePaths.clear()
|
|
1990
2368
|
val tree = makeView(screen, document, "root", next)
|
|
1991
2369
|
if (screen.view.getChildAt(0) !== tree) {
|
|
1992
2370
|
(tree.parent as? ViewGroup)?.removeView(tree)
|
|
@@ -1998,12 +2376,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
1998
2376
|
screen.controls.putAll(next)
|
|
1999
2377
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
2000
2378
|
screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
2379
|
+
// A full render can change a view's custom layout style without
|
|
2380
|
+
// changing the root bounds. Ensure native @layout observers still see
|
|
2381
|
+
// the settled child frame after the next traversal.
|
|
2382
|
+
scheduleLayoutEvents(screen)
|
|
2001
2383
|
}
|
|
2002
2384
|
|
|
2003
2385
|
private fun clearRenderedTree(screen: Screen) {
|
|
2004
2386
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
2005
2387
|
screen.view.removeAllViews()
|
|
2006
2388
|
screen.controls.clear()
|
|
2389
|
+
screen.nodeViews.clear()
|
|
2390
|
+
screen.nodePaths.clear()
|
|
2007
2391
|
screen.handlers.clear()
|
|
2008
2392
|
screen.flatListRows.clear()
|
|
2009
2393
|
screen.flatListOwners.clear()
|
|
@@ -2017,16 +2401,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
2017
2401
|
next: MutableMap<String, View>,
|
|
2018
2402
|
previousControls: Map<String, View> = screen.controls,
|
|
2019
2403
|
): View {
|
|
2020
|
-
val type = node.optString("type", "View")
|
|
2404
|
+
val type = canonicalType(node.optString("type", "View"))
|
|
2021
2405
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
2022
2406
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
2023
2407
|
val events = node.optJSONObject("events") ?: JSONObject()
|
|
2024
2408
|
val children = node.optJSONArray("children") ?: JSONArray()
|
|
2025
2409
|
val protocolId = node.optString("id").takeIf { it.isNotBlank() }
|
|
2026
|
-
val explicitKey =
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
?: explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
2410
|
+
val explicitKey = authoredKey(node, props)
|
|
2411
|
+
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
2412
|
+
?: protocolId?.let { "id:$it" }
|
|
2030
2413
|
?: path
|
|
2031
2414
|
val previous = previousControls[identity]
|
|
2032
2415
|
val reusableInput = previous as? EditText
|
|
@@ -2071,6 +2454,32 @@ class MainActivity : AppCompatActivity() {
|
|
|
2071
2454
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
2072
2455
|
}
|
|
2073
2456
|
}
|
|
2457
|
+
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
2458
|
+
text = props.optString("label").ifBlank { childText(children) }
|
|
2459
|
+
configureText(this, text.toString(), style)
|
|
2460
|
+
val requested = props.opt("checked") ?: props.opt("value")
|
|
2461
|
+
val checked = when (requested) {
|
|
2462
|
+
is Boolean -> requested
|
|
2463
|
+
is Number -> requested.toDouble() != 0.0
|
|
2464
|
+
is String -> requested.equals("true", ignoreCase = true) || requested == "1"
|
|
2465
|
+
else -> false
|
|
2466
|
+
}
|
|
2467
|
+
setOnCheckedChangeListener(null)
|
|
2468
|
+
// Keep the visual checked state separate from indeterminate:
|
|
2469
|
+
// an indeterminate control must announce "mixed" and its next
|
|
2470
|
+
// user toggle should transition to checked, not unchecked.
|
|
2471
|
+
isChecked = checked
|
|
2472
|
+
isActivated = props.optBoolean("indeterminate", false)
|
|
2473
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
2474
|
+
props.optString("color").takeIf { it.isNotBlank() }?.let { buttonTintList = ColorStateList.valueOf(color(it, defaultTextColor)) }
|
|
2475
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
2476
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
2477
|
+
setOnCheckedChangeListener { _, value ->
|
|
2478
|
+
screen.handlers[this]?.let { current ->
|
|
2479
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2074
2483
|
"TextInput" -> (reusableInput?.takeIf { it.isSingleLine == !props.optBoolean("multiline", false) }
|
|
2075
2484
|
?: EditText(this)).apply {
|
|
2076
2485
|
val defaultHintColors = inputHintDefaults.getOrPut(this) {
|
|
@@ -2213,6 +2622,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
2213
2622
|
setSelection(selected, false)
|
|
2214
2623
|
post { suppressInitialSelection = false }
|
|
2215
2624
|
}
|
|
2625
|
+
"DatePicker" -> (previous as? CraftNativeDatePickerView ?: CraftNativeDatePickerView(this)).apply {
|
|
2626
|
+
configure(props)
|
|
2627
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
2628
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
2629
|
+
onValueChanged = {
|
|
2630
|
+
screen.handlers[this]?.let { current ->
|
|
2631
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", event()))
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2216
2635
|
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
2217
2636
|
transparent = props.optBoolean("transparent", false)
|
|
2218
2637
|
content.configure(style, resources.displayMetrics.density)
|
|
@@ -2353,6 +2772,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2353
2772
|
indeterminateTintList = props.optString("color").takeIf { it.isNotBlank() }
|
|
2354
2773
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
2355
2774
|
}
|
|
2775
|
+
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
2776
|
+
configureIcon(this, props, style)
|
|
2777
|
+
}
|
|
2356
2778
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
2357
2779
|
scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
2358
2780
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
@@ -2371,6 +2793,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
2371
2793
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
2372
2794
|
updateImage(screen, this, props.opt("source"))
|
|
2373
2795
|
}
|
|
2796
|
+
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
2797
|
+
image.scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
2798
|
+
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
2799
|
+
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
2800
|
+
"center" -> ImageView.ScaleType.CENTER
|
|
2801
|
+
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
2802
|
+
}
|
|
2803
|
+
val onLoadStart = events.optString("onLoadStart")
|
|
2804
|
+
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
2805
|
+
val onLoad = events.optString("onLoad")
|
|
2806
|
+
if (onLoad.isNotBlank()) screen.imageLoadHandlers[image] = onLoad else screen.imageLoadHandlers.remove(image)
|
|
2807
|
+
val onLoadEnd = events.optString("onLoadEnd")
|
|
2808
|
+
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
2809
|
+
val onError = events.optString("onError")
|
|
2810
|
+
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
2811
|
+
updateImage(screen, image, props.opt("source"))
|
|
2812
|
+
configureContainer(content, style)
|
|
2813
|
+
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
2814
|
+
}
|
|
2374
2815
|
"Video" -> (previous as? CraftNativeVideoView ?: CraftNativeVideoView(this)).apply {
|
|
2375
2816
|
configure(
|
|
2376
2817
|
source = props.opt("source"),
|
|
@@ -2384,7 +2825,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2384
2825
|
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
2385
2826
|
drawing = node
|
|
2386
2827
|
}
|
|
2387
|
-
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2828
|
+
"ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2388
2829
|
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
2389
2830
|
setHorizontal(horizontal)
|
|
2390
2831
|
setNativeNestedScrollingEnabled(true)
|
|
@@ -2477,6 +2918,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
2477
2918
|
configureContainer(this, style)
|
|
2478
2919
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
2479
2920
|
}
|
|
2921
|
+
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
2922
|
+
configure(props, style, resources.displayMetrics.density)
|
|
2923
|
+
setKeyboardInset(keyboardInset)
|
|
2924
|
+
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
2925
|
+
}
|
|
2480
2926
|
else -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
2481
2927
|
configureContainer(this, style)
|
|
2482
2928
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
@@ -2484,12 +2930,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
2484
2930
|
}
|
|
2485
2931
|
if (control is CraftNativeFlexLayout) {
|
|
2486
2932
|
control.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2933
|
+
} else if (control is CraftNativeKeyboardAvoidingView) {
|
|
2934
|
+
control.content.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2487
2935
|
}
|
|
2488
2936
|
val pressInHandler = events.optString("onPressIn").takeIf { it.isNotBlank() }
|
|
2489
2937
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
2490
2938
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
2491
2939
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
2492
|
-
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator") {
|
|
2940
|
+
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView") {
|
|
2493
2941
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
2494
2942
|
val disabled = props.optBoolean("disabled", false)
|
|
2495
2943
|
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null
|
|
@@ -2571,6 +3019,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
2571
3019
|
if (!current.contains(message)) {
|
|
2572
3020
|
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
2573
3021
|
}
|
|
3022
|
+
} else if (control is CraftNativeImageBackgroundView) imageErrors[control.image]?.let { message ->
|
|
3023
|
+
val current = control.contentDescription?.toString().orEmpty()
|
|
3024
|
+
if (!current.contains(message)) {
|
|
3025
|
+
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
protocolId?.let {
|
|
3029
|
+
screen.nodeViews[it] = control
|
|
3030
|
+
screen.nodePaths[it] = path
|
|
2574
3031
|
}
|
|
2575
3032
|
next[identity] = control
|
|
2576
3033
|
applyViewStyle(control, style)
|
|
@@ -2609,6 +3066,48 @@ class MainActivity : AppCompatActivity() {
|
|
|
2609
3066
|
}
|
|
2610
3067
|
}
|
|
2611
3068
|
|
|
3069
|
+
private fun canonicalType(raw: String): String = when (raw.lowercase()) {
|
|
3070
|
+
"view" -> "View"
|
|
3071
|
+
"safeareaview" -> "SafeAreaView"
|
|
3072
|
+
"text" -> "Text"
|
|
3073
|
+
"button" -> "Button"
|
|
3074
|
+
"link" -> "Link"
|
|
3075
|
+
"checkbox" -> "Checkbox"
|
|
3076
|
+
"textinput", "input" -> "TextInput"
|
|
3077
|
+
"picker", "select" -> "Picker"
|
|
3078
|
+
"datepicker", "date-picker", "date" -> "DatePicker"
|
|
3079
|
+
"modal" -> "Modal"
|
|
3080
|
+
"switch" -> "Switch"
|
|
3081
|
+
"slider" -> "Slider"
|
|
3082
|
+
"activityindicator" -> "ActivityIndicator"
|
|
3083
|
+
"image" -> "Image"
|
|
3084
|
+
"imagebackground" -> "ImageBackground"
|
|
3085
|
+
"keyboardavoidingview" -> "KeyboardAvoidingView"
|
|
3086
|
+
"video" -> "Video"
|
|
3087
|
+
"icon" -> "Icon"
|
|
3088
|
+
"scrollview" -> "ScrollView"
|
|
3089
|
+
"sectionlist" -> "SectionList"
|
|
3090
|
+
"flatlist" -> "FlatList"
|
|
3091
|
+
"svg" -> "Svg"
|
|
3092
|
+
"touchableopacity" -> "TouchableOpacity"
|
|
3093
|
+
"touchablehighlight" -> "TouchableHighlight"
|
|
3094
|
+
"pressable" -> "Pressable"
|
|
3095
|
+
else -> raw
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
private fun authoredKey(node: JSONObject, props: JSONObject): String? {
|
|
3099
|
+
val direct = listOf(node.optString("key"), props.optString("key"), props.optString("testID"))
|
|
3100
|
+
.firstOrNull { it.isNotBlank() }
|
|
3101
|
+
if (direct != null) return direct
|
|
3102
|
+
val children = node.optJSONArray("children") ?: return null
|
|
3103
|
+
for (index in 0 until children.length()) {
|
|
3104
|
+
val child = children.optJSONObject(index) ?: continue
|
|
3105
|
+
val childProps = child.optJSONObject("props") ?: JSONObject()
|
|
3106
|
+
authoredKey(child, childProps)?.let { return it }
|
|
3107
|
+
}
|
|
3108
|
+
return node.optString("id").takeIf { it.isNotBlank() }
|
|
3109
|
+
}
|
|
3110
|
+
|
|
2612
3111
|
private fun configureContainer(layout: CraftNativeFlexLayout, style: JSONObject) {
|
|
2613
3112
|
layout.configure(style, resources.displayMetrics.density)
|
|
2614
3113
|
}
|
|
@@ -2793,6 +3292,59 @@ class MainActivity : AppCompatActivity() {
|
|
|
2793
3292
|
configureTextStyle(view, style)
|
|
2794
3293
|
}
|
|
2795
3294
|
|
|
3295
|
+
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
3296
|
+
val symbol = props.optString("symbol").ifBlank { props.optString("name") }
|
|
3297
|
+
view.text = androidIconGlyph(symbol)
|
|
3298
|
+
val size = style.optDouble("fontSize", 20.0).toFloat().coerceAtLeast(1f)
|
|
3299
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
3300
|
+
val foreground = style.optString("color").ifBlank { props.optString("color") }
|
|
3301
|
+
view.setTextColor(color(foreground, defaultTextColor))
|
|
3302
|
+
val weight = when (style.optString("fontWeight")) {
|
|
3303
|
+
"bold", "700", "800", "900" -> Typeface.BOLD
|
|
3304
|
+
else -> Typeface.NORMAL
|
|
3305
|
+
}
|
|
3306
|
+
view.typeface = Typeface.create("sans-serif", weight)
|
|
3307
|
+
if (!style.has("width") && !style.has("height")) {
|
|
3308
|
+
val box = dp(size.toDouble())
|
|
3309
|
+
view.minimumWidth = box
|
|
3310
|
+
view.minimumHeight = box
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
private fun androidIconGlyph(symbol: String): String = when (symbol) {
|
|
3315
|
+
"arrow.left" -> "←"
|
|
3316
|
+
"arrow.right" -> "→"
|
|
3317
|
+
"arrow.up" -> "↑"
|
|
3318
|
+
"arrow.down" -> "↓"
|
|
3319
|
+
"chevron.left" -> "‹"
|
|
3320
|
+
"chevron.right" -> "›"
|
|
3321
|
+
"chevron.up" -> "⌃"
|
|
3322
|
+
"chevron.down" -> "⌄"
|
|
3323
|
+
"plus" -> "+"
|
|
3324
|
+
"minus" -> "−"
|
|
3325
|
+
"xmark" -> "×"
|
|
3326
|
+
"checkmark" -> "✓"
|
|
3327
|
+
"house", "house.fill" -> "⌂"
|
|
3328
|
+
"magnifyingglass" -> "⌕"
|
|
3329
|
+
"gear", "gearshape" -> "⚙"
|
|
3330
|
+
"heart" -> "♡"
|
|
3331
|
+
"heart.fill" -> "♥"
|
|
3332
|
+
"star" -> "☆"
|
|
3333
|
+
"star.fill" -> "★"
|
|
3334
|
+
"sun.max" -> "☼"
|
|
3335
|
+
"moon" -> "☾"
|
|
3336
|
+
"info.circle", "info.circle.fill" -> "ⓘ"
|
|
3337
|
+
"questionmark.circle", "questionmark.circle.fill" -> "?"
|
|
3338
|
+
"exclamationmark.triangle", "exclamationmark.triangle.fill" -> "⚠"
|
|
3339
|
+
"play", "play.fill" -> "▶"
|
|
3340
|
+
"pause", "pause.fill" -> "Ⅱ"
|
|
3341
|
+
"stop", "stop.fill" -> "■"
|
|
3342
|
+
"camera" -> "▣"
|
|
3343
|
+
"circle" -> "○"
|
|
3344
|
+
"circle.fill" -> "●"
|
|
3345
|
+
else -> "○"
|
|
3346
|
+
}
|
|
3347
|
+
|
|
2796
3348
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
2797
3349
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
2798
3350
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize", 16.0).toFloat())
|
|
@@ -2939,7 +3491,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2939
3491
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
2940
3492
|
view.tag = fallbackLabel
|
|
2941
3493
|
val label = props.optString("accessibilityLabel").takeIf { it.isNotBlank() }
|
|
2942
|
-
?: props.optString("alt").takeIf { type == "Image" && it.isNotBlank() }
|
|
3494
|
+
?: props.optString("alt").takeIf { (type == "Image" || type == "ImageBackground") && it.isNotBlank() }
|
|
2943
3495
|
val value = props.opt("accessibilityValue")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
2944
3496
|
view.contentDescription = when {
|
|
2945
3497
|
label != null && value != null -> "$label, $value"
|
|
@@ -2948,14 +3500,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
2948
3500
|
else -> null
|
|
2949
3501
|
}
|
|
2950
3502
|
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
2951
|
-
val role = props.optString("accessibilityRole").ifBlank {
|
|
3503
|
+
val role = props.optString("accessibilityRole").ifBlank {
|
|
3504
|
+
if (type == "Icon") "image" else type.lowercase()
|
|
3505
|
+
}
|
|
2952
3506
|
val state = props.optJSONObject("accessibilityState")
|
|
2953
3507
|
val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
|
|
2954
|
-
if (type !in setOf("Button", "Link", "TextInput", "Picker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
3508
|
+
if (type !in setOf("Button", "Link", "Checkbox", "TextInput", "Picker", "DatePicker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
2955
3509
|
view.isEnabled = !disabled
|
|
2956
3510
|
} else if (disabled) {
|
|
2957
3511
|
view.isEnabled = false
|
|
2958
3512
|
}
|
|
3513
|
+
ViewCompat.setStateDescription(
|
|
3514
|
+
view,
|
|
3515
|
+
if (type == "Checkbox" && props.optBoolean("indeterminate", false)) "mixed" else null,
|
|
3516
|
+
)
|
|
2959
3517
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
2960
3518
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
2961
3519
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
@@ -3156,6 +3714,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3156
3714
|
emitLayoutEvents(screen, screen.view)
|
|
3157
3715
|
}
|
|
3158
3716
|
|
|
3717
|
+
private fun updateKeyboardAvoidingViews() {
|
|
3718
|
+
fun visit(view: View) {
|
|
3719
|
+
(view as? CraftNativeKeyboardAvoidingView)?.setKeyboardInset(keyboardInset)
|
|
3720
|
+
if (view is ViewGroup) for (index in 0 until view.childCount) visit(view.getChildAt(index))
|
|
3721
|
+
}
|
|
3722
|
+
if (::root.isInitialized) visit(root)
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3159
3725
|
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
3160
3726
|
emitLayoutEvent(screen, view, view.left, view.top, view.right, view.bottom)
|
|
3161
3727
|
if (view is ViewGroup) {
|
|
@@ -3213,8 +3779,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
3213
3779
|
}
|
|
3214
3780
|
}
|
|
3215
3781
|
|
|
3782
|
+
private fun imageViews(views: Collection<View>): Set<ImageView> = buildSet {
|
|
3783
|
+
fun collect(view: View) {
|
|
3784
|
+
if (view is ImageView) add(view)
|
|
3785
|
+
if (view is ViewGroup) for (index in 0 until view.childCount) collect(view.getChildAt(index))
|
|
3786
|
+
}
|
|
3787
|
+
views.forEach(::collect)
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3216
3790
|
private fun releaseImages(views: Collection<View>) {
|
|
3217
|
-
views
|
|
3791
|
+
imageViews(views).forEach { image ->
|
|
3218
3792
|
imageJobs.remove(image)?.cancel(true)
|
|
3219
3793
|
imageSources.remove(image)
|
|
3220
3794
|
imageErrors.remove(image)
|
|
@@ -3244,6 +3818,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3244
3818
|
preservingInputDrafts: Boolean = false,
|
|
3245
3819
|
) {
|
|
3246
3820
|
val released = views.toMutableSet()
|
|
3821
|
+
val releasedImages = imageViews(released)
|
|
3247
3822
|
val refreshableLists = released.filterIsInstance<CraftNativeRefreshableFlatList>()
|
|
3248
3823
|
val nestedLists = refreshableLists.map { it.list }.toSet()
|
|
3249
3824
|
released.addAll(nestedLists)
|
|
@@ -3256,6 +3831,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3256
3831
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
3257
3832
|
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
3258
3833
|
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
3834
|
+
released.filterIsInstance<CraftNativeKeyboardAvoidingView>().forEach { it.content.onLayoutChanged = null }
|
|
3259
3835
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|
|
3260
3836
|
releaseImages(released)
|
|
3261
3837
|
released.filterIsInstance<EditText>().forEach {
|
|
@@ -3287,10 +3863,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3287
3863
|
screen.blurHandlers.keys.removeAll(released)
|
|
3288
3864
|
screen.endEditingHandlers.keys.removeAll(released)
|
|
3289
3865
|
screen.submitHandlers.keys.removeAll(released)
|
|
3290
|
-
screen.imageLoadStartHandlers.keys.removeAll(
|
|
3291
|
-
screen.imageLoadHandlers.keys.removeAll(
|
|
3292
|
-
screen.imageLoadEndHandlers.keys.removeAll(
|
|
3293
|
-
screen.imageErrorHandlers.keys.removeAll(
|
|
3866
|
+
screen.imageLoadStartHandlers.keys.removeAll(releasedImages)
|
|
3867
|
+
screen.imageLoadHandlers.keys.removeAll(releasedImages)
|
|
3868
|
+
screen.imageLoadEndHandlers.keys.removeAll(releasedImages)
|
|
3869
|
+
screen.imageErrorHandlers.keys.removeAll(releasedImages)
|
|
3294
3870
|
screen.pressActiveOpacities.keys.removeAll(released)
|
|
3295
3871
|
screen.pressBaseOpacities.keys.removeAll(released)
|
|
3296
3872
|
screen.modalVisibility.keys.removeAll(released)
|
|
@@ -3304,6 +3880,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3304
3880
|
screen.pendingCapabilities.clear()
|
|
3305
3881
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
3306
3882
|
screen.controls.clear()
|
|
3883
|
+
screen.nodeViews.clear()
|
|
3884
|
+
screen.nodePaths.clear()
|
|
3307
3885
|
screen.handlers.clear()
|
|
3308
3886
|
screen.pressInHandlers.clear()
|
|
3309
3887
|
screen.pressOutHandlers.clear()
|
|
@@ -3341,6 +3919,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3341
3919
|
back()
|
|
3342
3920
|
}
|
|
3343
3921
|
|
|
3922
|
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
3923
|
+
if (::capabilities.isInitialized && capabilities.onActivityResult(requestCode, resultCode, data)) return
|
|
3924
|
+
super.onActivityResult(requestCode, resultCode, data)
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3344
3927
|
private fun showError(message: String) {
|
|
3345
3928
|
root.removeAllViews()
|
|
3346
3929
|
root.addView(TextView(this).apply {
|