craft-native 0.0.156 → 0.0.160
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 +24 -1
- package/dist/android/templates/CraftNativeCapabilities.kt.template +309 -4
- package/dist/android/templates/MainActivityNative.kt.template +606 -48
- package/dist/api/mobile.d.ts +3 -1
- 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 +23 -10
- package/dist/ios/src/testing.d.ts +7 -1
- package/dist/ios/templates/CraftHealth.swift +11 -0
- package/dist/ios/templates/CraftHybrid.swift +19 -0
- package/dist/ios/templates/CraftNativeScreen.swift +411 -46
- package/dist/ios/templates/testing/DriverTests.swift.template +161 -145
- 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
|
|
@@ -389,7 +395,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
389
395
|
private var columnGap = 0
|
|
390
396
|
private var grid = false
|
|
391
397
|
private var gridColumns = 1
|
|
392
|
-
private var gridAutoRows:
|
|
398
|
+
private var gridAutoRows: CraftNativeLength? = null
|
|
393
399
|
private var reverse = false
|
|
394
400
|
|
|
395
401
|
fun configure(style: JSONObject, density: Float) {
|
|
@@ -406,7 +412,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
406
412
|
gridColumns = craftNativeGridColumnCount(
|
|
407
413
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
408
414
|
)
|
|
409
|
-
gridAutoRows = style.
|
|
415
|
+
gridAutoRows = CraftNativeLength.from(style.opt("gridAutoRows"), density)
|
|
410
416
|
val padding = (style.optDouble("padding", 0.0) * density).toInt()
|
|
411
417
|
val horizontal = (style.optDouble("paddingHorizontal", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
412
418
|
val vertical = (style.optDouble("paddingVertical", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
@@ -534,7 +540,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
534
540
|
}
|
|
535
541
|
}
|
|
536
542
|
val rowHeights = rows.map { row ->
|
|
537
|
-
max(gridAutoRows ?: 0, row.maxOfOrNull { entry ->
|
|
543
|
+
max(gridAutoRows?.resolve(height - paddingTop - paddingBottom) ?: 0, row.maxOfOrNull { entry ->
|
|
538
544
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
539
545
|
entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
540
546
|
} ?: 0)
|
|
@@ -559,7 +565,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
559
565
|
val childHeight = measureChildHeight(child, style, height, verticalMargins)
|
|
560
566
|
val column = flowIndex % columns
|
|
561
567
|
val row = flowIndex / columns
|
|
562
|
-
while (rowHeights.size <= row) rowHeights += gridAutoRows ?: 0
|
|
568
|
+
while (rowHeights.size <= row) rowHeights += gridAutoRows?.resolve(null) ?: 0
|
|
563
569
|
columnWidths[column] = max(columnWidths[column], childWidth + horizontalMargins)
|
|
564
570
|
rowHeights[row] = max(rowHeights[row], childHeight + verticalMargins)
|
|
565
571
|
flowIndex += 1
|
|
@@ -578,11 +584,28 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
578
584
|
// previous fixed LayoutParams width after a reactive width is removed;
|
|
579
585
|
// using that value here prevents intrinsic sizing from being restored.
|
|
580
586
|
val explicit = style.width?.resolve(base) ?: flexBasis
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
587
|
+
if (explicit != null) {
|
|
588
|
+
child.measure(
|
|
589
|
+
View.MeasureSpec.makeMeasureSpec(explicit, View.MeasureSpec.EXACTLY),
|
|
590
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
591
|
+
)
|
|
592
|
+
return clamp(explicit, style.minWidth?.resolve(base), style.maxWidth?.resolve(base))
|
|
593
|
+
}
|
|
594
|
+
// Measure text and nested flex containers at max-content first. An
|
|
595
|
+
// AT_MOST spec equal to the available room can make Android report the
|
|
596
|
+
// longest-word (min-content) width for a wrapping TextView, collapsing
|
|
597
|
+
// shrink-to-fit bubbles and pills before the parent can clamp them.
|
|
598
|
+
val intrinsicSpec = if (child is TextView && child !is EditText) 1_000_000 else base
|
|
599
|
+
child.measure(
|
|
600
|
+
View.MeasureSpec.makeMeasureSpec(intrinsicSpec, View.MeasureSpec.AT_MOST),
|
|
601
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
602
|
+
)
|
|
603
|
+
val target = clamp(min(base, child.measuredWidth), style.minWidth?.resolve(base), style.maxWidth?.resolve(base))
|
|
604
|
+
child.measure(
|
|
605
|
+
View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.AT_MOST),
|
|
606
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
607
|
+
)
|
|
608
|
+
return target
|
|
586
609
|
}
|
|
587
610
|
|
|
588
611
|
private fun measureChildHeight(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
@@ -629,6 +652,68 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
629
652
|
else -> 0
|
|
630
653
|
}
|
|
631
654
|
|
|
655
|
+
/** Keep the old-runtime shim on the same bounded-flex water-fill rule as the Zig bridge. */
|
|
656
|
+
private fun fallbackFlexWidths(line: Line, available: Int, gap: Int): IntArray {
|
|
657
|
+
val horizontal = orientation == HORIZONTAL
|
|
658
|
+
val hypothetical = line.entries.sumOf { entry ->
|
|
659
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
660
|
+
val margins = if (horizontal) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
661
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
662
|
+
(if (horizontal) entry.width else entry.height) + margins
|
|
663
|
+
} + max(0, line.entries.size - 1) * gap
|
|
664
|
+
val free = available - hypothetical
|
|
665
|
+
val growTotal = line.entries.sumOf { it.style.flexGrow }
|
|
666
|
+
val shrinkTotal = line.entries.sumOf {
|
|
667
|
+
(it.style.flexShrink ?: defaultFlexShrink(it.view)) * (if (horizontal) it.width else it.height)
|
|
668
|
+
}
|
|
669
|
+
val growing = free > 0 && growTotal > 0
|
|
670
|
+
val shrinking = free < 0 && shrinkTotal > 0
|
|
671
|
+
val widths = IntArray(line.entries.size) { index -> if (horizontal) line.entries[index].width else line.entries[index].height }
|
|
672
|
+
if (!growing && !shrinking) return widths
|
|
673
|
+
|
|
674
|
+
val frozen = BooleanArray(widths.size)
|
|
675
|
+
repeat(widths.size + 1) {
|
|
676
|
+
var fixed = max(0, widths.size - 1) * gap.toDouble()
|
|
677
|
+
var activeBasis = 0.0
|
|
678
|
+
var factor = 0.0
|
|
679
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
680
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
681
|
+
val margins = if (horizontal) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
682
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
683
|
+
fixed += margins
|
|
684
|
+
if (frozen[index]) fixed += widths[index]
|
|
685
|
+
else {
|
|
686
|
+
val basis = if (horizontal) entry.width else entry.height
|
|
687
|
+
activeBasis += basis
|
|
688
|
+
factor += if (growing) entry.style.flexGrow
|
|
689
|
+
else (entry.style.flexShrink ?: defaultFlexShrink(entry.view)) * basis
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (factor <= 0) return@repeat
|
|
693
|
+
val remaining = available - fixed - activeBasis
|
|
694
|
+
var froze = false
|
|
695
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
696
|
+
if (frozen[index]) continue
|
|
697
|
+
val basis = (if (horizontal) entry.width else entry.height).toDouble()
|
|
698
|
+
val share = if (growing) remaining * entry.style.flexGrow / factor
|
|
699
|
+
else -remaining * (entry.style.flexShrink ?: defaultFlexShrink(entry.view)) * basis / factor
|
|
700
|
+
val raw = if (growing) basis + share else basis - share
|
|
701
|
+
val lower = (if (horizontal) entry.style.minWidth else entry.style.minHeight)?.resolve(if (horizontal) width - paddingLeft - paddingRight else height - paddingTop - paddingBottom)
|
|
702
|
+
val upper = (if (horizontal) entry.style.maxWidth else entry.style.maxHeight)?.resolve(if (horizontal) width - paddingLeft - paddingRight else height - paddingTop - paddingBottom)
|
|
703
|
+
var bounded = raw
|
|
704
|
+
if (lower != null) bounded = max(bounded, lower.toDouble())
|
|
705
|
+
if (upper != null) bounded = min(bounded, upper.toDouble())
|
|
706
|
+
widths[index] = bounded.roundToInt().coerceAtLeast(0)
|
|
707
|
+
if (bounded != raw) {
|
|
708
|
+
frozen[index] = true
|
|
709
|
+
froze = true
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (!froze) return@repeat
|
|
713
|
+
}
|
|
714
|
+
return widths
|
|
715
|
+
}
|
|
716
|
+
|
|
632
717
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
633
718
|
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
634
719
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -667,6 +752,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
667
752
|
val contentWidth = width - paddingLeft - paddingRight
|
|
668
753
|
val contentHeight = height - paddingTop - paddingBottom
|
|
669
754
|
val flow = layout.rows.flatten()
|
|
755
|
+
// Older runtimes return null for the optional symbol; the local
|
|
756
|
+
// layout below remains the compatibility path in that case.
|
|
670
757
|
val sharedFrames = CraftNative.grid(
|
|
671
758
|
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap.toFloat(), rowGap.toFloat(),
|
|
672
759
|
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
@@ -684,6 +771,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
684
771
|
val align = it.style.alignSelf ?: alignItems
|
|
685
772
|
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
686
773
|
}.toFloatArray(),
|
|
774
|
+
gridAutoRows?.resolve(contentHeight)?.toFloat(),
|
|
687
775
|
)
|
|
688
776
|
if (sharedFrames != null) {
|
|
689
777
|
for ((index, entry) in flow.withIndex()) {
|
|
@@ -770,7 +858,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
770
858
|
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + crossLeading
|
|
771
859
|
for ((lineIndex, line) in lines.withIndex()) {
|
|
772
860
|
val lineOffset = sharedCross?.first?.get(lineIndex)?.roundToInt()?.let { (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + it } ?: crossOffset
|
|
773
|
-
val
|
|
861
|
+
val fallbackWidths = fallbackFlexWidths(line, availableMain, gap)
|
|
774
862
|
val sharedPrimary = CraftNative.flexRow(
|
|
775
863
|
availableMain.toFloat(),
|
|
776
864
|
gap.toFloat(),
|
|
@@ -801,16 +889,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
801
889
|
sharedPrimary[index].roundToInt() + margins
|
|
802
890
|
} + max(0, line.entries.size - 1) * gap
|
|
803
891
|
} 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
892
|
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
|
-
}
|
|
893
|
+
val constrained = fallbackWidths[index]
|
|
814
894
|
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
815
895
|
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
816
896
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
@@ -851,17 +931,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
851
931
|
?: mainOffset + marginStart
|
|
852
932
|
val x = if (orientation == HORIZONTAL) positionedMain else lineOffset + crossPosition
|
|
853
933
|
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
|
-
}
|
|
934
|
+
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: fallbackWidths[index]
|
|
865
935
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
866
936
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
867
937
|
entry.view.layout(x, y, x + w, y + h)
|
|
@@ -890,6 +960,67 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
890
960
|
}
|
|
891
961
|
}
|
|
892
962
|
|
|
963
|
+
/** An image-backed flex container with ordinary native children over the image. */
|
|
964
|
+
private class CraftNativeImageBackgroundView(context: Context) : FrameLayout(context) {
|
|
965
|
+
val image = ImageView(context)
|
|
966
|
+
val content = CraftNativeFlexLayout(context)
|
|
967
|
+
|
|
968
|
+
init {
|
|
969
|
+
clipChildren = true
|
|
970
|
+
image.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
971
|
+
image.isClickable = false
|
|
972
|
+
addView(image, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
973
|
+
addView(content, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** A flex container that responds to the activity's IME inset. */
|
|
978
|
+
private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(context) {
|
|
979
|
+
val content = CraftNativeFlexLayout(context)
|
|
980
|
+
private var behavior = "padding"
|
|
981
|
+
private var keyboardVerticalOffset = 0
|
|
982
|
+
private var keyboardInset = 0
|
|
983
|
+
private var basePadding = intArrayOf(0, 0, 0, 0)
|
|
984
|
+
|
|
985
|
+
init {
|
|
986
|
+
addView(content, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
fun configure(props: JSONObject, style: JSONObject, density: Float) {
|
|
990
|
+
behavior = props.optString("behavior", "padding").takeIf { it in setOf("height", "position", "padding") } ?: "padding"
|
|
991
|
+
keyboardVerticalOffset = (props.optDouble("keyboardVerticalOffset", 0.0) * density).roundToInt()
|
|
992
|
+
content.configure(style, density)
|
|
993
|
+
basePadding = intArrayOf(content.paddingLeft, content.paddingTop, content.paddingRight, content.paddingBottom)
|
|
994
|
+
applyKeyboardInset()
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
fun setKeyboardInset(inset: Int) {
|
|
998
|
+
if (keyboardInset == inset) return
|
|
999
|
+
keyboardInset = max(0, inset)
|
|
1000
|
+
applyKeyboardInset()
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private fun effectiveInset(): Int = max(0, keyboardInset - keyboardVerticalOffset)
|
|
1004
|
+
|
|
1005
|
+
private fun applyKeyboardInset() {
|
|
1006
|
+
val inset = effectiveInset()
|
|
1007
|
+
translationY = if (behavior == "position") -inset.toFloat() else 0f
|
|
1008
|
+
val bottom = if (behavior == "padding") basePadding[3] + inset else basePadding[3]
|
|
1009
|
+
content.setPadding(basePadding[0], basePadding[1], basePadding[2], bottom)
|
|
1010
|
+
requestLayout()
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1014
|
+
val inset = effectiveInset()
|
|
1015
|
+
val adjustedHeight = if (behavior == "height" && inset > 0 && MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
|
|
1016
|
+
MeasureSpec.makeMeasureSpec(max(0, MeasureSpec.getSize(heightMeasureSpec) - inset), MeasureSpec.getMode(heightMeasureSpec))
|
|
1017
|
+
} else {
|
|
1018
|
+
heightMeasureSpec
|
|
1019
|
+
}
|
|
1020
|
+
super.onMeasure(widthMeasureSpec, adjustedHeight)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
893
1024
|
private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
|
|
894
1025
|
val content = CraftNativeFlexLayout(context)
|
|
895
1026
|
private val vertical = ScrollView(context)
|
|
@@ -939,6 +1070,21 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
939
1070
|
addView(scroller, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
940
1071
|
}
|
|
941
1072
|
|
|
1073
|
+
/**
|
|
1074
|
+
* A targeted mutation can change a child's size without reconciling the
|
|
1075
|
+
* scroll content tree. Invalidate both the custom content layout and the
|
|
1076
|
+
* platform scroller so its measured content range is recomputed too.
|
|
1077
|
+
*/
|
|
1078
|
+
fun invalidateContentLayout() {
|
|
1079
|
+
content.forceLayout()
|
|
1080
|
+
content.requestLayout()
|
|
1081
|
+
val scroller = if (horizontalMode == true) horizontal else vertical
|
|
1082
|
+
scroller.forceLayout()
|
|
1083
|
+
scroller.requestLayout()
|
|
1084
|
+
forceLayout()
|
|
1085
|
+
requestLayout()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
942
1088
|
fun setNativeNestedScrollingEnabled(value: Boolean) {
|
|
943
1089
|
vertical.isNestedScrollingEnabled = value
|
|
944
1090
|
horizontal.isNestedScrollingEnabled = value
|
|
@@ -1084,6 +1230,25 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1084
1230
|
|
|
1085
1231
|
private class CraftNativeFlexSpacer(context: Context) : View(context)
|
|
1086
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* A small, dependency-free native icon surface for Android.
|
|
1235
|
+
*
|
|
1236
|
+
* STX resolves Iconify names to the SF Symbol name that iOS draws. Android
|
|
1237
|
+
* does not ship SF Symbols, so the maintained host uses the corresponding
|
|
1238
|
+
* Unicode glyph for the common cross-platform symbols and a hollow circle for
|
|
1239
|
+
* an unknown symbol. Keeping this as a TextView gives the glyph the same
|
|
1240
|
+
* intrinsic measurement, tint, accessibility, and reconciliation behavior as
|
|
1241
|
+
* the other native leaf controls without adding an icon-font dependency.
|
|
1242
|
+
*/
|
|
1243
|
+
private class CraftNativeIconView(context: Context) : TextView(context) {
|
|
1244
|
+
init {
|
|
1245
|
+
includeFontPadding = false
|
|
1246
|
+
gravity = Gravity.CENTER
|
|
1247
|
+
setPadding(0, 0, 0, 0)
|
|
1248
|
+
isSingleLine = true
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1087
1252
|
private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
1088
1253
|
val content = CraftNativeFlexLayout(context)
|
|
1089
1254
|
private val blocker = View(context)
|
|
@@ -1214,6 +1379,176 @@ private class CraftNativeActivityIndicator(context: Context) :
|
|
|
1214
1379
|
}
|
|
1215
1380
|
}
|
|
1216
1381
|
|
|
1382
|
+
/** Android counterpart to iOS's UIDatePicker-backed native DatePicker. */
|
|
1383
|
+
private class CraftNativeDatePickerView(context: Context) : FrameLayout(context) {
|
|
1384
|
+
private enum class PickerMode { DATE, TIME, DATE_TIME, COUNTDOWN }
|
|
1385
|
+
private val maxCountdownSeconds = 23 * 3600 + 59 * 60 + 59
|
|
1386
|
+
|
|
1387
|
+
private var pickerMode: PickerMode? = null
|
|
1388
|
+
private var datePicker: DatePicker? = null
|
|
1389
|
+
private var timePicker: TimePicker? = null
|
|
1390
|
+
private var minuteInterval = 1
|
|
1391
|
+
private var suppressChanges = true
|
|
1392
|
+
var onValueChanged: (() -> Unit)? = null
|
|
1393
|
+
|
|
1394
|
+
init {
|
|
1395
|
+
isFocusable = true
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
fun configure(props: JSONObject) {
|
|
1399
|
+
val requestedMode = when (props.optString("mode")) {
|
|
1400
|
+
"time" -> PickerMode.TIME
|
|
1401
|
+
"datetime", "dateAndTime", "datetime-local" -> PickerMode.DATE_TIME
|
|
1402
|
+
"countdown" -> PickerMode.COUNTDOWN
|
|
1403
|
+
else -> PickerMode.DATE
|
|
1404
|
+
}
|
|
1405
|
+
if (pickerMode != requestedMode) {
|
|
1406
|
+
pickerMode = requestedMode
|
|
1407
|
+
datePicker = null
|
|
1408
|
+
timePicker = null
|
|
1409
|
+
removeAllViews()
|
|
1410
|
+
val content = LinearLayout(context).apply {
|
|
1411
|
+
orientation = LinearLayout.VERTICAL
|
|
1412
|
+
gravity = Gravity.CENTER
|
|
1413
|
+
}
|
|
1414
|
+
if (requestedMode == PickerMode.DATE || requestedMode == PickerMode.DATE_TIME) {
|
|
1415
|
+
datePicker = DatePicker(context).also { picker ->
|
|
1416
|
+
val today = Calendar.getInstance()
|
|
1417
|
+
picker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) { _, _, _, _ -> changed() }
|
|
1418
|
+
content.addView(picker, LinearLayout.LayoutParams(
|
|
1419
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1420
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1421
|
+
))
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (requestedMode != PickerMode.DATE) {
|
|
1425
|
+
timePicker = TimePicker(context).also { picker ->
|
|
1426
|
+
picker.setIs24HourView(true)
|
|
1427
|
+
picker.setOnTimeChangedListener { _, _, _ -> changed() }
|
|
1428
|
+
content.addView(picker, LinearLayout.LayoutParams(
|
|
1429
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1430
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1431
|
+
))
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
suppressChanges = true
|
|
1438
|
+
minuteInterval = props.optInt("minuteInterval", 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
|
|
1439
|
+
pickerDate(props.opt("minimumDate"))?.let { datePicker?.minDate = it.timeInMillis }
|
|
1440
|
+
pickerDate(props.opt("maximumDate"))?.let { datePicker?.maxDate = it.timeInMillis }
|
|
1441
|
+
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1442
|
+
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
1443
|
+
} else {
|
|
1444
|
+
pickerDate(props.opt("value"))?.let { applyDate(it) }
|
|
1445
|
+
}
|
|
1446
|
+
val enabled = !props.optBoolean("disabled", false)
|
|
1447
|
+
isEnabled = enabled
|
|
1448
|
+
datePicker?.isEnabled = enabled
|
|
1449
|
+
timePicker?.isEnabled = enabled
|
|
1450
|
+
for (index in 0 until childCount) getChildAt(index).isEnabled = enabled
|
|
1451
|
+
suppressChanges = false
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
fun event(): JSONObject {
|
|
1455
|
+
if (pickerMode == PickerMode.COUNTDOWN) {
|
|
1456
|
+
val seconds = currentCountdownSeconds()
|
|
1457
|
+
return JSONObject()
|
|
1458
|
+
.put("value", formatCountdown(seconds))
|
|
1459
|
+
.put("iso", "PT${seconds}S")
|
|
1460
|
+
.put("timestamp", seconds * 1000L)
|
|
1461
|
+
}
|
|
1462
|
+
val current = currentDate()
|
|
1463
|
+
val valueFormat = when (pickerMode) {
|
|
1464
|
+
PickerMode.DATE -> "yyyy-MM-dd"
|
|
1465
|
+
PickerMode.TIME -> "HH:mm"
|
|
1466
|
+
else -> "yyyy-MM-dd'T'HH:mm"
|
|
1467
|
+
}
|
|
1468
|
+
val value = SimpleDateFormat(valueFormat, Locale.US).apply { isLenient = false }.format(current.time)
|
|
1469
|
+
val iso = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(current.time)
|
|
1470
|
+
return JSONObject().put("value", value).put("iso", iso).put("timestamp", current.timeInMillis)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
private fun changed() {
|
|
1474
|
+
if (suppressChanges) return
|
|
1475
|
+
val picker = timePicker ?: return
|
|
1476
|
+
val snapped = ((picker.minute + minuteInterval / 2) / minuteInterval * minuteInterval).coerceAtMost(59)
|
|
1477
|
+
if (snapped != picker.minute) {
|
|
1478
|
+
suppressChanges = true
|
|
1479
|
+
picker.minute = snapped
|
|
1480
|
+
suppressChanges = false
|
|
1481
|
+
}
|
|
1482
|
+
onValueChanged?.invoke()
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private fun applyDate(value: Calendar) {
|
|
1486
|
+
datePicker?.updateDate(value.get(Calendar.YEAR), value.get(Calendar.MONTH), value.get(Calendar.DAY_OF_MONTH))
|
|
1487
|
+
timePicker?.hour = value.get(Calendar.HOUR_OF_DAY)
|
|
1488
|
+
timePicker?.minute = value.get(Calendar.MINUTE)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private fun applyCountdown(seconds: Int) {
|
|
1492
|
+
timePicker?.hour = (seconds / 3600).coerceIn(0, 23)
|
|
1493
|
+
timePicker?.minute = (seconds % 3600 / 60).coerceIn(0, 59)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private fun currentCountdownSeconds(): Int = timePicker?.let { it.hour * 3600 + it.minute * 60 } ?: 0
|
|
1497
|
+
|
|
1498
|
+
private fun formatCountdown(seconds: Int): String = "%02d:%02d:%02d".format(
|
|
1499
|
+
Locale.US,
|
|
1500
|
+
seconds / 3600,
|
|
1501
|
+
seconds % 3600 / 60,
|
|
1502
|
+
seconds % 60,
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
private fun currentDate(): Calendar = Calendar.getInstance().also { current ->
|
|
1506
|
+
datePicker?.let {
|
|
1507
|
+
current.set(Calendar.YEAR, it.year)
|
|
1508
|
+
current.set(Calendar.MONTH, it.month)
|
|
1509
|
+
current.set(Calendar.DAY_OF_MONTH, it.dayOfMonth)
|
|
1510
|
+
}
|
|
1511
|
+
timePicker?.let {
|
|
1512
|
+
current.set(Calendar.HOUR_OF_DAY, it.hour)
|
|
1513
|
+
current.set(Calendar.MINUTE, it.minute)
|
|
1514
|
+
current.set(Calendar.SECOND, 0)
|
|
1515
|
+
current.set(Calendar.MILLISECOND, 0)
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
private fun pickerDate(value: Any?): Calendar? {
|
|
1520
|
+
if (value is Number && value !is Boolean) return Calendar.getInstance().apply { timeInMillis = value.toLong() }
|
|
1521
|
+
val text = value?.toString()?.trim().orEmpty()
|
|
1522
|
+
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1523
|
+
val formats = listOf(
|
|
1524
|
+
"HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
|
|
1525
|
+
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
|
|
1526
|
+
)
|
|
1527
|
+
formats.firstNotNullOfOrNull { format ->
|
|
1528
|
+
runCatching {
|
|
1529
|
+
SimpleDateFormat(format, Locale.US).apply { isLenient = false }.parse(text)
|
|
1530
|
+
}.getOrNull()
|
|
1531
|
+
}?.let { parsed ->
|
|
1532
|
+
return Calendar.getInstance().apply { time = parsed }
|
|
1533
|
+
}
|
|
1534
|
+
return null
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
private fun countdownSeconds(value: Any?): Int? {
|
|
1538
|
+
if (value is Number && value !is Boolean) return value.toDouble().coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1539
|
+
val text = value?.toString()?.trim().orEmpty()
|
|
1540
|
+
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1541
|
+
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
1542
|
+
val parts = text.split(':').mapNotNull { it.toDoubleOrNull() }
|
|
1543
|
+
if (parts.size != 2 && parts.size != 3) return null
|
|
1544
|
+
val hours = parts[0]
|
|
1545
|
+
val minutes = parts[1]
|
|
1546
|
+
val seconds = if (parts.size == 3) parts[2] else 0.0
|
|
1547
|
+
if (hours < 0 || minutes !in 0.0..<60.0 || seconds !in 0.0..<60.0) return null
|
|
1548
|
+
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1217
1552
|
private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
1218
1553
|
private var sourceUri = ""
|
|
1219
1554
|
private var shouldLoop = false
|
|
@@ -1437,6 +1772,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1437
1772
|
val inputIdentities = mutableMapOf<EditText, String>()
|
|
1438
1773
|
val suppressedTextChanges = mutableSetOf<EditText>()
|
|
1439
1774
|
val controls = mutableMapOf<String, View>()
|
|
1775
|
+
// Protocol ids identify mutation targets; authored keys identify the
|
|
1776
|
+
// retained view. Keep both mappings so a targeted update can recover
|
|
1777
|
+
// the original tree path without manufacturing a detached view.
|
|
1778
|
+
val nodeViews = mutableMapOf<String, View>()
|
|
1779
|
+
val nodePaths = mutableMapOf<String, String>()
|
|
1440
1780
|
val handlers = mutableMapOf<View, String>()
|
|
1441
1781
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
1442
1782
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
@@ -1520,6 +1860,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1520
1860
|
private val inputHintDefaults = mutableMapOf<EditText, ColorStateList>()
|
|
1521
1861
|
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
1522
1862
|
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
1863
|
+
private var keyboardInset = 0
|
|
1523
1864
|
|
|
1524
1865
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
1525
1866
|
applyConfiguredAppearance()
|
|
@@ -1531,7 +1872,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1531
1872
|
root = findViewById(R.id.native_root)
|
|
1532
1873
|
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
1533
1874
|
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
1875
|
+
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
1876
|
+
keyboardInset = max(0, ime.bottom - safe.bottom)
|
|
1534
1877
|
view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
|
|
1878
|
+
updateKeyboardAvoidingViews()
|
|
1535
1879
|
view.requestLayout()
|
|
1536
1880
|
view.post { screens.lastOrNull()?.takeIf { it.active }?.let(::emitLayoutEvents) }
|
|
1537
1881
|
insets
|
|
@@ -1926,9 +2270,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
1926
2270
|
val target = screen.flatListOwners[id] ?: id
|
|
1927
2271
|
if (!applied.add(target)) continue
|
|
1928
2272
|
val document = screen.mutations.node(target) ?: return false
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2273
|
+
val path = screen.nodePaths[target] ?: return false
|
|
2274
|
+
if (screen.nodeViews[target] == null) return false
|
|
2275
|
+
makeView(screen, document, path, screen.controls)
|
|
2276
|
+
val updated = screen.nodeViews[target] ?: return false
|
|
1932
2277
|
screen.lastLayoutBounds.remove(updated)
|
|
1933
2278
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
1934
2279
|
}
|
|
@@ -1966,6 +2311,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
1966
2311
|
private fun updateParentLayoutStyle(view: View, style: JSONObject): Boolean {
|
|
1967
2312
|
var parent = view.parent
|
|
1968
2313
|
var found = false
|
|
2314
|
+
// A targeted style update can arrive while Android considers the
|
|
2315
|
+
// existing traversal settled. Invalidate the node itself as well as
|
|
2316
|
+
// every ancestor, including the platform ScrollView between the
|
|
2317
|
+
// native content layout and CraftNativeScrollView.
|
|
2318
|
+
view.forceLayout()
|
|
2319
|
+
view.requestLayout()
|
|
1969
2320
|
while (parent != null) {
|
|
1970
2321
|
when (parent) {
|
|
1971
2322
|
is CraftNativeFlexLayout -> {
|
|
@@ -1975,10 +2326,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
1975
2326
|
found = true
|
|
1976
2327
|
}
|
|
1977
2328
|
is CraftNativeScrollView -> {
|
|
1978
|
-
parent.
|
|
1979
|
-
parent.requestLayout()
|
|
2329
|
+
parent.invalidateContentLayout()
|
|
1980
2330
|
}
|
|
1981
2331
|
}
|
|
2332
|
+
(parent as? View)?.let {
|
|
2333
|
+
it.forceLayout()
|
|
2334
|
+
it.requestLayout()
|
|
2335
|
+
}
|
|
1982
2336
|
parent = parent.parent
|
|
1983
2337
|
}
|
|
1984
2338
|
return found
|
|
@@ -1987,6 +2341,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
1987
2341
|
private fun renderCommitted(screen: Screen, document: JSONObject) {
|
|
1988
2342
|
// Keep live controls (especially the focused EditText) across renders.
|
|
1989
2343
|
val next = mutableMapOf<String, View>()
|
|
2344
|
+
screen.nodeViews.clear()
|
|
2345
|
+
screen.nodePaths.clear()
|
|
1990
2346
|
val tree = makeView(screen, document, "root", next)
|
|
1991
2347
|
if (screen.view.getChildAt(0) !== tree) {
|
|
1992
2348
|
(tree.parent as? ViewGroup)?.removeView(tree)
|
|
@@ -1998,12 +2354,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
1998
2354
|
screen.controls.putAll(next)
|
|
1999
2355
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
2000
2356
|
screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
2357
|
+
// A full render can change a view's custom layout style without
|
|
2358
|
+
// changing the root bounds. Ensure native @layout observers still see
|
|
2359
|
+
// the settled child frame after the next traversal.
|
|
2360
|
+
scheduleLayoutEvents(screen)
|
|
2001
2361
|
}
|
|
2002
2362
|
|
|
2003
2363
|
private fun clearRenderedTree(screen: Screen) {
|
|
2004
2364
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
2005
2365
|
screen.view.removeAllViews()
|
|
2006
2366
|
screen.controls.clear()
|
|
2367
|
+
screen.nodeViews.clear()
|
|
2368
|
+
screen.nodePaths.clear()
|
|
2007
2369
|
screen.handlers.clear()
|
|
2008
2370
|
screen.flatListRows.clear()
|
|
2009
2371
|
screen.flatListOwners.clear()
|
|
@@ -2017,16 +2379,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
2017
2379
|
next: MutableMap<String, View>,
|
|
2018
2380
|
previousControls: Map<String, View> = screen.controls,
|
|
2019
2381
|
): View {
|
|
2020
|
-
val type = node.optString("type", "View")
|
|
2382
|
+
val type = canonicalType(node.optString("type", "View"))
|
|
2021
2383
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
2022
2384
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
2023
2385
|
val events = node.optJSONObject("events") ?: JSONObject()
|
|
2024
2386
|
val children = node.optJSONArray("children") ?: JSONArray()
|
|
2025
2387
|
val protocolId = node.optString("id").takeIf { it.isNotBlank() }
|
|
2026
|
-
val explicitKey =
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
?: explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
2388
|
+
val explicitKey = authoredKey(node, props)
|
|
2389
|
+
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
2390
|
+
?: protocolId?.let { "id:$it" }
|
|
2030
2391
|
?: path
|
|
2031
2392
|
val previous = previousControls[identity]
|
|
2032
2393
|
val reusableInput = previous as? EditText
|
|
@@ -2071,6 +2432,32 @@ class MainActivity : AppCompatActivity() {
|
|
|
2071
2432
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
2072
2433
|
}
|
|
2073
2434
|
}
|
|
2435
|
+
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
2436
|
+
text = props.optString("label").ifBlank { childText(children) }
|
|
2437
|
+
configureText(this, text.toString(), style)
|
|
2438
|
+
val requested = props.opt("checked") ?: props.opt("value")
|
|
2439
|
+
val checked = when (requested) {
|
|
2440
|
+
is Boolean -> requested
|
|
2441
|
+
is Number -> requested.toDouble() != 0.0
|
|
2442
|
+
is String -> requested.equals("true", ignoreCase = true) || requested == "1"
|
|
2443
|
+
else -> false
|
|
2444
|
+
}
|
|
2445
|
+
setOnCheckedChangeListener(null)
|
|
2446
|
+
// Keep the visual checked state separate from indeterminate:
|
|
2447
|
+
// an indeterminate control must announce "mixed" and its next
|
|
2448
|
+
// user toggle should transition to checked, not unchecked.
|
|
2449
|
+
isChecked = checked
|
|
2450
|
+
isActivated = props.optBoolean("indeterminate", false)
|
|
2451
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
2452
|
+
props.optString("color").takeIf { it.isNotBlank() }?.let { buttonTintList = ColorStateList.valueOf(color(it, defaultTextColor)) }
|
|
2453
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
2454
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
2455
|
+
setOnCheckedChangeListener { _, value ->
|
|
2456
|
+
screen.handlers[this]?.let { current ->
|
|
2457
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2074
2461
|
"TextInput" -> (reusableInput?.takeIf { it.isSingleLine == !props.optBoolean("multiline", false) }
|
|
2075
2462
|
?: EditText(this)).apply {
|
|
2076
2463
|
val defaultHintColors = inputHintDefaults.getOrPut(this) {
|
|
@@ -2213,6 +2600,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
2213
2600
|
setSelection(selected, false)
|
|
2214
2601
|
post { suppressInitialSelection = false }
|
|
2215
2602
|
}
|
|
2603
|
+
"DatePicker" -> (previous as? CraftNativeDatePickerView ?: CraftNativeDatePickerView(this)).apply {
|
|
2604
|
+
configure(props)
|
|
2605
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
2606
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
2607
|
+
onValueChanged = {
|
|
2608
|
+
screen.handlers[this]?.let { current ->
|
|
2609
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", event()))
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2216
2613
|
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
2217
2614
|
transparent = props.optBoolean("transparent", false)
|
|
2218
2615
|
content.configure(style, resources.displayMetrics.density)
|
|
@@ -2353,6 +2750,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
2353
2750
|
indeterminateTintList = props.optString("color").takeIf { it.isNotBlank() }
|
|
2354
2751
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
2355
2752
|
}
|
|
2753
|
+
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
2754
|
+
configureIcon(this, props, style)
|
|
2755
|
+
}
|
|
2356
2756
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
2357
2757
|
scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
2358
2758
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
@@ -2371,6 +2771,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
2371
2771
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
2372
2772
|
updateImage(screen, this, props.opt("source"))
|
|
2373
2773
|
}
|
|
2774
|
+
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
2775
|
+
image.scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
2776
|
+
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
2777
|
+
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
2778
|
+
"center" -> ImageView.ScaleType.CENTER
|
|
2779
|
+
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
2780
|
+
}
|
|
2781
|
+
val onLoadStart = events.optString("onLoadStart")
|
|
2782
|
+
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
2783
|
+
val onLoad = events.optString("onLoad")
|
|
2784
|
+
if (onLoad.isNotBlank()) screen.imageLoadHandlers[image] = onLoad else screen.imageLoadHandlers.remove(image)
|
|
2785
|
+
val onLoadEnd = events.optString("onLoadEnd")
|
|
2786
|
+
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
2787
|
+
val onError = events.optString("onError")
|
|
2788
|
+
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
2789
|
+
updateImage(screen, image, props.opt("source"))
|
|
2790
|
+
configureContainer(content, style)
|
|
2791
|
+
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
2792
|
+
}
|
|
2374
2793
|
"Video" -> (previous as? CraftNativeVideoView ?: CraftNativeVideoView(this)).apply {
|
|
2375
2794
|
configure(
|
|
2376
2795
|
source = props.opt("source"),
|
|
@@ -2477,6 +2896,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
2477
2896
|
configureContainer(this, style)
|
|
2478
2897
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
2479
2898
|
}
|
|
2899
|
+
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
2900
|
+
configure(props, style, resources.displayMetrics.density)
|
|
2901
|
+
setKeyboardInset(keyboardInset)
|
|
2902
|
+
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
2903
|
+
}
|
|
2480
2904
|
else -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
2481
2905
|
configureContainer(this, style)
|
|
2482
2906
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
@@ -2484,12 +2908,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
2484
2908
|
}
|
|
2485
2909
|
if (control is CraftNativeFlexLayout) {
|
|
2486
2910
|
control.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2911
|
+
} else if (control is CraftNativeKeyboardAvoidingView) {
|
|
2912
|
+
control.content.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2487
2913
|
}
|
|
2488
2914
|
val pressInHandler = events.optString("onPressIn").takeIf { it.isNotBlank() }
|
|
2489
2915
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
2490
2916
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
2491
2917
|
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") {
|
|
2918
|
+
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView") {
|
|
2493
2919
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
2494
2920
|
val disabled = props.optBoolean("disabled", false)
|
|
2495
2921
|
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null
|
|
@@ -2571,6 +2997,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
2571
2997
|
if (!current.contains(message)) {
|
|
2572
2998
|
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
2573
2999
|
}
|
|
3000
|
+
} else if (control is CraftNativeImageBackgroundView) imageErrors[control.image]?.let { message ->
|
|
3001
|
+
val current = control.contentDescription?.toString().orEmpty()
|
|
3002
|
+
if (!current.contains(message)) {
|
|
3003
|
+
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
protocolId?.let {
|
|
3007
|
+
screen.nodeViews[it] = control
|
|
3008
|
+
screen.nodePaths[it] = path
|
|
2574
3009
|
}
|
|
2575
3010
|
next[identity] = control
|
|
2576
3011
|
applyViewStyle(control, style)
|
|
@@ -2609,6 +3044,47 @@ class MainActivity : AppCompatActivity() {
|
|
|
2609
3044
|
}
|
|
2610
3045
|
}
|
|
2611
3046
|
|
|
3047
|
+
private fun canonicalType(raw: String): String = when (raw.lowercase()) {
|
|
3048
|
+
"view" -> "View"
|
|
3049
|
+
"safeareaview" -> "SafeAreaView"
|
|
3050
|
+
"text" -> "Text"
|
|
3051
|
+
"button" -> "Button"
|
|
3052
|
+
"link" -> "Link"
|
|
3053
|
+
"checkbox" -> "Checkbox"
|
|
3054
|
+
"textinput", "input" -> "TextInput"
|
|
3055
|
+
"picker", "select" -> "Picker"
|
|
3056
|
+
"datepicker", "date-picker", "date" -> "DatePicker"
|
|
3057
|
+
"modal" -> "Modal"
|
|
3058
|
+
"switch" -> "Switch"
|
|
3059
|
+
"slider" -> "Slider"
|
|
3060
|
+
"activityindicator" -> "ActivityIndicator"
|
|
3061
|
+
"image" -> "Image"
|
|
3062
|
+
"imagebackground" -> "ImageBackground"
|
|
3063
|
+
"keyboardavoidingview" -> "KeyboardAvoidingView"
|
|
3064
|
+
"video" -> "Video"
|
|
3065
|
+
"icon" -> "Icon"
|
|
3066
|
+
"scrollview" -> "ScrollView"
|
|
3067
|
+
"flatlist" -> "FlatList"
|
|
3068
|
+
"svg" -> "Svg"
|
|
3069
|
+
"touchableopacity" -> "TouchableOpacity"
|
|
3070
|
+
"touchablehighlight" -> "TouchableHighlight"
|
|
3071
|
+
"pressable" -> "Pressable"
|
|
3072
|
+
else -> raw
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
private fun authoredKey(node: JSONObject, props: JSONObject): String? {
|
|
3076
|
+
val direct = listOf(node.optString("key"), props.optString("key"), props.optString("testID"))
|
|
3077
|
+
.firstOrNull { it.isNotBlank() }
|
|
3078
|
+
if (direct != null) return direct
|
|
3079
|
+
val children = node.optJSONArray("children") ?: return null
|
|
3080
|
+
for (index in 0 until children.length()) {
|
|
3081
|
+
val child = children.optJSONObject(index) ?: continue
|
|
3082
|
+
val childProps = child.optJSONObject("props") ?: JSONObject()
|
|
3083
|
+
authoredKey(child, childProps)?.let { return it }
|
|
3084
|
+
}
|
|
3085
|
+
return node.optString("id").takeIf { it.isNotBlank() }
|
|
3086
|
+
}
|
|
3087
|
+
|
|
2612
3088
|
private fun configureContainer(layout: CraftNativeFlexLayout, style: JSONObject) {
|
|
2613
3089
|
layout.configure(style, resources.displayMetrics.density)
|
|
2614
3090
|
}
|
|
@@ -2793,6 +3269,59 @@ class MainActivity : AppCompatActivity() {
|
|
|
2793
3269
|
configureTextStyle(view, style)
|
|
2794
3270
|
}
|
|
2795
3271
|
|
|
3272
|
+
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
3273
|
+
val symbol = props.optString("symbol").ifBlank { props.optString("name") }
|
|
3274
|
+
view.text = androidIconGlyph(symbol)
|
|
3275
|
+
val size = style.optDouble("fontSize", 20.0).toFloat().coerceAtLeast(1f)
|
|
3276
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
3277
|
+
val foreground = style.optString("color").ifBlank { props.optString("color") }
|
|
3278
|
+
view.setTextColor(color(foreground, defaultTextColor))
|
|
3279
|
+
val weight = when (style.optString("fontWeight")) {
|
|
3280
|
+
"bold", "700", "800", "900" -> Typeface.BOLD
|
|
3281
|
+
else -> Typeface.NORMAL
|
|
3282
|
+
}
|
|
3283
|
+
view.typeface = Typeface.create("sans-serif", weight)
|
|
3284
|
+
if (!style.has("width") && !style.has("height")) {
|
|
3285
|
+
val box = dp(size.toDouble())
|
|
3286
|
+
view.minimumWidth = box
|
|
3287
|
+
view.minimumHeight = box
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
private fun androidIconGlyph(symbol: String): String = when (symbol) {
|
|
3292
|
+
"arrow.left" -> "←"
|
|
3293
|
+
"arrow.right" -> "→"
|
|
3294
|
+
"arrow.up" -> "↑"
|
|
3295
|
+
"arrow.down" -> "↓"
|
|
3296
|
+
"chevron.left" -> "‹"
|
|
3297
|
+
"chevron.right" -> "›"
|
|
3298
|
+
"chevron.up" -> "⌃"
|
|
3299
|
+
"chevron.down" -> "⌄"
|
|
3300
|
+
"plus" -> "+"
|
|
3301
|
+
"minus" -> "−"
|
|
3302
|
+
"xmark" -> "×"
|
|
3303
|
+
"checkmark" -> "✓"
|
|
3304
|
+
"house", "house.fill" -> "⌂"
|
|
3305
|
+
"magnifyingglass" -> "⌕"
|
|
3306
|
+
"gear", "gearshape" -> "⚙"
|
|
3307
|
+
"heart" -> "♡"
|
|
3308
|
+
"heart.fill" -> "♥"
|
|
3309
|
+
"star" -> "☆"
|
|
3310
|
+
"star.fill" -> "★"
|
|
3311
|
+
"sun.max" -> "☼"
|
|
3312
|
+
"moon" -> "☾"
|
|
3313
|
+
"info.circle", "info.circle.fill" -> "ⓘ"
|
|
3314
|
+
"questionmark.circle", "questionmark.circle.fill" -> "?"
|
|
3315
|
+
"exclamationmark.triangle", "exclamationmark.triangle.fill" -> "⚠"
|
|
3316
|
+
"play", "play.fill" -> "▶"
|
|
3317
|
+
"pause", "pause.fill" -> "Ⅱ"
|
|
3318
|
+
"stop", "stop.fill" -> "■"
|
|
3319
|
+
"camera" -> "▣"
|
|
3320
|
+
"circle" -> "○"
|
|
3321
|
+
"circle.fill" -> "●"
|
|
3322
|
+
else -> "○"
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2796
3325
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
2797
3326
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
2798
3327
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize", 16.0).toFloat())
|
|
@@ -2939,7 +3468,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2939
3468
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
2940
3469
|
view.tag = fallbackLabel
|
|
2941
3470
|
val label = props.optString("accessibilityLabel").takeIf { it.isNotBlank() }
|
|
2942
|
-
?: props.optString("alt").takeIf { type == "Image" && it.isNotBlank() }
|
|
3471
|
+
?: props.optString("alt").takeIf { (type == "Image" || type == "ImageBackground") && it.isNotBlank() }
|
|
2943
3472
|
val value = props.opt("accessibilityValue")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
2944
3473
|
view.contentDescription = when {
|
|
2945
3474
|
label != null && value != null -> "$label, $value"
|
|
@@ -2951,11 +3480,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
2951
3480
|
val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
|
|
2952
3481
|
val state = props.optJSONObject("accessibilityState")
|
|
2953
3482
|
val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
|
|
2954
|
-
if (type !in setOf("Button", "Link", "TextInput", "Picker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
3483
|
+
if (type !in setOf("Button", "Link", "Checkbox", "TextInput", "Picker", "DatePicker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
2955
3484
|
view.isEnabled = !disabled
|
|
2956
3485
|
} else if (disabled) {
|
|
2957
3486
|
view.isEnabled = false
|
|
2958
3487
|
}
|
|
3488
|
+
ViewCompat.setStateDescription(
|
|
3489
|
+
view,
|
|
3490
|
+
if (type == "Checkbox" && props.optBoolean("indeterminate", false)) "mixed" else null,
|
|
3491
|
+
)
|
|
2959
3492
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
2960
3493
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
2961
3494
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
@@ -3156,6 +3689,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3156
3689
|
emitLayoutEvents(screen, screen.view)
|
|
3157
3690
|
}
|
|
3158
3691
|
|
|
3692
|
+
private fun updateKeyboardAvoidingViews() {
|
|
3693
|
+
fun visit(view: View) {
|
|
3694
|
+
(view as? CraftNativeKeyboardAvoidingView)?.setKeyboardInset(keyboardInset)
|
|
3695
|
+
if (view is ViewGroup) for (index in 0 until view.childCount) visit(view.getChildAt(index))
|
|
3696
|
+
}
|
|
3697
|
+
if (::root.isInitialized) visit(root)
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3159
3700
|
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
3160
3701
|
emitLayoutEvent(screen, view, view.left, view.top, view.right, view.bottom)
|
|
3161
3702
|
if (view is ViewGroup) {
|
|
@@ -3213,8 +3754,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
3213
3754
|
}
|
|
3214
3755
|
}
|
|
3215
3756
|
|
|
3757
|
+
private fun imageViews(views: Collection<View>): Set<ImageView> = buildSet {
|
|
3758
|
+
fun collect(view: View) {
|
|
3759
|
+
if (view is ImageView) add(view)
|
|
3760
|
+
if (view is ViewGroup) for (index in 0 until view.childCount) collect(view.getChildAt(index))
|
|
3761
|
+
}
|
|
3762
|
+
views.forEach(::collect)
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3216
3765
|
private fun releaseImages(views: Collection<View>) {
|
|
3217
|
-
views
|
|
3766
|
+
imageViews(views).forEach { image ->
|
|
3218
3767
|
imageJobs.remove(image)?.cancel(true)
|
|
3219
3768
|
imageSources.remove(image)
|
|
3220
3769
|
imageErrors.remove(image)
|
|
@@ -3244,6 +3793,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3244
3793
|
preservingInputDrafts: Boolean = false,
|
|
3245
3794
|
) {
|
|
3246
3795
|
val released = views.toMutableSet()
|
|
3796
|
+
val releasedImages = imageViews(released)
|
|
3247
3797
|
val refreshableLists = released.filterIsInstance<CraftNativeRefreshableFlatList>()
|
|
3248
3798
|
val nestedLists = refreshableLists.map { it.list }.toSet()
|
|
3249
3799
|
released.addAll(nestedLists)
|
|
@@ -3256,6 +3806,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3256
3806
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
3257
3807
|
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
3258
3808
|
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
3809
|
+
released.filterIsInstance<CraftNativeKeyboardAvoidingView>().forEach { it.content.onLayoutChanged = null }
|
|
3259
3810
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|
|
3260
3811
|
releaseImages(released)
|
|
3261
3812
|
released.filterIsInstance<EditText>().forEach {
|
|
@@ -3287,10 +3838,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3287
3838
|
screen.blurHandlers.keys.removeAll(released)
|
|
3288
3839
|
screen.endEditingHandlers.keys.removeAll(released)
|
|
3289
3840
|
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(
|
|
3841
|
+
screen.imageLoadStartHandlers.keys.removeAll(releasedImages)
|
|
3842
|
+
screen.imageLoadHandlers.keys.removeAll(releasedImages)
|
|
3843
|
+
screen.imageLoadEndHandlers.keys.removeAll(releasedImages)
|
|
3844
|
+
screen.imageErrorHandlers.keys.removeAll(releasedImages)
|
|
3294
3845
|
screen.pressActiveOpacities.keys.removeAll(released)
|
|
3295
3846
|
screen.pressBaseOpacities.keys.removeAll(released)
|
|
3296
3847
|
screen.modalVisibility.keys.removeAll(released)
|
|
@@ -3304,6 +3855,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3304
3855
|
screen.pendingCapabilities.clear()
|
|
3305
3856
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
3306
3857
|
screen.controls.clear()
|
|
3858
|
+
screen.nodeViews.clear()
|
|
3859
|
+
screen.nodePaths.clear()
|
|
3307
3860
|
screen.handlers.clear()
|
|
3308
3861
|
screen.pressInHandlers.clear()
|
|
3309
3862
|
screen.pressOutHandlers.clear()
|
|
@@ -3341,6 +3894,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3341
3894
|
back()
|
|
3342
3895
|
}
|
|
3343
3896
|
|
|
3897
|
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
3898
|
+
if (::capabilities.isInitialized && capabilities.onActivityResult(requestCode, resultCode, data)) return
|
|
3899
|
+
super.onActivityResult(requestCode, resultCode, data)
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3344
3902
|
private fun showError(message: String) {
|
|
3345
3903
|
root.removeAllViews()
|
|
3346
3904
|
root.addView(TextView(this).apply {
|