@craft-native/android 0.0.207 → 0.0.209

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/android",
3
- "version": "0.0.207",
3
+ "version": "0.0.209",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/android#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -578,6 +578,11 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
578
578
  private val largeTitleView = TextView(context)
579
579
  private val buttons = LinearLayout(context)
580
580
  private val tabBar = LinearLayout(context)
581
+ private val tabRow = LinearLayout(context)
582
+ private val accessory = FrameLayout(context)
583
+ private val accessoryIcon = ImageView(context)
584
+ private val accessoryBadge = TextView(context)
585
+ private var accessorySpec: JSONObject? = null
581
586
  private val tabViews = mutableListOf<Pair<TextView, TextView>>()
582
587
  private val tabIconText = mutableMapOf<String, String>()
583
588
  private val tabImageURLs = mutableMapOf<String, String>()
@@ -596,6 +601,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
596
601
  var onBack: (() -> Unit)? = null
597
602
  var onButton: ((String) -> Unit)? = null
598
603
  var onTab: ((Int) -> Unit)? = null
604
+ var onAccessory: ((String) -> Unit)? = null
599
605
  var onTabSwipeStart: ((Float, Float, Float) -> Boolean)? = null
600
606
  var onTabSwipeMove: ((Float) -> Unit)? = null
601
607
  var onTabSwipeEnd: ((Float, Boolean) -> Unit)? = null
@@ -660,8 +666,22 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
660
666
  tabBar.addView(cell, LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f))
661
667
  tabViews.add(icon to label)
662
668
  }
663
- addView(tabBar, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64)))
664
- tabBar.visibility = if (tabs.isEmpty()) View.GONE else View.VISIBLE
669
+ tabRow.orientation = HORIZONTAL
670
+ tabRow.gravity = Gravity.CENTER_VERTICAL
671
+ tabRow.setPadding(dp(12), dp(4), dp(12), dp(4))
672
+ tabRow.addView(tabBar, LayoutParams(0, dp(64), 1f))
673
+ accessory.addView(accessoryIcon, FrameLayout.LayoutParams(dp(26), dp(26), Gravity.CENTER))
674
+ accessoryBadge.textSize = 10f
675
+ accessoryBadge.gravity = Gravity.CENTER
676
+ accessoryBadge.setTextColor(Color.WHITE)
677
+ accessoryBadge.setPadding(dp(4), 0, dp(4), 0)
678
+ accessoryBadge.background = GradientDrawable().apply { setColor(Color.RED); cornerRadius = dp(9).toFloat() }
679
+ accessory.addView(accessoryBadge, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp(18), Gravity.TOP or Gravity.END))
680
+ accessory.setOnClickListener { accessorySpec?.optString("path")?.let { onAccessory?.invoke(it) } }
681
+ accessory.visibility = View.GONE
682
+ tabRow.addView(accessory, LayoutParams(dp(64), dp(64)).apply { marginStart = dp(8) })
683
+ addView(tabRow, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(72)))
684
+ tabRow.visibility = if (tabs.isEmpty()) View.GONE else View.VISIBLE
665
685
  refreshColors()
666
686
  }
667
687
 
@@ -671,7 +691,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
671
691
  val overHorizontalScroll = onTabSwipeShouldDefer?.invoke(event.x, event.y) == true
672
692
  horizontalScrollDeferred = overHorizontalScroll
673
693
  if (overHorizontalScroll) parent?.requestDisallowInterceptTouchEvent(true)
674
- swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll && tabBar.visibility == View.VISIBLE && event.y < tabBar.top
694
+ swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll && tabRow.visibility == View.VISIBLE && event.y < tabRow.top
675
695
  swipeIntercepted = false
676
696
  swipeDownX = event.x
677
697
  swipeDownY = event.y
@@ -742,7 +762,17 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
742
762
  buttons.addView(navButton(button))
743
763
  }
744
764
  val tabBarHidden = options.optBoolean("tabBarHidden", false) || depth > 0 && options.optBoolean("tabBarHiddenWhenPushed", false)
