@craft-native/android 0.0.206 → 0.0.207

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.206",
3
+ "version": "0.0.207",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/android#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -148,6 +148,10 @@ private fun craftNativeOptionalString(value: Any?): String? = value
148
148
  ?.takeUnless { it == "null" || it == "undefined" }
149
149
  ?.takeIf { it.isNotBlank() }
150
150
 
151
+ private fun craftNativeBoolean(value: Any?, fallback: Boolean = false): Boolean = (value as? Boolean) ?: fallback
152
+
153
+ private fun craftNativeInt(value: Any?, fallback: Int): Int = (value as? Number)?.toInt() ?: fallback
154
+
151
155
  private fun craftNativePickerValue(value: Any?): String? = when (value) {
152
156
  is String -> value.takeIf { it.isNotEmpty() }
153
157
  is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
@@ -157,20 +161,35 @@ private fun craftNativePickerValue(value: Any?): String? = when (value) {
157
161
  private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
158
162
  names.firstNotNullOfOrNull { name -> craftNativeOptionalString(events.opt(name)) }.orEmpty()
159
163
 
164
+ private fun craftNativeNumber(value: Any?): Double? = (value as? Number)
165
+ ?.toDouble()
166
+ ?.takeIf { it.isFinite() }
167
+
168
+ private fun craftNativeFontWeight(value: Any?): String? = when (value) {
169
+ is String -> value
170
+ is Number -> value.toDouble().takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toInt()?.toString()
171
+ else -> null
172
+ }
173
+
160
174
  internal fun craftNativePadding(style: JSONObject?, name: String, fallback: Int, density: Float): Int {
161
- val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
175
+ val value = craftNativeNumber(style?.opt(name)) ?: Double.NaN
162
176
  val pixels = value * density
163
177
  return if (value.isFinite() && value >= 0 && pixels.isFinite()) pixels.toInt() else fallback
164
178
  }
165
179
 
166
180
  internal fun craftNativeFiniteFloat(style: JSONObject?, name: String, fallback: Float): Float {
167
- val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
181
+ val value = craftNativeNumber(style?.opt(name)) ?: Double.NaN
168
182
  val narrowed = value.toFloat()
169
183
  return if (value.isFinite() && narrowed.isFinite()) narrowed else fallback
170
184
  }
171
185
 
172
186
  internal fun craftNativeFiniteDouble(style: JSONObject?, name: String, fallback: Double): Double {
173
- val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
187
+ val value = craftNativeNumber(style?.opt(name)) ?: Double.NaN
188
+ return value.takeIf { it.isFinite() } ?: fallback
189
+ }
190
+
191
+ internal fun craftNativeFiniteNumber(props: JSONObject?, name: String, fallback: Double): Double {
192
+ val value = (props?.opt(name) as? Number)?.toDouble() ?: Double.NaN
174
193
  return value.takeIf { it.isFinite() } ?: fallback
175
194
  }
176
195
 
@@ -221,16 +240,16 @@ private data class CraftNativeLayoutStyle(
221
240
  companion object {
222
241
  fun from(raw: JSONObject, density: Float): CraftNativeLayoutStyle {
223
242
  fun length(name: String): CraftNativeLength? = CraftNativeLength.from(raw.opt(name), density)
224
- fun number(name: String): Int? {
243
+ fun numeric(name: String): Double? {
225
244
  if (!raw.has(name) || raw.isNull(name)) return null
226
- return raw.optDouble(name, Double.NaN).takeIf { it.isFinite() }?.let { points ->
245
+ return (raw.opt(name) as? Number)?.toDouble()?.takeIf { it.isFinite() }
246
+ }
247
+ fun number(name: String): Int? {
248
+ return numeric(name)?.let { points ->
227
249
  (points * density).takeIf { it.isFinite() }?.roundToInt()
228
250
  }
229
251
  }
230
- fun decimal(name: String): Double? {
231
- if (!raw.has(name) || raw.isNull(name)) return null
232
- return raw.optDouble(name, Double.NaN).takeIf { it.isFinite() }
233
- }
252
+ fun decimal(name: String): Double? = numeric(name)
234
253
  val flex = decimal("flex")
235
254
  return CraftNativeLayoutStyle(
236
255
  width = length("width"), height = length("height"),
@@ -377,7 +396,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
377
396
  fun dashes(rawValue: Any?): List<Float>? {
378
397
  val values = rawValue as? JSONArray ?: return null
379
398
  return (0 until values.length()).mapNotNull {
380
- values.optDouble(it, Double.NaN).toFloat().takeIf { value -> value.isFinite() && value >= 0 }
399
+ craftNativeNumber(values.opt(it))?.toFloat()?.takeIf { value -> value.isFinite() && value >= 0 }
381
400
  }
382
401
  }
383
402
  // Keep the cascade explicit: computed style wins over a raw prop
@@ -455,10 +474,10 @@ private class CraftNativeSvgView(context: Context) : View(context) {
455
474
  private fun viewBox(): Quadruple<Float, Float, Float, Float>? {
456
475
  val values = drawing.optJSONObject("props")?.optJSONArray("viewBox") ?: return null
457
476
  if (values.length() != 4) return null
458
- val x = values.optDouble(0, Double.NaN).toFloat()
459
- val y = values.optDouble(1, Double.NaN).toFloat()
460
- val width = values.optDouble(2, Double.NaN).toFloat()
461
- val height = values.optDouble(3, Double.NaN).toFloat()
477
+ val x = craftNativeNumber(values.opt(0))?.toFloat() ?: Float.NaN
478
+ val y = craftNativeNumber(values.opt(1))?.toFloat() ?: Float.NaN
479
+ val width = craftNativeNumber(values.opt(2))?.toFloat() ?: Float.NaN
480
+ val height = craftNativeNumber(values.opt(3))?.toFloat() ?: Float.NaN
462
481
  if (!x.isFinite() || !y.isFinite() || !width.isFinite() || !height.isFinite() || width <= 0f || height <= 0f) return null
463
482
  return Quadruple(width, height, x, y)
464
483
  }
@@ -480,12 +499,12 @@ private class CraftNativeSvgView(context: Context) : View(context) {
480
499
  private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
481
500
  val props = node.optJSONObject("props") ?: JSONObject()
482
501
  val geometry = props.optJSONArray("geometry") ?: return
483
- if ((0 until geometry.length()).any { !geometry.optDouble(it, Double.NaN).toFloat().isFinite() }) return
502
+ if ((0 until geometry.length()).any { craftNativeNumber(geometry.opt(it))?.toFloat()?.isFinite() != true }) return
484
503
  val path = Path()
485
504
  var index = 0
486
505
  while (index < geometry.length()) {
487
- val command = geometry.optDouble(index, Double.NaN).toInt()
488
- fun number(offset: Int): Float = geometry.optDouble(index + offset, Double.NaN).toFloat()
506
+ val command = craftNativeNumber(geometry.opt(index))?.toInt() ?: break
507
+ fun number(offset: Int): Float = craftNativeNumber(geometry.opt(index + offset))?.toFloat() ?: Float.NaN
489
508
  when (command) {
490
509
  0 -> if (index + 2 < geometry.length()) { path.moveTo(number(1), number(2)); index += 3 } else break
491
510
  1 -> if (index + 2 < geometry.length()) { path.lineTo(number(1), number(2)); index += 3 } else break
@@ -551,9 +570,9 @@ private class CraftNativeSvgView(context: Context) : View(context) {
551
570
  for (index in 0 until children.length()) if (children.opt(index) is String) append(children.optString(index))
552
571
  }
553
572
  if (text.isEmpty()) return
554
- val size = (if (props.has("fontSize")) props.optDouble("fontSize", 12.0) else nodeStyle.optDouble("fontSize", 12.0))
555
- .takeIf { it.isFinite() }
573
+ val size = (if (props.has("fontSize")) craftNativeNumber(props.opt("fontSize")) else craftNativeNumber(nodeStyle.opt("fontSize")))
556
574
  ?.toFloat()
575
+ ?.takeIf { it.isFinite() }
557
576
  ?: 12f
558
577
  val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
559
578
  color = resolveColor(style.fill, style.color, Color.BLACK)
@@ -561,8 +580,8 @@ private class CraftNativeSvgView(context: Context) : View(context) {
561
580
  textSize = size
562
581
  typeface = style.typeface
563
582
  }
564
- var x = props.optDouble("x", 0.0).takeIf { it.isFinite() }?.toFloat() ?: 0f
565
- val y = props.optDouble("y", 0.0).takeIf { it.isFinite() }?.toFloat() ?: 0f
583
+ var x = craftNativeNumber(props.opt("x"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
584
+ val y = craftNativeNumber(props.opt("y"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
566
585
  when (props.optString("textAnchor")) {
567
586
  "middle" -> x -= paint.measureText(text) / 2f
568
587
  "end" -> x -= paint.measureText(text)
@@ -629,16 +648,16 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
629
648
 
630
649
  private fun applyPending() {
631
650
  val map = googleMap ?: return
632
- map.uiSettings.isScrollGesturesEnabled = pendingProps.optBoolean("scrollEnabled", true)
633
- map.uiSettings.isZoomGesturesEnabled = pendingProps.optBoolean("zoomEnabled", true)
634
- map.uiSettings.isRotateGesturesEnabled = pendingProps.optBoolean("rotateEnabled", false)
651
+ map.uiSettings.isScrollGesturesEnabled = craftNativeBoolean(pendingProps.opt("scrollEnabled"), true)
652
+ map.uiSettings.isZoomGesturesEnabled = craftNativeBoolean(pendingProps.opt("zoomEnabled"), true)
653
+ map.uiSettings.isRotateGesturesEnabled = craftNativeBoolean(pendingProps.opt("rotateEnabled"))
635
654
  map.mapType = when (craftNativeOptionalString(pendingProps.opt("mapType")).orEmpty().lowercase()) {
636
655
  "satellite" -> GoogleMap.MAP_TYPE_SATELLITE
637
656
  "hybrid" -> GoogleMap.MAP_TYPE_HYBRID
638
657
  "terrain" -> GoogleMap.MAP_TYPE_TERRAIN
639
658
  else -> GoogleMap.MAP_TYPE_NORMAL
640
659
  }
641
- val showUserLocation = pendingProps.optBoolean("showsUserLocation", false) && hasLocationPermission()
660
+ val showUserLocation = craftNativeBoolean(pendingProps.opt("showsUserLocation")) && hasLocationPermission()
642
661
  runCatching { map.isMyLocationEnabled = showUserLocation }
643
662
  val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
644
663
  val nextHandlers = mutableMapOf<String, String>()
@@ -654,7 +673,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
654
673
  val points = coordinates(props.optJSONArray("geometry"))
655
674
  if (type == "mapmarker" || type == "marker") {
656
675
  points.firstOrNull()?.let { point ->
657
- nextMarkers[id] = point to craftNativeOptionalString(props.opt("title"))
676
+ nextMarkers[id] = point to (props.opt("title") as? String)
658
677
  listOf(events.opt("onPress"), events.opt("onMarkerPress"))
659
678
  .firstNotNullOfOrNull(::craftNativeOptionalString)
660
679
  ?.let { nextHandlers[id] = it }
@@ -717,7 +736,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
717
736
  moveCamera(initialRegion)
718
737
  initialCameraKey = key
719
738
  }
720
- } else if (pendingProps.optBoolean("fitToBounds", false)) {
739
+ } else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
721
740
  val key = "fit:${overlayKey(nextMarkers, nextPolylines)}"
722
741
  if (initialCameraKey != key) {
723
742
  if (fitToBounds()) initialCameraKey = key
@@ -731,7 +750,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
731
750
  super.onSizeChanged(width, height, oldWidth, oldHeight)
732
751
  // MapView can become ready before its host has a usable viewport. Do
733
752
  // not mark a fit as complete until the camera update can be issued.
734
- if (width > 0 && height > 0 && pendingProps.optBoolean("fitToBounds", false) && initialCameraKey == null) {
753
+ if (width > 0 && height > 0 && craftNativeBoolean(pendingProps.opt("fitToBounds")) && initialCameraKey == null) {
735
754
  applyPending()
736
755
  }
737
756
  }
@@ -1017,7 +1036,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1017
1036
  alignContent = style.optString("alignContent", "flex-start")
1018
1037
  justifyContent = style.optString("justifyContent", "flex-start")
1019
1038
  fun spacing(name: String, fallback: Float): Float {
1020
- val value = style.optDouble(name, Double.NaN)
1039
+ val value = craftNativeNumber(style.opt(name)) ?: Double.NaN
1021
1040
  val pixels = value * density
1022
1041
  val narrowed = pixels.toFloat()
1023
1042
  return if (value.isFinite() && value >= 0 && pixels.isFinite() && narrowed.isFinite()) narrowed else fallback
@@ -2734,7 +2753,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
2734
2753
  }
2735
2754
 
2736
2755
  fun configure(props: JSONObject) {
2737
- val requestedMode = when (props.optString("mode")) {
2756
+ val requestedMode = when (props.opt("mode") as? String) {
2738
2757
  "time" -> PickerMode.TIME
2739
2758
  "datetime", "dateAndTime", "datetime-local" -> PickerMode.DATE_TIME
2740
2759
  "countdown" -> PickerMode.COUNTDOWN
@@ -2774,7 +2793,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
2774
2793
  }
2775
2794
 
2776
2795
  suppressChanges = true
2777
- minuteInterval = props.optInt("minuteInterval", 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
2796
+ minuteInterval = craftNativeInt(props.opt("minuteInterval"), 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
2778
2797
  val defaults = defaultDateBounds
2779
2798
  datePicker?.minDate = pickerDate(props.opt("minimumDate"))?.timeInMillis ?: defaults?.first ?: datePicker?.minDate ?: 0L
2780
2799
  datePicker?.maxDate = pickerDate(props.opt("maximumDate"))?.timeInMillis ?: defaults?.second ?: datePicker?.maxDate ?: Long.MAX_VALUE
@@ -2784,7 +2803,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
2784
2803
  } else {
2785
2804
  pickerDate(props.opt("value"))?.let { applyDate(it) }
2786
2805
  }
2787
- val enabled = !props.optBoolean("disabled", false)
2806
+ val enabled = !craftNativeBoolean(props.opt("disabled"))
2788
2807
  isEnabled = enabled
2789
2808
  datePicker?.isEnabled = enabled
2790
2809
  timePicker?.isEnabled = enabled
@@ -4925,7 +4944,7 @@ class MainActivity : AppCompatActivity() {
4925
4944
  val control = when (type) {
4926
4945
  "Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
4927
4946
  includeFontPadding = false
4928
- maxLines = props.optInt("numberOfLines", 0).takeIf { it > 0 } ?: Int.MAX_VALUE
4947
+ maxLines = craftNativeInt(props.opt("numberOfLines"), 0).takeIf { it > 0 } ?: Int.MAX_VALUE
4929
4948
  ellipsize = when (props.optString("ellipsizeMode")) {
4930
4949
  "head" -> TextUtils.TruncateAt.START
4931
4950
  "middle" -> TextUtils.TruncateAt.MIDDLE
@@ -4936,7 +4955,7 @@ class MainActivity : AppCompatActivity() {
4936
4955
  }
4937
4956
  "StatusBar" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
4938
4957
  screen.statusBarStyle = craftNativeOptionalString(props.opt("barStyle"))
4939
- screen.statusBarHidden = if (props.has("hidden") && !props.isNull("hidden")) props.optBoolean("hidden") else null
4958
+ screen.statusBarHidden = if (props.has("hidden") && !props.isNull("hidden")) craftNativeBoolean(props.opt("hidden")) else null
4940
4959
  visibility = View.GONE
4941
4960
  }
4942
4961
  "RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
@@ -4944,17 +4963,18 @@ class MainActivity : AppCompatActivity() {
4944
4963
  }
4945
4964
  "Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
4946
4965
  includeFontPadding = false
4947
- configureText(this, craftNativeOptionalString(props.opt("title")) ?: childText(children), style)
4966
+ configureText(this, (props.opt("title") as? String) ?: childText(children), style)
4948
4967
  val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
4949
4968
  setTextColor(color(linkColor.orEmpty(), Color.rgb(33, 150, 243)))
4950
4969
  paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
4951
- isEnabled = !props.optBoolean("disabled", false)
4970
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
4952
4971
  isClickable = true
4953
4972
  isFocusable = true
4954
4973
  val target = listOf(props.opt("href"), props.opt("to"))
4955
- .firstNotNullOfOrNull(::craftNativeOptionalString)
4974
+ .mapNotNull { it as? String }
4975
+ .firstOrNull { it.trim().isNotEmpty() }
4956
4976
  ?.trim()
4957
- ?.takeIf { !props.optBoolean("disabled", false) }
4977
+ ?.takeIf { !craftNativeBoolean(props.opt("disabled")) }
4958
4978
  if (target != null) screen.linkTargets[this] = target else screen.linkTargets.remove(this)
4959
4979
  val handler = craftNativeEventName(events, "onPress", "onClick")
4960
4980
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -4975,13 +4995,13 @@ class MainActivity : AppCompatActivity() {
4975
4995
  minimumHeight = 0
4976
4996
  setPadding(0, 0, 0, 0)
4977
4997
  }).apply {
4978
- val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
4998
+ val title = (props.opt("title") as? String) ?: childText(children)
4979
4999
  isAllCaps = false
4980
5000
  configureText(this, title, style)
4981
5001
  val buttonColor = craftNativeOptionalString(props.opt("color"))
4982
5002
  ?: craftNativeOptionalString(style.opt("color"))
4983
5003
  setTextColor(buttonColor?.let { color(it, defaultTextColor) } ?: defaultTextColor)
4984
- isEnabled = !props.optBoolean("disabled", false)
5004
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
4985
5005
  val handler = craftNativeEventName(events, "onPress", "onClick")
4986
5006
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
4987
5007
  setOnClickListener {
@@ -4990,7 +5010,7 @@ class MainActivity : AppCompatActivity() {
4990
5010
  }
4991
5011
  }
4992
5012
  "Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
4993
- text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
5013
+ text = (props.opt("label") as? String) ?: childText(children)
4994
5014
  configureText(this, text.toString(), style)
4995
5015
  val checkboxChecked = props.opt("checked")
4996
5016
  val checkboxValue = props.opt("value")
@@ -5006,7 +5026,7 @@ class MainActivity : AppCompatActivity() {
5006
5026
  isChecked = checked
5007
5027
  val indeterminate = (props.opt("indeterminate") as? Boolean) == true
5008
5028
  isActivated = indeterminate
5009
- isEnabled = !props.optBoolean("disabled", false)
5029
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
5010
5030
  val checkboxColor = craftNativeOptionalString(props.opt("color"))
5011
5031
  ?: craftNativeOptionalString(style.opt("color"))
5012
5032
  buttonTintList = checkboxColor
@@ -5035,17 +5055,17 @@ class MainActivity : AppCompatActivity() {
5035
5055
  val defaultPadding = inputPaddingDefaults.getOrPut(this) {
5036
5056
  intArrayOf(paddingLeft, paddingTop, paddingRight, paddingBottom)
5037
5057
  }
5038
- hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
5058
+ hint = (props.opt("placeholder") as? String).orEmpty()
5039
5059
  setHintTextColor(
5040
- craftNativeOptionalString(props.opt("placeholderTextColor"))
5060
+ (props.opt("placeholderTextColor") as? String)
5041
5061
  ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5042
5062
  ?.let { ColorStateList.valueOf(it) }
5043
5063
  ?: defaultHintColors,
5044
5064
  )
5045
- highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
5065
+ highlightColor = (props.opt("selectionColor") as? String)
5046
5066
  ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5047
5067
  ?: defaultHighlightColor
5048
- val maxLength = props.optInt("maxLength", 0)
5068
+ val maxLength = craftNativeInt(props.opt("maxLength"), 0)
5049
5069
  val currentLength = (filters.firstOrNull { it is InputFilter.LengthFilter } as? InputFilter.LengthFilter)?.max ?: 0
5050
5070
  if (currentLength != maxLength || (maxLength == 0 && filters.isNotEmpty())) {
5051
5071
  filters = if (maxLength > 0) arrayOf(InputFilter.LengthFilter(maxLength)) else emptyArray()
@@ -5054,11 +5074,11 @@ class MainActivity : AppCompatActivity() {
5054
5074
  if (inputType != wantedType) inputType = wantedType
5055
5075
  val wantedIme = imeOptionsFor(props)
5056
5076
  if (imeOptions != wantedIme) imeOptions = wantedIme
5057
- val multiline = props.optBoolean("multiline", false)
5077
+ val multiline = craftNativeBoolean(props.opt("multiline"))
5058
5078
  if (isSingleLine == multiline) isSingleLine = !multiline
5059
- val wantedLines = if (multiline) props.optInt("numberOfLines", Int.MAX_VALUE).coerceAtLeast(1) else 1
5079
+ val wantedLines = if (multiline) craftNativeInt(props.opt("numberOfLines"), Int.MAX_VALUE).coerceAtLeast(1) else 1
5060
5080
  if (maxLines != wantedLines) maxLines = wantedLines
5061
- isEnabled = props.optBoolean("editable", true)
5081
+ isEnabled = craftNativeBoolean(props.opt("editable"), true)
5062
5082
  configureTextStyle(this, style)
5063
5083
  // The style's padding is where the text starts, as on iOS and
5064
5084
  // the web; without one the field keeps its own.
@@ -5086,14 +5106,16 @@ class MainActivity : AppCompatActivity() {
5086
5106
  // them after keyed reconciliation.
5087
5107
  val autofillHint = autofillHintFor(props)
5088
5108
  if (autofillHint == null) setAutofillHints(*emptyArray()) else setAutofillHints(autofillHint)
5089
- val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
5109
+ val controlledValue = props.opt("value") as? String
5090
5110
  val echoes = screen.inputEchoes.getOrPut(this) { CraftNativeInputEcho() }
5091
5111
  if (echoes.accept(controlledValue, text.toString()) && controlledValue != null) {
5092
5112
  screen.suppressedTextChanges.add(this)
5093
5113
  setTextPreservingSelection(this, controlledValue)
5094
5114
  }
5095
5115
  else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
5096
- else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
5116
+ else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) {
5117
+ (props.opt("defaultValue") as? String)?.let(::setText)
5118
+ }
5097
5119
  applyTextDecoration(this, style)
5098
5120
  screen.inputIdentities[this] = identity
5099
5121
  val handler = craftNativeEventName(events, "onChange", "onChangeText")
@@ -5128,7 +5150,7 @@ class MainActivity : AppCompatActivity() {
5128
5150
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("text", text.toString())))
5129
5151
  true
5130
5152
  }
5131
- if (props.optBoolean("autoFocus", false) && !isFocused) post {
5153
+ if (craftNativeBoolean(props.opt("autoFocus")) && !isFocused) post {
5132
5154
  if (!isFocused) {
5133
5155
  requestFocus()
5134
5156
  (getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
@@ -5158,7 +5180,7 @@ class MainActivity : AppCompatActivity() {
5158
5180
  val itemOptions = props.optJSONArray("items")?.let { items ->
5159
5181
  (0 until items.length()).mapNotNull { index ->
5160
5182
  val item = items.optJSONObject(index) ?: return@mapNotNull null
5161
- val label = craftNativeOptionalString(item.opt("label")) ?: return@mapNotNull null
5183
+ val label = (item.opt("label") as? String)?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null
5162
5184
  craftNativePickerValue(item.opt("value"))?.let { it to label } ?: (label to label)
5163
5185
  }
5164
5186
  }.orEmpty()
@@ -5166,7 +5188,7 @@ class MainActivity : AppCompatActivity() {
5166
5188
  val option = children.optJSONObject(index) ?: return@mapNotNull null
5167
5189
  if (option.optString("type") != "Text") return@mapNotNull null
5168
5190
  val label = (option.optJSONArray("children") ?: JSONArray()).let { text ->
5169
- (0 until text.length()).joinToString("") { text.optString(it) }
5191
+ (0 until text.length()).mapNotNull { text.opt(it) as? String }.joinToString("")
5170
5192
  }
5171
5193
  if (label.isBlank()) return@mapNotNull null
5172
5194
  val optionProps = option.optJSONObject("props")
@@ -5185,7 +5207,7 @@ class MainActivity : AppCompatActivity() {
5185
5207
  val requested = craftNativePickerValue(props.opt("selectedValue"))
5186
5208
  ?: craftNativePickerValue(props.opt("value")).orEmpty()
5187
5209
  val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
5188
- isEnabled = !props.optBoolean("disabled", false)
5210
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
5189
5211
  // Mounting a controlled picker must not look like a user
5190
5212
  // change. Android may dispatch the initial adapter selection
5191
5213
  // during the first layout pass; iOS only reports a choice
@@ -5215,7 +5237,7 @@ class MainActivity : AppCompatActivity() {
5215
5237
  }
5216
5238
  }
5217
5239
  "Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
5218
- transparent = props.optBoolean("transparent", false)
5240
+ transparent = craftNativeBoolean(props.opt("transparent"))
5219
5241
  content.configure(style, resources.displayMetrics.density)
5220
5242
  reconcileChildren(screen, content, children, identity, next, style, previousControls)
5221
5243
  // Dynamic :visible bindings arrive as a display style mutation;
@@ -5224,7 +5246,7 @@ class MainActivity : AppCompatActivity() {
5224
5246
  // An explicit visible prop owns modal visibility. The
5225
5247
  // binding runtime may leave a stale display:none style
5226
5248
  // from the previous hidden state when it patches props.
5227
- props.optBoolean("visible", false)
5249
+ craftNativeBoolean(props.opt("visible"))
5228
5250
  } else {
5229
5251
  style.has("display") && style.optString("display") != "none"
5230
5252
  }
@@ -5260,7 +5282,7 @@ class MainActivity : AppCompatActivity() {
5260
5282
  }
5261
5283
  "TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
5262
5284
  configureContainer(this, style)
5263
- val disabled = props.optBoolean("disabled", false)
5285
+ val disabled = craftNativeBoolean(props.opt("disabled"))
5264
5286
  screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
5265
5287
  screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
5266
5288
  if (type == "TouchableHighlight" && !disabled) {
@@ -5281,7 +5303,7 @@ class MainActivity : AppCompatActivity() {
5281
5303
  requestedChecked is Boolean -> requestedChecked
5282
5304
  else -> false
5283
5305
  }
5284
- isEnabled = !props.optBoolean("disabled", false)
5306
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
5285
5307
  props.optJSONObject("trackColor")?.let { track ->
5286
5308
  val falseRaw = craftNativeOptionalString(track.opt("false"))
5287
5309
  val trueRaw = craftNativeOptionalString(track.opt("true"))
@@ -5309,11 +5331,11 @@ class MainActivity : AppCompatActivity() {
5309
5331
  }
5310
5332
  }
5311
5333
  "Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
5312
- val minimum = craftNativeFiniteDouble(props, "minimumValue", 0.0)
5313
- val maximumCandidate = craftNativeFiniteDouble(props, "maximumValue", 1.0)
5334
+ val minimum = craftNativeFiniteNumber(props, "minimumValue", 0.0)
5335
+ val maximumCandidate = craftNativeFiniteNumber(props, "maximumValue", 1.0)
5314
5336
  val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
5315
5337
  setOnSeekBarChangeListener(null)
5316
- val step = craftNativeFiniteDouble(props, "step", 0.0).takeIf { it > 0 }
5338
+ val step = craftNativeFiniteNumber(props, "step", 0.0).takeIf { it > 0 }
5317
5339
  // A stepped slider needs one native position per authored
5318
5340
  // step; a fixed 1,000-position SeekBar silently made large
5319
5341
  // ranges unable to represent most valid values. Continuous
@@ -5327,7 +5349,7 @@ class MainActivity : AppCompatActivity() {
5327
5349
  .toInt()
5328
5350
  } ?: 1000
5329
5351
  max = progressMax
5330
- val requested = craftNativeFiniteDouble(props, "value", minimum)
5352
+ val requested = craftNativeFiniteNumber(props, "value", minimum)
5331
5353
  val value = (if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested)
5332
5354
  .coerceIn(minimum, maximum)
5333
5355
  progress = if (step != null) {
@@ -5345,7 +5367,7 @@ class MainActivity : AppCompatActivity() {
5345
5367
  thumbTintList = craftNativeOptionalString(props.opt("thumbTintColor"))
5346
5368
  ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5347
5369
  ?.let { ColorStateList.valueOf(it) }
5348
- isEnabled = !props.optBoolean("disabled", false)
5370
+ isEnabled = !craftNativeBoolean(props.opt("disabled"))
5349
5371
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
5350
5372
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
5351
5373
  val complete = craftNativeEventName(events, "onSlidingComplete")
@@ -5379,9 +5401,9 @@ class MainActivity : AppCompatActivity() {
5379
5401
  else -> 1f
5380
5402
  }
5381
5403
  indicatorScale = scale
5382
- val animating = props.optBoolean("animating", true)
5404
+ val animating = craftNativeBoolean(props.opt("animating"), true)
5383
5405
  isIndeterminate = animating
5384
- visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
5406
+ visibility = if (!animating && craftNativeBoolean(props.opt("hidesWhenStopped"), true)) View.GONE else View.VISIBLE
5385
5407
  indeterminateTintList = craftNativeOptionalString(props.opt("color"))
5386
5408
  ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5387
5409
  ?.let { ColorStateList.valueOf(it) }
@@ -5437,13 +5459,13 @@ class MainActivity : AppCompatActivity() {
5437
5459
  onPictureInPictureChanged = { updatePictureInPicture() }
5438
5460
  configure(
5439
5461
  source = props.opt("source") ?: props.opt("src"),
5440
- autoplay = props.optBoolean("autoplay", props.optBoolean("autoPlay", false)),
5441
- paused = props.optBoolean("paused", false),
5442
- loop = props.optBoolean("loop", false),
5443
- muted = props.optBoolean("muted", false),
5444
- controls = props.optBoolean("controls", false),
5462
+ autoplay = craftNativeBoolean(props.opt("autoplay"), craftNativeBoolean(props.opt("autoPlay"))),
5463
+ paused = craftNativeBoolean(props.opt("paused")),
5464
+ loop = craftNativeBoolean(props.opt("loop")),
5465
+ muted = craftNativeBoolean(props.opt("muted")),
5466
+ controls = craftNativeBoolean(props.opt("controls")),
5445
5467
  resizeMode = craftNativeResizeMode(style, props),
5446
- pictureInPicture = props.optBoolean("pictureInPicture", false),
5468
+ pictureInPicture = craftNativeBoolean(props.opt("pictureInPicture")),
5447
5469
  )
5448
5470
  }
5449
5471
  "Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
@@ -5458,21 +5480,21 @@ class MainActivity : AppCompatActivity() {
5458
5480
  drawing = node
5459
5481
  }
5460
5482
  "ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
5461
- val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
5483
+ val horizontal = craftNativeBoolean(props.opt("horizontal")) || style.optString("flexDirection") in setOf("row", "row-reverse")
5462
5484
  setHorizontal(horizontal)
5463
5485
  setNativeNestedScrollingEnabled(true)
5464
5486
  setScrollIndicators(
5465
- props.optBoolean("showsVerticalScrollIndicator", true),
5466
- props.optBoolean("showsHorizontalScrollIndicator", true),
5487
+ craftNativeBoolean(props.opt("showsVerticalScrollIndicator"), true),
5488
+ craftNativeBoolean(props.opt("showsHorizontalScrollIndicator"), true),
5467
5489
  )
5468
- setScrollEnabled(props.optBoolean("scrollEnabled", true))
5490
+ setScrollEnabled(craftNativeBoolean(props.opt("scrollEnabled"), true))
5469
5491
  setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5470
- setBounces(props.optBoolean("bounces", true))
5492
+ setBounces(craftNativeBoolean(props.opt("bounces"), true))
5471
5493
  setAlwaysBounce(
5472
- props.optBoolean("alwaysBounceVertical", !horizontal),
5473
- props.optBoolean("alwaysBounceHorizontal", horizontal),
5494
+ craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
5495
+ craftNativeBoolean(props.opt("alwaysBounceHorizontal"), horizontal),
5474
5496
  )
5475
- setPagingEnabled(props.optBoolean("pagingEnabled", false))
5497
+ setPagingEnabled(craftNativeBoolean(props.opt("pagingEnabled")))
5476
5498
  val refreshNode = (0 until children.length()).asSequence()
5477
5499
  .mapNotNull { children.optJSONObject(it) }
5478
5500
  .firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
@@ -5485,7 +5507,7 @@ class MainActivity : AppCompatActivity() {
5485
5507
  val refreshHandler = refreshHandlerName?.let { handler ->
5486
5508
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5487
5509
  }
5488
- setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5510
+ setRefreshHandler(refreshHandler, craftNativeBoolean(refreshProps.opt("refreshing"), craftNativeBoolean(props.opt("refreshing"))))
5489
5511
  setRefreshAppearance(refreshProps, resources.displayMetrics.density, refreshTintFallback(screen))
5490
5512
  val onScroll = craftNativeEventName(events, "onScroll")
5491
5513
  if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
@@ -5563,7 +5585,7 @@ class MainActivity : AppCompatActivity() {
5563
5585
  ?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
5564
5586
  configureRefresh(refreshHandlerName?.let { handler ->
5565
5587
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5566
- }, propRefresh.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5588
+ }, craftNativeBoolean(propRefresh.opt("refreshing"), craftNativeBoolean(props.opt("refreshing"))))
5567
5589
  setRefreshAppearance(propRefresh, refreshTintFallback(screen))
5568
5590
  nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5569
5591
  nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
@@ -5603,7 +5625,7 @@ class MainActivity : AppCompatActivity() {
5603
5625
  if (underlayHideHandler != null) screen.pressUnderlayHideHandlers[control] = underlayHideHandler else screen.pressUnderlayHideHandlers.remove(control)
5604
5626
  if (type != "Button" && type != "Link" && type != "Checkbox" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView" && type != "StatusBar" && type != "RefreshControl" && type != "Map") {
5605
5627
  val handler = craftNativeEventName(events, "onPress", "onClick")
5606
- val disabled = props.optBoolean("disabled", false)
5628
+ val disabled = craftNativeBoolean(props.opt("disabled"))
5607
5629
  val target = listOf(props.opt("href"), props.opt("to"))
5608
5630
  .firstNotNullOfOrNull(::craftNativeOptionalString)
5609
5631
  ?.trim()
@@ -5674,8 +5696,8 @@ class MainActivity : AppCompatActivity() {
5674
5696
  }
5675
5697
  control.isEnabled = !disabled
5676
5698
  }
5677
- val longPressDisabled = props.optBoolean("disabled", false) ||
5678
- props.optJSONObject("accessibilityState")?.optBoolean("disabled", false) == true
5699
+ val longPressDisabled = craftNativeBoolean(props.opt("disabled")) ||
5700
+ craftNativeBoolean(props.optJSONObject("accessibilityState")?.opt("disabled"))
5679
5701
  val longPressHandler = craftNativeEventName(events, "onLongPress").takeUnless { longPressDisabled } ?: ""
5680
5702
  if (longPressHandler.isNotBlank()) {
5681
5703
  screen.longPressHandlers[control] = longPressHandler
@@ -5720,8 +5742,8 @@ class MainActivity : AppCompatActivity() {
5720
5742
  next[identity] = control
5721
5743
  applyViewStyle(control, style)
5722
5744
  if (type == "ActivityIndicator") {
5723
- val animating = props.optBoolean("animating", true)
5724
- val hidesWhenStopped = props.optBoolean("hidesWhenStopped", true)
5745
+ val animating = craftNativeBoolean(props.opt("animating"), true)
5746
+ val hidesWhenStopped = craftNativeBoolean(props.opt("hidesWhenStopped"), true)
5725
5747
  control.visibility = if (style.optString("display") == "none" || (!animating && hidesWhenStopped)) {
5726
5748
  View.GONE
5727
5749
  } else {
@@ -5892,12 +5914,12 @@ class MainActivity : AppCompatActivity() {
5892
5914
  }
5893
5915
  val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
5894
5916
  val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
5895
- val horizontal = props.optBoolean("horizontal", false)
5917
+ val horizontal = craftNativeBoolean(props.opt("horizontal"))
5896
5918
  list.apply(
5897
5919
  nodes = nodes,
5898
5920
  horizontal = horizontal,
5899
- columns = props.optInt("numColumns", 1),
5900
- inverted = props.optBoolean("inverted", false),
5921
+ columns = craftNativeInt(props.opt("numColumns"), 1),
5922
+ inverted = craftNativeBoolean(props.opt("inverted")),
5901
5923
  endReachedThreshold = authoredThreshold,
5902
5924
  renderItem = { node, itemIdentity, _ ->
5903
5925
  val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
@@ -5917,16 +5939,16 @@ class MainActivity : AppCompatActivity() {
5917
5939
  },
5918
5940
  )
5919
5941
  list.setScrollIndicators(
5920
- props.optBoolean("showsVerticalScrollIndicator", true),
5921
- props.optBoolean("showsHorizontalScrollIndicator", true),
5942
+ craftNativeBoolean(props.opt("showsVerticalScrollIndicator"), true),
5943
+ craftNativeBoolean(props.opt("showsHorizontalScrollIndicator"), true),
5922
5944
  )
5923
- list.setScrollEnabled(props.optBoolean("scrollEnabled", true))
5924
- list.setPagingEnabled(props.optBoolean("pagingEnabled", false))
5945
+ list.setScrollEnabled(craftNativeBoolean(props.opt("scrollEnabled"), true))
5946
+ list.setPagingEnabled(craftNativeBoolean(props.opt("pagingEnabled")))
5925
5947
  list.setScrollTarget(props.opt("scrollTarget"))
5926
5948
  list.setBounceOptions(
5927
- props.optBoolean("bounces", true),
5928
- props.optBoolean("alwaysBounceVertical", !horizontal),
5929
- props.optBoolean("alwaysBounceHorizontal", horizontal),
5949
+ craftNativeBoolean(props.opt("bounces"), true),
5950
+ craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
5951
+ craftNativeBoolean(props.opt("alwaysBounceHorizontal"), horizontal),
5930
5952
  )
5931
5953
  }
5932
5954
 
@@ -6191,11 +6213,22 @@ class MainActivity : AppCompatActivity() {
6191
6213
  )
6192
6214
  view.vector = vector
6193
6215
  view.text = if (vector != null) "" else androidIconGlyph(symbol)
6194
- val weight = when (style.optString("fontWeight")) {
6195
- "bold", "700", "800", "900" -> Typeface.BOLD
6196
- else -> Typeface.NORMAL
6216
+ val weight = when (craftNativeFontWeight(style.opt("fontWeight"))) {
6217
+ "100" -> 100
6218
+ "200" -> 200
6219
+ "300" -> 300
6220
+ "500" -> 500
6221
+ "600" -> 600
6222
+ "bold", "700" -> 700
6223
+ "800" -> 800
6224
+ "900" -> 900
6225
+ else -> 400
6226
+ }
6227
+ view.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
6228
+ Typeface.create("sans-serif", weight, false)
6229
+ } else {
6230
+ Typeface.create("sans-serif", if (weight >= 600) Typeface.BOLD else Typeface.NORMAL)
6197
6231
  }
6198
- view.typeface = Typeface.create("sans-serif", weight)
6199
6232
  val hasExplicitWidth = style.has("width") && !style.isNull("width")
6200
6233
  val hasExplicitHeight = style.has("height") && !style.isNull("height")
6201
6234
  if (!hasExplicitWidth && !hasExplicitHeight) {
@@ -6248,7 +6281,8 @@ class MainActivity : AppCompatActivity() {
6248
6281
  private fun configureTextStyle(view: TextView, style: JSONObject) {
6249
6282
  view.setTextColor(color(style.optString("color"), defaultTextColor))
6250
6283
  view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
6251
- val weight = when (style.optString("fontWeight")) {
6284
+ val fontWeight = style.opt("fontWeight") as? String
6285
+ val weight = when (fontWeight) {
6252
6286
  "bold" -> 700
6253
6287
  "100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
6254
6288
  else -> 400
@@ -6273,12 +6307,12 @@ class MainActivity : AppCompatActivity() {
6273
6307
  Layout.JUSTIFICATION_MODE_NONE
6274
6308
  }
6275
6309
  }
6276
- view.letterSpacing = style.optDouble("letterSpacing", Double.NaN)
6310
+ view.letterSpacing = craftNativeFiniteDouble(style, "letterSpacing", Double.NaN)
6277
6311
  .takeIf { it.isFinite() }
6278
6312
  ?.let { if (view.textSize > 0) dp(it).toFloat() / view.textSize else 0f }
6279
6313
  ?: 0f
6280
6314
  view.setLineSpacing(
6281
- style.optDouble("lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
6315
+ craftNativeFiniteDouble(style, "lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
6282
6316
  1f
6283
6317
  )
6284
6318
  val shadowColor = style.optString("textShadowColor").takeIf { it.isNotBlank() }
@@ -6510,7 +6544,7 @@ class MainActivity : AppCompatActivity() {
6510
6544
  if (type == "Icon") "image" else type.lowercase()
6511
6545
  }
6512
6546
  val state = props.optJSONObject("accessibilityState")
6513
- val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
6547
+ val disabled = craftNativeBoolean(props.opt("disabled")) || craftNativeBoolean(state?.opt("disabled"))
6514
6548
  if (type !in setOf("Button", "Link", "Checkbox", "TextInput", "Picker", "DatePicker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
6515
6549
  view.isEnabled = !disabled
6516
6550
  } else if (disabled) {
@@ -6534,8 +6568,8 @@ class MainActivity : AppCompatActivity() {
6534
6568
  info.roleDescription = role.takeIf { it !in setOf("view", "text", "textinput", "scrollview") }
6535
6569
  info.hintText = host.tooltipText
6536
6570
  info.isEnabled = host.isEnabled && !disabled
6537
- info.isSelected = state?.optBoolean("selected", false) == true
6538
- info.isChecked = state?.optBoolean("checked", false) == true
6571
+ info.isSelected = craftNativeBoolean(state?.opt("selected"))
6572
+ info.isChecked = craftNativeBoolean(state?.opt("checked"))
6539
6573
  }
6540
6574
  })
6541
6575
  ViewCompat.setImportantForAccessibility(
@@ -6551,11 +6585,11 @@ class MainActivity : AppCompatActivity() {
6551
6585
  private fun color(raw: String, fallback: Int) = runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
6552
6586
 
6553
6587
  private fun inputTypeFor(props: JSONObject): Int {
6554
- val base = if (props.optBoolean("multiline", false)) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
6588
+ val base = if (craftNativeBoolean(props.opt("multiline"))) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
6555
6589
  else InputType.TYPE_CLASS_TEXT
6556
6590
  // Text variations do not carry their input class or multiline flag.
6557
6591
  // Preserve both when selecting an email, URL, or search keyboard.
6558
- val keyboard = when (props.optString("keyboardType")) {
6592
+ val keyboard = when (props.opt("keyboardType") as? String) {
6559
6593
  "email-address" -> base or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
6560
6594
  "url" -> base or InputType.TYPE_TEXT_VARIATION_URI
6561
6595
  "numeric", "number-pad" -> InputType.TYPE_CLASS_NUMBER
@@ -6565,21 +6599,22 @@ class MainActivity : AppCompatActivity() {
6565
6599
  "visible-password" -> base or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
6566
6600
  else -> base
6567
6601
  }
6568
- val capitalization = when (props.optString("autoCapitalize")) {
6602
+ val capitalization = when (props.opt("autoCapitalize") as? String) {
6569
6603
  "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
6570
6604
  "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
6571
6605
  "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
6572
6606
  else -> 0
6573
6607
  }
6574
- val correction = if (props.optBoolean("autoCorrect", true)) InputType.TYPE_TEXT_FLAG_AUTO_CORRECT else 0
6575
- val secure = if (props.optBoolean("secureTextEntry", false)) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0
6608
+ val correction = if (craftNativeBoolean(props.opt("autoCorrect"), true)) InputType.TYPE_TEXT_FLAG_AUTO_CORRECT else 0
6609
+ val secure = if (craftNativeBoolean(props.opt("secureTextEntry"))) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0
6576
6610
  return keyboard or capitalization or correction or secure
6577
6611
  }
6578
6612
 
6579
6613
  /// The autofill hint for `textContentType` (iOS's names) or `autoComplete`
6580
6614
  /// (HTML's), so a password manager fills the field.
6581
6615
  private fun autofillHintFor(props: JSONObject): String? {
6582
- val raw = props.optString("textContentType").ifBlank { props.optString("autoComplete") }
6616
+ val raw = (props.opt("textContentType") as? String)?.takeIf { it.isNotBlank() }
6617
+ ?: (props.opt("autoComplete") as? String).orEmpty()
6583
6618
  return when (raw.replace("-", "").replace("_", "").lowercase()) {
6584
6619
  "username" -> View.AUTOFILL_HINT_USERNAME
6585
6620
  "nickname" -> "personNickname"
@@ -6610,7 +6645,7 @@ class MainActivity : AppCompatActivity() {
6610
6645
  }
6611
6646
  }
6612
6647
 
6613
- private fun imeOptionsFor(props: JSONObject): Int = when (props.optString("returnKeyType")) {
6648
+ private fun imeOptionsFor(props: JSONObject): Int = when (props.opt("returnKeyType") as? String) {
6614
6649
  "done" -> EditorInfo.IME_ACTION_DONE
6615
6650
  "go" -> EditorInfo.IME_ACTION_GO
6616
6651
  "next" -> EditorInfo.IME_ACTION_NEXT