@craft-native/android 0.0.189 → 0.0.191
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/package.json
CHANGED
|
@@ -576,6 +576,9 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
576
576
|
private val buttons = LinearLayout(context)
|
|
577
577
|
private val tabBar = LinearLayout(context)
|
|
578
578
|
private val tabViews = mutableListOf<Pair<TextView, TextView>>()
|
|
579
|
+
private val tabIconText = mutableMapOf<String, String>()
|
|
580
|
+
private val tabImageURLs = mutableMapOf<String, String>()
|
|
581
|
+
private val tabImages = mutableMapOf<String, android.graphics.drawable.Drawable>()
|
|
579
582
|
private val density = resources.displayMetrics.density
|
|
580
583
|
private var selectedTab = 0
|
|
581
584
|
private var swipeTracking = false
|
|
@@ -703,6 +706,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
703
706
|
|
|
704
707
|
/** Draws `options` (a screen's `setOptions`) for a screen `depth` deep in its stack. */
|
|
705
708
|
fun apply(options: JSONObject, depth: Int, fallbackTitle: String?) {
|
|
709
|
+
options.optJSONObject("tabItem")?.let(::updateTab)
|
|
706
710
|
val hidden = options.optBoolean("hidden", false)
|
|
707
711
|
val large = options.optBoolean("largeTitle", false) && depth == 0
|
|
708
712
|
val title = options.optString("title").takeIf { options.has("title") } ?: fallbackTitle.orEmpty()
|
|
@@ -728,6 +732,42 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
728
732
|
refreshColors()
|
|
729
733
|
}
|
|
730
734
|
|
|
735
|
+
private fun updateTab(item: JSONObject) {
|
|
736
|
+
val id = item.optString("id")
|
|
737
|
+
val index = tabs.indexOfFirst { it.id == id }
|
|
738
|
+
if (index < 0) return
|
|
739
|
+
val cell = tabBar.getChildAt(index)
|
|
740
|
+
if (item.has("accessibilityLabel")) cell.contentDescription = item.optString("accessibilityLabel").takeUnless { item.isNull("accessibilityLabel") || it.isBlank() } ?: tabs[index].title
|
|
741
|
+
if (item.has("iconText")) {
|
|
742
|
+
tabIconText.remove(id)
|
|
743
|
+
item.optString("iconText").takeUnless { item.isNull("iconText") || it.isBlank() }?.let { tabIconText[id] = it.take(8) }
|
|
744
|
+
}
|
|
745
|
+
if (item.has("image")) {
|
|
746
|
+
val address = item.optString("image").takeIf { !item.isNull("image") && it.startsWith("https://") }
|
|
747
|
+
if (address != tabImageURLs[id]) {
|
|
748
|
+
tabImages.remove(id)
|
|
749
|
+
tabImageURLs.remove(id)
|
|
750
|
+
if (address != null) {
|
|
751
|
+
tabImageURLs[id] = address
|
|
752
|
+
Thread {
|
|
753
|
+
val image = runCatching {
|
|
754
|
+
val connection = java.net.URL(address).openConnection().apply { connectTimeout = 5000; readTimeout = 5000 }
|
|
755
|
+
val bitmap = connection.getInputStream().use { android.graphics.BitmapFactory.decodeStream(it) } ?: return@runCatching null
|
|
756
|
+
androidx.core.graphics.drawable.RoundedBitmapDrawableFactory.create(resources, android.graphics.Bitmap.createScaledBitmap(bitmap, dp(27), dp(27), true)).apply { isCircular = true }
|
|
757
|
+
}.getOrNull()
|
|
758
|
+
post {
|
|
759
|
+
if (tabImageURLs[id] == address && image != null) {
|
|
760
|
+
tabImages[id] = image
|
|
761
|
+
refreshColors()
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}.start()
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
refreshColors()
|
|
769
|
+
}
|
|
770
|
+
|
|
731
771
|
/** Light or dark changed: the bars follow the screen's background. */
|
|
732
772
|
fun refreshColors() {
|
|
733
773
|
val background = ContextCompat.getColor(context, R.color.background)
|
|
@@ -749,11 +789,12 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
749
789
|
)).apply { setLayerInset(1, 0, maxOf(1, (density / 2).toInt()), 0, 0) }
|
|
750
790
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
751
791
|
val color = if (index == selectedTab) accent else muted
|
|
752
|
-
|
|
792
|
+
val number = tabIconText[tabs[index].id]
|
|
793
|
+
icon.setTextColor(if (number != null) Color.parseColor("#FF2D55") else color)
|
|
753
794
|
label.setTextColor(color)
|
|
754
|
-
val vector = CraftNativeIcons.drawable(context, null, tabs[index].symbol, color)
|
|
795
|
+
val vector = tabImages[tabs[index].id] ?: if (number == null) CraftNativeIcons.drawable(context, null, tabs[index].symbol, color) else null
|
|
755
796
|
icon.foreground = vector
|
|
756
|
-
if (vector != null)
|
|
797
|
+
icon.text = if (vector != null) "" else number ?: CraftNativeSymbols.glyph(tabs[index].symbol)
|
|
757
798
|
}
|
|
758
799
|
for (index in 0 until buttons.childCount) (buttons.getChildAt(index) as? TextView)?.setTextColor(accent)
|
|
759
800
|
}
|
|
@@ -2653,6 +2653,37 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2653
2653
|
}
|
|
2654
2654
|
}
|
|
2655
2655
|
|
|
2656
|
+
/** A field's pending native edits and the props acknowledged by the screen. */
|
|
2657
|
+
internal class CraftNativeInputEcho {
|
|
2658
|
+
private val pending = ArrayDeque<String>()
|
|
2659
|
+
private var rendered: String? = null
|
|
2660
|
+
private var initialized = false
|
|
2661
|
+
|
|
2662
|
+
fun record(value: String) {
|
|
2663
|
+
pending.addLast(value)
|
|
2664
|
+
while (pending.size > 64) pending.removeFirst()
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
fun accept(value: String?, nativeValue: String): Boolean {
|
|
2668
|
+
if (value == null) {
|
|
2669
|
+
initialized = false
|
|
2670
|
+
rendered = null
|
|
2671
|
+
pending.clear()
|
|
2672
|
+
return false
|
|
2673
|
+
}
|
|
2674
|
+
// A retained field can render the same prop more than once, after
|
|
2675
|
+
// its echo was consumed. That repeated prop must not overwrite a
|
|
2676
|
+
// keystroke that has not made the native/JS round trip yet.
|
|
2677
|
+
val changed = !initialized || rendered != value
|
|
2678
|
+
initialized = true
|
|
2679
|
+
rendered = value
|
|
2680
|
+
if (!changed) return false
|
|
2681
|
+
val index = pending.indexOf(value)
|
|
2682
|
+
if (index >= 0) repeat(index + 1) { pending.removeFirst() }
|
|
2683
|
+
return nativeValue != value && index < 0
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2656
2687
|
/**
|
|
2657
2688
|
* A native screen's `<Video>`: the platform's VideoView.
|
|
2658
2689
|
*
|
|
@@ -3261,7 +3292,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3261
3292
|
* the screen answer with a value the field has already moved past;
|
|
3262
3293
|
* that echo is not written back over what was typed since.
|
|
3263
3294
|
*/
|
|
3264
|
-
val inputEchoes = mutableMapOf<EditText,
|
|
3295
|
+
val inputEchoes = mutableMapOf<EditText, CraftNativeInputEcho>()
|
|
3265
3296
|
val controls = mutableMapOf<String, View>()
|
|
3266
3297
|
// Protocol ids identify mutation targets; authored keys identify the
|
|
3267
3298
|
// retained view. Keep both mappings so a targeted update can recover
|
|
@@ -4601,10 +4632,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4601
4632
|
val autofillHint = autofillHintFor(props)
|
|
4602
4633
|
if (autofillHint == null) setAutofillHints(*emptyArray()) else setAutofillHints(autofillHint)
|
|
4603
4634
|
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
4604
|
-
val echoes = screen.inputEchoes
|
|
4605
|
-
|
|
4606
|
-
if (echoIndex >= 0) repeat(echoIndex + 1) { echoes!!.removeFirst() }
|
|
4607
|
-
if (controlledValue != null && text.toString() != controlledValue && echoIndex < 0) {
|
|
4635
|
+
val echoes = screen.inputEchoes.getOrPut(this) { CraftNativeInputEcho() }
|
|
4636
|
+
if (echoes.accept(controlledValue, text.toString()) && controlledValue != null) {
|
|
4608
4637
|
screen.suppressedTextChanges.add(this)
|
|
4609
4638
|
setTextPreservingSelection(this, controlledValue)
|
|
4610
4639
|
}
|
|
@@ -4662,9 +4691,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4662
4691
|
(screen.inputIdentities[this@apply] ?: identity).let { screen.drafts[it] = updated }
|
|
4663
4692
|
if (screen.suppressedTextChanges.remove(this@apply)) return
|
|
4664
4693
|
val current = screen.handlers[this@apply] ?: return
|
|
4665
|
-
|
|
4666
|
-
echoes.addLast(updated)
|
|
4667
|
-
while (echoes.size > 64) echoes.removeFirst()
|
|
4694
|
+
screen.inputEchoes.getOrPut(this@apply) { CraftNativeInputEcho() }.record(updated)
|
|
4668
4695
|
send(screen, "EVENT", JSONObject()
|
|
4669
4696
|
.put("handlerName", current)
|
|
4670
4697
|
.put("nativeEvent", JSONObject().put("text", updated)))
|
|
@@ -5430,6 +5457,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5430
5457
|
view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
|
|
5431
5458
|
view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
|
|
5432
5459
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
5460
|
+
// VideoView's Surface is behind this window. An opaque View
|
|
5461
|
+
// background paints over the decoded picture (only rounded corners
|
|
5462
|
+
// showed it). Keep the outline/borders, with a transparent fill.
|
|
5463
|
+
val backgroundColor = if (view is CraftNativeVideoView) Color.TRANSPARENT
|
|
5464
|
+
else color(style.optString("backgroundColor"), Color.TRANSPARENT)
|
|
5433
5465
|
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5434
5466
|
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
5435
5467
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
@@ -5452,14 +5484,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
5452
5484
|
if (background || border > 0 || radius > 0 || hasSideBorders) {
|
|
5453
5485
|
if (hasSideBorders) {
|
|
5454
5486
|
view.background = CraftNativeSideBorderDrawable(
|
|
5455
|
-
|
|
5487
|
+
backgroundColor,
|
|
5456
5488
|
cornerRadii,
|
|
5457
5489
|
sideWidths,
|
|
5458
5490
|
sideColors,
|
|
5459
5491
|
)
|
|
5460
5492
|
} else {
|
|
5461
5493
|
view.background = GradientDrawable().apply {
|
|
5462
|
-
setColor(
|
|
5494
|
+
setColor(backgroundColor)
|
|
5463
5495
|
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
5464
5496
|
if (border > 0) {
|
|
5465
5497
|
val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
|