745
- tabBar.visibility = if (tabs.isEmpty() || tabBarHidden) View.GONE else View.VISIBLE
765
+ tabRow.visibility = if (tabs.isEmpty() || tabBarHidden) View.GONE else View.VISIBLE
766
+ accessorySpec = options.optJSONObject("tabAccessory")?.takeIf {
767
+ val path = it.optString("path")
768
+ path.startsWith("/") && !path.startsWith("//") && it.has("title")
769
+ }
770
+ accessory.visibility = if (accessorySpec == null) View.GONE else View.VISIBLE
771
+ accessorySpec?.let {
772
+ accessory.contentDescription = it.optString("accessibilityLabel").ifBlank { it.optString("title") } + it.optString("badge").takeIf { badge -> badge.isNotBlank() && badge != "null" }?.let { badge -> ", $badge" }.orEmpty()
773
+ accessoryBadge.text = it.optString("badge").takeUnless { badge -> badge == "null" }.orEmpty().take(8)
774
+ accessoryBadge.visibility = if (accessoryBadge.text.isEmpty()) View.GONE else View.VISIBLE
775
+ }
746
776
  tabSwipeEnabled = options.optBoolean("tabSwipeEnabled", true)
747
777
  refreshColors()
748
778
  }
@@ -801,13 +831,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
801
831
  titleView.setTextColor(text)
802
832
  largeTitleView.setTextColor(text)
803
833
  backButton.imageTintList = ColorStateList.valueOf(accent)
804
- tabBar.setBackgroundColor(if (dark) Color.parseColor("#121212") else Color.parseColor("#F9F9F9"))
805
- val divider = GradientDrawable().apply { setColor(if (dark) Color.parseColor("#262626") else Color.parseColor("#D8D8DC")) }
806
- tabBar.foreground = null
807
- tabBar.background = android.graphics.drawable.LayerDrawable(arrayOf(
808
- divider,
809
- GradientDrawable().apply { setColor(if (dark) Color.parseColor("#121212") else Color.parseColor("#F9F9F9")) },
810
- )).apply { setLayerInset(1, 0, maxOf(1, (density / 2).toInt()), 0, 0) }
834
+ val surface = if (dark) Color.parseColor("#1C1C1E") else Color.parseColor("#F9F9F9")
835
+ tabBar.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
836
+ accessory.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
837
+ accessorySpec?.let { accessoryIcon.setImageDrawable(CraftNativeIcons.drawable(context, null, it.optString("symbol"), text)) }
811
838
  tabViews.forEachIndexed { index, (icon, label) ->
812
839
  val color = if (index == selectedTab) accent else muted
813
840
  val number = tabIconText[tabs[index].id]
@@ -256,16 +256,9 @@ private data class CraftNativeLayoutStyle(
256
256
  aspectRatio = decimal("aspectRatio")?.takeIf { it.isFinite() && it > 0.0 },
257
257
  minWidth = length("minWidth"), maxWidth = length("maxWidth"),
258
258
  minHeight = length("minHeight"), maxHeight = length("maxHeight"),
259
- position = raw.opt("position")
260
- ?.takeUnless { it == JSONObject.NULL }
261
- ?.toString()
262
- ?.takeIf { it.isNotBlank() }
263
- ?: "relative",
259
+ position = craftNativeOptionalString(raw.opt("position")) ?: "relative",
264
260
  top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
265
- alignSelf = raw.opt("alignSelf")
266
- ?.takeUnless { it == JSONObject.NULL }
267
- ?.toString()
268
- ?.takeIf { it.isNotBlank() && it != "auto" },
261
+ alignSelf = craftNativeOptionalString(raw.opt("alignSelf"))?.takeUnless { it == "auto" },
269
262
  flexGrow = decimal("flexGrow") ?: flex?.coerceAtLeast(0.0) ?: 0.0,
270
263
  flexShrink = decimal("flexShrink") ?: flex?.let { if (it != 0.0) 1.0 else 0.0 },
271
264
  flexBasis = length("flexBasis")
@@ -383,11 +376,8 @@ private class CraftNativeSvgView(context: Context) : View(context) {
383
376
  fun merge(node: JSONObject): SvgPaint {
384
377
  val props = node.optJSONObject("props") ?: JSONObject()
385
378
  val style = node.optJSONObject("style") ?: JSONObject()
386
- fun value(name: String): String? = when {
387
- style.has(name) && !style.isNull(name) -> style.optString(name)
388
- props.has(name) && !props.isNull(name) -> props.optString(name)
389
- else -> null
390
- }
379
+ fun value(name: String): String? = craftNativeOptionalString(style.opt(name))
380
+ ?: craftNativeOptionalString(props.opt(name))
391
381
  fun raw(name: String): Any? = when {
392
382
  style.has(name) && !style.isNull(name) -> style.opt(name)
393
383
  props.has(name) && !props.isNull(name) -> props.opt(name)
@@ -486,7 +476,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
486
476
  for (index in 0 until children.length()) {
487
477
  val node = children.optJSONObject(index) ?: continue
488
478
  val style = node.optJSONObject("style") ?: JSONObject()
489
- if (style.optString("display") == "none") continue
479
+ if (craftNativeOptionalString(style.opt("display")) == "none") continue
490
480
  val paint = inherited.merge(node)
491
481
  when (node.optString("type")) {
492
482
  "G" -> drawNodes(canvas, node.optJSONArray("children") ?: JSONArray(), paint)
@@ -582,7 +572,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
582
572
  }
583
573
  var x = craftNativeNumber(props.opt("x"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
584
574
  val y = craftNativeNumber(props.opt("y"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
585
- when (props.optString("textAnchor")) {
575
+ when (craftNativeOptionalString(props.opt("textAnchor"))) {
586
576
  "middle" -> x -= paint.measureText(text) / 2f
587
577
  "end" -> x -= paint.measureText(text)
588
578
  }
@@ -1027,14 +1017,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1027
1017
  }
1028
1018
 
1029
1019
  fun configure(style: JSONObject, density: Float) {
1030
- orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
1031
- reverse = style.optString("flexDirection").endsWith("-reverse")
1032
- val flexWrap = style.optString("flexWrap")
1033
- wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || style.optBoolean("flexWrap", false)
1020
+ val flexDirection = craftNativeOptionalString(style.opt("flexDirection")).orEmpty()
1021
+ orientation = if (flexDirection in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
1022
+ reverse = flexDirection.endsWith("-reverse")
1023
+ val flexWrap = craftNativeOptionalString(style.opt("flexWrap")).orEmpty()
1024
+ wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || craftNativeBoolean(style.opt("flexWrap"))
1034
1025
  wrapReverse = flexWrap == "wrap-reverse"
1035
- alignItems = style.optString("alignItems", "stretch")
1036
- alignContent = style.optString("alignContent", "flex-start")
1037
- justifyContent = style.optString("justifyContent", "flex-start")
1026
+ alignItems = craftNativeOptionalString(style.opt("alignItems")) ?: "stretch"
1027
+ alignContent = craftNativeOptionalString(style.opt("alignContent")) ?: "flex-start"
1028
+ justifyContent = craftNativeOptionalString(style.opt("justifyContent")) ?: "flex-start"
1038
1029
  fun spacing(name: String, fallback: Float): Float {
1039
1030
  val value = craftNativeNumber(style.opt(name)) ?: Double.NaN
1040
1031
  val pixels = value * density
@@ -1044,7 +1035,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1044
1035
  val gap = spacing("gap", 0f)
1045
1036
  rowGap = spacing("rowGap", gap)
1046
1037
  columnGap = spacing("columnGap", gap)
1047
- grid = style.optString("display") == "grid"
1038
+ grid = craftNativeOptionalString(style.opt("display")) == "grid"
1048
1039
  gridColumns = craftNativeGridColumnCount(
1049
1040
  if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
1050
1041
  )
@@ -2095,7 +2086,7 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
2095
2086
  }
2096
2087
 
2097
2088
  fun configure(props: JSONObject, style: JSONObject, density: Float) {
2098
- behavior = props.optString("behavior", "padding").takeIf { it in setOf("height", "position", "padding") } ?: "padding"
2089
+ behavior = craftNativeOptionalString(props.opt("behavior"))?.takeIf { it in setOf("height", "position", "padding") } ?: "padding"
2099
2090
  val authoredOffset = craftNativeFiniteDouble(props, "keyboardVerticalOffset", 0.0)
2100
2091
  val pixelOffset = (authoredOffset * density.toDouble()).takeIf { it.isFinite() } ?: 0.0
2101
2092
  keyboardVerticalOffset = pixelOffset.roundToInt()
@@ -3958,6 +3949,7 @@ class MainActivity : AppCompatActivity() {
3958
3949
  val bars = CraftNativeChrome(this, routes.tabs)
3959
3950
  bars.layoutParams = FrameLayout.LayoutParams(-1, -1)
3960
3951
  bars.onBack = { onBackPressedDispatcherCompat() }
3952
+ bars.onAccessory = { path -> navigateOpen(path) }
3961
3953
  bars.onButton = { id -> visibleScreen?.takeIf { it.active }?.let { send(it, "NAV_BUTTON", JSONObject().put("id", id)) } }
3962
3954
  bars.onTab = { index ->
3963
3955
  haptic()
@@ -4945,7 +4937,7 @@ class MainActivity : AppCompatActivity() {
4945
4937
  "Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
4946
4938
  includeFontPadding = false
4947
4939
  maxLines = craftNativeInt(props.opt("numberOfLines"), 0).takeIf { it > 0 } ?: Int.MAX_VALUE
4948
- ellipsize = when (props.optString("ellipsizeMode")) {
4940
+ ellipsize = when (craftNativeOptionalString(props.opt("ellipsizeMode"))) {
4949
4941
  "head" -> TextUtils.TruncateAt.START
4950
4942
  "middle" -> TextUtils.TruncateAt.MIDDLE
4951
4943
  "tail" -> TextUtils.TruncateAt.END
@@ -5248,7 +5240,7 @@ class MainActivity : AppCompatActivity() {
5248
5240
  // from the previous hidden state when it patches props.
5249
5241
  craftNativeBoolean(props.opt("visible"))
5250
5242
  } else {
5251
- style.has("display") && style.optString("display") != "none"
5243
+ style.has("display") && craftNativeOptionalString(style.opt("display")) != "none"
5252
5244
  }
5253
5245
  val wasVisible = screen.modalVisibility[this]
5254
5246
  screen.modalVisibility[this] = visible
@@ -5480,7 +5472,7 @@ class MainActivity : AppCompatActivity() {
5480
5472
  drawing = node
5481
5473
  }
5482
5474
  "ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
5483
- val horizontal = craftNativeBoolean(props.opt("horizontal")) || style.optString("flexDirection") in setOf("row", "row-reverse")
5475
+ val horizontal = craftNativeBoolean(props.opt("horizontal")) || craftNativeOptionalString(style.opt("flexDirection")) in setOf("row", "row-reverse")
5484
5476
  setHorizontal(horizontal)
5485
5477
  setNativeNestedScrollingEnabled(true)
5486
5478
  setScrollIndicators(
@@ -5488,7 +5480,7 @@ class MainActivity : AppCompatActivity() {
5488
5480
  craftNativeBoolean(props.opt("showsHorizontalScrollIndicator"), true),
5489
5481
  )
5490
5482
  setScrollEnabled(craftNativeBoolean(props.opt("scrollEnabled"), true))
5491
- setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5483
+ setKeyboardShouldPersistTaps(craftNativeOptionalString(props.opt("keyboardShouldPersistTaps")) ?: "never")
5492
5484
  setBounces(craftNativeBoolean(props.opt("bounces"), true))
5493
5485
  setAlwaysBounce(
5494
5486
  craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
@@ -5550,7 +5542,7 @@ class MainActivity : AppCompatActivity() {
5550
5542
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
5551
5543
  }
5552
5544
  },
5553
- keyboardDismissMode = props.optString("keyboardDismissMode"),
5545
+ keyboardDismissMode = craftNativeOptionalString(props.opt("keyboardDismissMode")).orEmpty(),
5554
5546
  dismissKeyboard = {
5555
5547
  (getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
5556
5548
  ?.hideSoftInputFromWindow(nativeScroll.windowToken, 0)
@@ -5587,9 +5579,9 @@ class MainActivity : AppCompatActivity() {
5587
5579
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5588
5580
  }, craftNativeBoolean(propRefresh.opt("refreshing"), craftNativeBoolean(props.opt("refreshing"))))
5589
5581
  setRefreshAppearance(propRefresh, refreshTintFallback(screen))
5590
- nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5582
+ nativeList.setKeyboardShouldPersistTaps(craftNativeOptionalString(props.opt("keyboardShouldPersistTaps")) ?: "never")
5591
5583
  nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
5592
- nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
5584
+ nativeList.setKeyboardDismissMode(craftNativeOptionalString(props.opt("keyboardDismissMode")).orEmpty()) {
5593
5585
  (getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
5594
5586
  ?.hideSoftInputFromWindow(windowToken, 0)
5595
5587
  }
@@ -5598,6 +5590,7 @@ class MainActivity : AppCompatActivity() {
5598
5590
  "View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
5599
5591
  configureContainer(this, style)
5600
5592
  if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
5593
+ else setWindowSafeInsets(0, 0, 0, 0)
5601
5594
  reconcileChildren(screen, this, children, identity, next, style, previousControls)
5602
5595
  }
5603
5596
  "KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
@@ -5744,7 +5737,7 @@ class MainActivity : AppCompatActivity() {
5744
5737
  if (type == "ActivityIndicator") {
5745
5738
  val animating = craftNativeBoolean(props.opt("animating"), true)
5746
5739
  val hidesWhenStopped = craftNativeBoolean(props.opt("hidesWhenStopped"), true)
5747
- control.visibility = if (style.optString("display") == "none" || (!animating && hidesWhenStopped)) {
5740
+ control.visibility = if (craftNativeOptionalString(style.opt("display")) == "none" || (!animating && hidesWhenStopped)) {
5748
5741
  View.GONE
5749
5742
  } else {
5750
5743
  View.VISIBLE
@@ -5865,7 +5858,7 @@ class MainActivity : AppCompatActivity() {
5865
5858
  layout.setChildStyle(childView, child.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
5866
5859
  }
5867
5860
  }
5868
- if (style.optString("flexDirection").endsWith("-reverse")) desired.reverse()
5861
+ if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
5869
5862
  val desiredSet = desired.toSet()
5870
5863
  for ((index, childView) in desired.withIndex()) {
5871
5864
  if (layout.getChildAt(index) !== childView) {
@@ -5992,7 +5985,7 @@ class MainActivity : AppCompatActivity() {
5992
5985
  }
5993
5986
  applyTransform(view, style.optJSONArray("transform"))
5994
5987
  if (view !is CraftNativeModalView) {
5995
- view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
5988
+ view.visibility = if (craftNativeOptionalString(style.opt("display")) == "none") View.GONE else View.VISIBLE
5996
5989
  }
5997
5990
  view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
5998
5991
  view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
@@ -6001,13 +5994,13 @@ class MainActivity : AppCompatActivity() {
6001
5994
  // background paints over the decoded picture (only rounded corners
6002
5995
  // showed it). Keep the outline/borders, with a transparent fill.
6003
5996
  val backgroundColor = if (view is CraftNativeVideoView) Color.TRANSPARENT
6004
- else color(style.optString("backgroundColor"), Color.TRANSPARENT)
5997
+ else color(craftNativeOptionalString(style.opt("backgroundColor")).orEmpty(), Color.TRANSPARENT)
6005
5998
  val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
6006
5999
  val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
6007
- val gradientDirection = style.optString("gradientDirection").takeIf { it.isNotBlank() }
6008
- val gradientFrom = style.optString("gradientFrom").takeIf { it.isNotBlank() }
6009
- val gradientVia = style.optString("gradientVia").takeIf { it.isNotBlank() }
6010
- val gradientTo = style.optString("gradientTo").takeIf { it.isNotBlank() }
6000
+ val gradientDirection = craftNativeOptionalString(style.opt("gradientDirection"))
6001
+ val gradientFrom = craftNativeOptionalString(style.opt("gradientFrom"))
6002
+ val gradientVia = craftNativeOptionalString(style.opt("gradientVia"))
6003
+ val gradientTo = craftNativeOptionalString(style.opt("gradientTo"))
6011
6004
  val gradientColors = if (gradientDirection != null && gradientFrom != null && gradientTo != null) {
6012
6005
  listOfNotNull(
6013
6006
  color(gradientFrom, Color.TRANSPARENT),
@@ -6025,7 +6018,7 @@ class MainActivity : AppCompatActivity() {
6025
6018
  listOf(radius, radius)
6026
6019
  }.toFloatArray()
6027
6020
  val sideColors = listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")
6028
- .map { color(style.optString(it).ifBlank { style.optString("borderColor") }, Color.TRANSPARENT) }
6021
+ .map { color(craftNativeOptionalString(style.opt(it)) ?: craftNativeOptionalString(style.opt("borderColor")).orEmpty(), Color.TRANSPARENT) }
6029
6022
  .toIntArray()
6030
6023
  val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
6031
6024
  defaultBackgrounds[view]
@@ -6039,7 +6032,7 @@ class MainActivity : AppCompatActivity() {
6039
6032
  cornerRadii,
6040
6033
  sideWidths,
6041
6034
  sideColors,
6042
- style.optString("borderStyle").takeIf { it == "dashed" || it == "dotted" },
6035
+ craftNativeOptionalString(style.opt("borderStyle"))?.takeIf { it == "dashed" || it == "dotted" },
6043
6036
  gradientDirection,
6044
6037
  gradientColors,
6045
6038
  )
@@ -6051,8 +6044,8 @@ class MainActivity : AppCompatActivity() {
6051
6044
  } else setColor(backgroundColor)
6052
6045
  if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
6053
6046
  if (border > 0) {
6054
- val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
6055
- when (style.optString("borderStyle")) {
6047
+ val borderColor = color(craftNativeOptionalString(style.opt("borderColor")).orEmpty(), Color.TRANSPARENT)
6048
+ when (craftNativeOptionalString(style.opt("borderStyle"))) {
6056
6049
  "dashed" -> setStroke(dp(border), borderColor, dp(border * 3).toFloat(), dp(border * 2).toFloat())
6057
6050
  "dotted" -> setStroke(dp(border), borderColor, dp(border).toFloat(), dp(border * 1.5).toFloat())
6058
6051
  else -> setStroke(dp(border), borderColor)
@@ -6065,13 +6058,14 @@ class MainActivity : AppCompatActivity() {
6065
6058
  // and the web.
6066
6059
  null
6067
6060
  } else defaultBackground
6068
- view.clipToOutline = style.optString("overflow") == "hidden"
6069
- if (view is ViewGroup) view.clipChildren = style.optString("overflow") == "hidden"
6061
+ val overflow = craftNativeOptionalString(style.opt("overflow"))
6062
+ view.clipToOutline = overflow == "hidden"
6063
+ if (view is ViewGroup) view.clipChildren = overflow == "hidden"
6070
6064
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
6071
6065
  (style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
6072
6066
  ) {
6073
6067
  val shadowOpacity = craftNativeFiniteFloat(style, "shadowOpacity", 1f).coerceIn(0f, 1f)
6074
- val shadow = color(style.optString("shadowColor"), Color.BLACK)
6068
+ val shadow = color(craftNativeOptionalString(style.opt("shadowColor")).orEmpty(), Color.BLACK)
6075
6069
  val alphaShadow = Color.argb((Color.alpha(shadow) * shadowOpacity).roundToInt(), Color.red(shadow), Color.green(shadow), Color.blue(shadow))
6076
6070
  view.outlineAmbientShadowColor = alphaShadow
6077
6071
  view.outlineSpotShadowColor = alphaShadow
@@ -6169,7 +6163,7 @@ class MainActivity : AppCompatActivity() {
6169
6163
  }
6170
6164
 
6171
6165
  private fun configureText(view: TextView, value: String, style: JSONObject) {
6172
- view.text = when (style.optString("textTransform")) {
6166
+ view.text = when (craftNativeOptionalString(style.opt("textTransform"))) {
6173
6167
  "uppercase" -> value.uppercase()
6174
6168
  "lowercase" -> value.lowercase()
6175
6169
  "capitalize" -> value.split(' ').joinToString(" ") { word -> word.replaceFirstChar(Char::uppercase) }
@@ -6279,7 +6273,7 @@ class MainActivity : AppCompatActivity() {
6279
6273
  }
6280
6274
 
6281
6275
  private fun configureTextStyle(view: TextView, style: JSONObject) {
6282
- view.setTextColor(color(style.optString("color"), defaultTextColor))
6276
+ view.setTextColor(color(craftNativeOptionalString(style.opt("color")).orEmpty(), defaultTextColor))
6283
6277
  view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
6284
6278
  val fontWeight = style.opt("fontWeight") as? String
6285
6279
  val weight = when (fontWeight) {
@@ -6287,13 +6281,14 @@ class MainActivity : AppCompatActivity() {
6287
6281
  "100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
6288
6282
  else -> 400
6289
6283
  }
6290
- val italic = style.optString("fontStyle") == "italic"
6291
- val family = style.optString("fontFamily").takeIf { it.isNotBlank() }
6284
+ val italic = craftNativeOptionalString(style.opt("fontStyle")) == "italic"
6285
+ val family = craftNativeOptionalString(style.opt("fontFamily"))?.takeIf { it.isNotBlank() }
6292
6286
  view.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
6293
6287
  Typeface.create(Typeface.create(family, Typeface.NORMAL), weight, italic)
6294
6288
  } else Typeface.create(family, (if (weight >= 600) Typeface.BOLD else Typeface.NORMAL) or
6295
6289
  (if (italic) Typeface.ITALIC else Typeface.NORMAL))
6296
- view.textAlignment = when (style.optString("textAlign")) {
6290
+ val textAlign = craftNativeOptionalString(style.opt("textAlign"))
6291
+ view.textAlignment = when (textAlign) {
6297
6292
  "left" -> View.TEXT_ALIGNMENT_TEXT_START
6298
6293
  "right" -> View.TEXT_ALIGNMENT_TEXT_END
6299
6294
  "center" -> View.TEXT_ALIGNMENT_CENTER
@@ -6301,7 +6296,7 @@ class MainActivity : AppCompatActivity() {
6301
6296
  else -> View.TEXT_ALIGNMENT_INHERIT
6302
6297
  }
6303
6298
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
6304
- view.justificationMode = if (style.optString("textAlign") == "justify") {
6299
+ view.justificationMode = if (textAlign == "justify") {
6305
6300
  Layout.JUSTIFICATION_MODE_INTER_WORD
6306
6301
  } else {
6307
6302
  Layout.JUSTIFICATION_MODE_NONE
@@ -6315,7 +6310,7 @@ class MainActivity : AppCompatActivity() {
6315
6310
  craftNativeFiniteDouble(style, "lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
6316
6311
  1f
6317
6312
  )
6318
- val shadowColor = style.optString("textShadowColor").takeIf { it.isNotBlank() }
6313
+ val shadowColor = craftNativeOptionalString(style.opt("textShadowColor"))?.takeIf { it.isNotBlank() }
6319
6314
  if (shadowColor == null) {
6320
6315
  view.paint.clearShadowLayer()
6321
6316
  } else {
@@ -6332,16 +6327,16 @@ class MainActivity : AppCompatActivity() {
6332
6327
  }
6333
6328
 
6334
6329
  private fun applyTextDecoration(view: TextView, style: JSONObject) {
6335
- val line = style.optString("textDecorationLine")
6330
+ val line = craftNativeOptionalString(style.opt("textDecorationLine")).orEmpty()
6336
6331
  if (view is EditText) {
6337
6332
  // A field's own text, spanned in place: replacing it would move
6338
6333
  // the caret to the start and tell the screen the text changed.
6339
6334
  val editable = view.text ?: return
6340
6335
  editable.getSpans(0, editable.length, CraftTextDecorationSpan::class.java).forEach(editable::removeSpan)
6341
6336
  if (line.isBlank() || line == "none" || editable.isEmpty()) return
6342
- val fieldColor = style.optString("textDecorationColor").takeIf { it.isNotBlank() }?.let { color(it, Color.TRANSPARENT) }
6337
+ val fieldColor = craftNativeOptionalString(style.opt("textDecorationColor"))?.takeIf { it.isNotBlank() }?.let { color(it, Color.TRANSPARENT) }
6343
6338
  editable.setSpan(
6344
- CraftTextDecorationSpan(line, style.optString("textDecorationStyle").ifBlank { "solid" }, fieldColor),
6339
+ CraftTextDecorationSpan(line, craftNativeOptionalString(style.opt("textDecorationStyle")).orEmpty().ifBlank { "solid" }, fieldColor),
6345
6340
  0,
6346
6341
  editable.length,
6347
6342
  Spannable.SPAN_INCLUSIVE_INCLUSIVE,
@@ -6353,14 +6348,14 @@ class MainActivity : AppCompatActivity() {
6353
6348
  if (view.text is Spannable) view.text = text
6354
6349
  return
6355
6350
  }
6356
- val decorationColor = style.optString("textDecorationColor")
6351
+ val decorationColor = craftNativeOptionalString(style.opt("textDecorationColor")).orEmpty()
6357
6352
  .takeIf { it.isNotBlank() }
6358
6353
  ?.let { color(it, Color.TRANSPARENT) }
6359
6354
  val decorated = SpannableString(text)
6360
6355
  decorated.setSpan(
6361
6356
  CraftTextDecorationSpan(
6362
6357
  line,
6363
- style.optString("textDecorationStyle").ifBlank { "solid" },
6358
+ craftNativeOptionalString(style.opt("textDecorationStyle")).orEmpty().ifBlank { "solid" },
6364
6359
  decorationColor,
6365
6360
  ),
6366
6361
  0,
@@ -6568,8 +6563,16 @@ class MainActivity : AppCompatActivity() {
6568
6563
  info.roleDescription = role.takeIf { it !in setOf("view", "text", "textinput", "scrollview") }
6569
6564
  info.hintText = host.tooltipText
6570
6565
  info.isEnabled = host.isEnabled && !disabled
6571
- info.isSelected = craftNativeBoolean(state?.opt("selected"))
6572
- info.isChecked = craftNativeBoolean(state?.opt("checked"))
6566
+ // Keep the native widget state when no accessibilityState was
6567
+ // authored. Overwriting it with the strict false fallback
6568
+ // made a checked Switch/CheckBox announce as unchecked unless
6569
+ // the screen redundantly mirrored its value into metadata.
6570
+ if (state?.has("selected") == true) {
6571
+ info.isSelected = craftNativeBoolean(state.opt("selected"))
6572
+ }
6573
+ if (state?.has("checked") == true) {
6574
+ info.isChecked = craftNativeBoolean(state.opt("checked"))
6575
+ }
6573
6576
  }
6574
6577
  })
6575
6578
  ViewCompat.setImportantForAccessibility(