@craft-native/android 0.0.205 → 0.0.206

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.205",
3
+ "version": "0.0.206",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/android#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,7 @@
1
1
  package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.content.Context
4
+ import org.json.JSONObject
4
5
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
5
6
 
6
7
  /** Swipe-to-refresh host for a native FlatList. */
@@ -21,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
21
22
  if (handler == null) isRefreshing = false
22
23
  }
23
24
 
25
+ fun setRefreshAppearance(props: JSONObject, fallback: Int) {
26
+ val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
27
+ val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
28
+ setColorSchemeColors(resolved)
29
+ }
30
+
24
31
  override fun onDetachedFromWindow() {
25
32
  refreshHandler = null
26
33
  isRefreshing = false
@@ -149,8 +149,9 @@ private fun craftNativeOptionalString(value: Any?): String? = value
149
149
  ?.takeIf { it.isNotBlank() }
150
150
 
151
151
  private fun craftNativePickerValue(value: Any?): String? = when (value) {
152
+ is String -> value.takeIf { it.isNotEmpty() }
152
153
  is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
153
- else -> craftNativeOptionalString(value)
154
+ else -> null
154
155
  }
155
156
 
156
157
  private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
@@ -352,6 +353,14 @@ private class CraftNativeSvgView(context: Context) : View(context) {
352
353
  val vectorEffect: String? = null,
353
354
  val dominantBaseline: String? = null,
354
355
  ) {
356
+ private fun svgTypeface(weight: String?, inherited: Typeface): Typeface = when (weight?.lowercase()) {
357
+ "500" -> Typeface.create("sans-serif-medium", Typeface.NORMAL)
358
+ "600" -> Typeface.create("sans-serif", Typeface.BOLD)
359
+ "bold", "700", "800", "900" -> Typeface.DEFAULT_BOLD
360
+ "normal", "100", "200", "300", "400" -> Typeface.DEFAULT
361
+ else -> inherited
362
+ }
363
+
355
364
  fun merge(node: JSONObject): SvgPaint {
356
365
  val props = node.optJSONObject("props") ?: JSONObject()
357
366
  val style = node.optJSONObject("style") ?: JSONObject()
@@ -387,7 +396,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
387
396
  strokeDashoffset = number("strokeDashoffset") ?: strokeDashoffset,
388
397
  strokeMiterlimit = number("strokeMiterlimit")?.takeIf { it > 0 } ?: strokeMiterlimit,
389
398
  fillRule = value("fillRule") ?: fillRule,
390
- typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
399
+ typeface = svgTypeface(weight, typeface),
391
400
  strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
392
401
  strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
393
402
  strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
@@ -2209,13 +2218,13 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
2209
2218
  }
2210
2219
  }
2211
2220
 
2212
- fun setRefreshAppearance(props: JSONObject, density: Float) {
2221
+ fun setRefreshAppearance(props: JSONObject, density: Float, fallback: Int) {
2213
2222
  val tint = craftNativeOptionalString(props.opt("tintColor"))
2214
- if (tint != null) setColorSchemeColors(craftNativeColor(tint))
2215
- else setColorSchemeColors(Color.WHITE)
2223
+ setColorSchemeColors(tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback)
2216
2224
  val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
2217
- if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
2218
- else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
2225
+ setProgressBackgroundColorSchemeColor(
2226
+ background?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: Color.TRANSPARENT,
2227
+ )
2219
2228
  val offset = props.optDouble("progressViewOffset", Double.NaN)
2220
2229
  val requested = if (offset.isFinite()) (offset * density).roundToInt() else null
2221
2230
  if (configuredProgressOffset != requested) {
@@ -2862,7 +2871,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
2862
2871
  if (text.isBlank() || text == JSONObject.NULL.toString()) return null
2863
2872
  val formats = listOf(
2864
2873
  "HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
2865
- "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
2874
+ "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS",
2875
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
2866
2876
  )
2867
2877
  formats.firstNotNullOfOrNull { format ->
2868
2878
  runCatching {
@@ -4485,6 +4495,13 @@ class MainActivity : AppCompatActivity() {
4485
4495
  screen.view.setBackgroundColor(configured ?: ContextCompat.getColor(this, R.color.background))
4486
4496
  }
4487
4497
 
4498
+ /** Refresh controls use a contrasting system tint when no color is authored. */
4499
+ private fun refreshTintFallback(screen: Screen): Int {
4500
+ val background = (screen.view.background as? ColorDrawable)?.color
4501
+ ?: ContextCompat.getColor(this, R.color.background)
4502
+ return if (Color.luminance(background) < 0.5f) Color.WHITE else Color.BLACK
4503
+ }
4504
+
4488
4505
  /** The bridge's capability names: Craft's core set and this host's services. */
4489
4506
  private fun capabilityNames(): JSONArray = capabilities.names().apply {
4490
4507
  services.names().forEach { name -> put(name) }
@@ -4961,7 +4978,9 @@ class MainActivity : AppCompatActivity() {
4961
4978
  val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
4962
4979
  isAllCaps = false
4963
4980
  configureText(this, title, style)
4964
- craftNativeOptionalString(props.opt("color"))?.let { setTextColor(color(it, defaultTextColor)) }
4981
+ val buttonColor = craftNativeOptionalString(props.opt("color"))
4982
+ ?: craftNativeOptionalString(style.opt("color"))
4983
+ setTextColor(buttonColor?.let { color(it, defaultTextColor) } ?: defaultTextColor)
4965
4984
  isEnabled = !props.optBoolean("disabled", false)
4966
4985
  val handler = craftNativeEventName(events, "onPress", "onClick")
4967
4986
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -4973,11 +4992,11 @@ class MainActivity : AppCompatActivity() {
4973
4992
  "Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
4974
4993
  text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
4975
4994
  configureText(this, text.toString(), style)
4976
- val requested = props.opt("checked") ?: props.opt("value")
4977
- val checked = when (requested) {
4978
- is Boolean -> requested
4979
- is Number -> requested.toDouble() != 0.0
4980
- is String -> requested.equals("true", ignoreCase = true) || requested == "1"
4995
+ val checkboxChecked = props.opt("checked")
4996
+ val checkboxValue = props.opt("value")
4997
+ val checked = when {
4998
+ checkboxChecked is Boolean -> checkboxChecked
4999
+ checkboxValue is Boolean -> checkboxValue
4981
5000
  else -> false
4982
5001
  }
4983
5002
  setOnCheckedChangeListener(null)
@@ -4985,11 +5004,14 @@ class MainActivity : AppCompatActivity() {
4985
5004
  // an indeterminate control must announce "mixed" and its next
4986
5005
  // user toggle should transition to checked, not unchecked.
4987
5006
  isChecked = checked
4988
- isActivated = props.optBoolean("indeterminate", false)
5007
+ val indeterminate = (props.opt("indeterminate") as? Boolean) == true
5008
+ isActivated = indeterminate
4989
5009
  isEnabled = !props.optBoolean("disabled", false)
4990
5010
  val checkboxColor = craftNativeOptionalString(props.opt("color"))
4991
5011
  ?: craftNativeOptionalString(style.opt("color"))
4992
- buttonTintList = checkboxColor?.let { ColorStateList.valueOf(color(it, defaultTextColor)) }
5012
+ buttonTintList = checkboxColor
5013
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5014
+ ?.let { ColorStateList.valueOf(it) }
4993
5015
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
4994
5016
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
4995
5017
  setOnCheckedChangeListener { _, value ->
@@ -5016,11 +5038,12 @@ class MainActivity : AppCompatActivity() {
5016
5038
  hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
5017
5039
  setHintTextColor(
5018
5040
  craftNativeOptionalString(props.opt("placeholderTextColor"))
5019
- ?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
5041
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5042
+ ?.let { ColorStateList.valueOf(it) }
5020
5043
  ?: defaultHintColors,
5021
5044
  )
5022
5045
  highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
5023
- ?.let { color(it, defaultHighlightColor) }
5046
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5024
5047
  ?: defaultHighlightColor
5025
5048
  val maxLength = props.optInt("maxLength", 0)
5026
5049
  val currentLength = (filters.firstOrNull { it is InputFilter.LengthFilter } as? InputFilter.LengthFilter)?.max ?: 0
@@ -5254,24 +5277,29 @@ class MainActivity : AppCompatActivity() {
5254
5277
  val requestedValue = props.opt("value")
5255
5278
  val requestedChecked = props.opt("checked")
5256
5279
  isChecked = when {
5257
- requestedValue != null && requestedValue != JSONObject.NULL -> props.optBoolean("value")
5258
- requestedChecked == null || requestedChecked == JSONObject.NULL -> false
5280
+ requestedValue != null && requestedValue != JSONObject.NULL -> requestedValue as? Boolean ?: false
5259
5281
  requestedChecked is Boolean -> requestedChecked
5260
- requestedChecked is Number -> requestedChecked.toDouble() != 0.0
5261
- requestedChecked is String -> requestedChecked.isBlank() || requestedChecked.equals("true", ignoreCase = true)
5262
- else -> props.optBoolean("checked", false)
5282
+ else -> false
5263
5283
  }
5264
5284
  isEnabled = !props.optBoolean("disabled", false)
5265
5285
  props.optJSONObject("trackColor")?.let { track ->
5266
- val falseColor = color(track.optString("false"), Color.GRAY)
5267
- val trueColor = color(track.optString("true"), falseColor)
5268
- trackTintList = ColorStateList(
5269
- arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
5270
- intArrayOf(trueColor, falseColor),
5271
- )
5286
+ val falseRaw = craftNativeOptionalString(track.opt("false"))
5287
+ val trueRaw = craftNativeOptionalString(track.opt("true"))
5288
+ val falseColor = falseRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5289
+ val trueColor = trueRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5290
+ trackTintList = if ((falseRaw != null && falseColor == null) || (trueRaw != null && trueColor == null)) {
5291
+ null
5292
+ } else {
5293
+ val resolvedFalse = falseColor ?: Color.GRAY
5294
+ ColorStateList(
5295
+ arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
5296
+ intArrayOf(trueColor ?: resolvedFalse, resolvedFalse),
5297
+ )
5298
+ }
5272
5299
  } ?: run { trackTintList = null }
5273
5300
  craftNativeOptionalString(props.opt("thumbColor"))
5274
- ?.let { thumbTintList = ColorStateList.valueOf(color(it, Color.WHITE)) }
5301
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5302
+ ?.let { thumbTintList = ColorStateList.valueOf(it) }
5275
5303
  ?: run { thumbTintList = null }
5276
5304
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
5277
5305
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -5308,15 +5336,15 @@ class MainActivity : AppCompatActivity() {
5308
5336
  (((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
5309
5337
  }
5310
5338
  if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
5311
- craftNativeOptionalString(props.opt("minimumTrackTintColor"))
5312
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5313
- ?: run { progressTintList = null }
5314
- craftNativeOptionalString(props.opt("maximumTrackTintColor"))
5315
- ?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
5316
- ?: run { progressBackgroundTintList = null }
5317
- craftNativeOptionalString(props.opt("thumbTintColor"))
5318
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5319
- ?: run { thumbTintList = null }
5339
+ progressTintList = craftNativeOptionalString(props.opt("minimumTrackTintColor"))
5340
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5341
+ ?.let { ColorStateList.valueOf(it) }
5342
+ progressBackgroundTintList = craftNativeOptionalString(props.opt("maximumTrackTintColor"))
5343
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5344
+ ?.let { ColorStateList.valueOf(it) }
5345
+ thumbTintList = craftNativeOptionalString(props.opt("thumbTintColor"))
5346
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5347
+ ?.let { ColorStateList.valueOf(it) }
5320
5348
  isEnabled = !props.optBoolean("disabled", false)
5321
5349
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
5322
5350
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -5354,9 +5382,9 @@ class MainActivity : AppCompatActivity() {
5354
5382
  val animating = props.optBoolean("animating", true)
5355
5383
  isIndeterminate = animating
5356
5384
  visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
5357
- craftNativeOptionalString(props.opt("color"))
5358
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5359
- ?: run { indeterminateTintList = null }
5385
+ indeterminateTintList = craftNativeOptionalString(props.opt("color"))
5386
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5387
+ ?.let { ColorStateList.valueOf(it) }
5360
5388
  }
5361
5389
  "Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
5362
5390
  configureIcon(this, props, style)
@@ -5386,9 +5414,9 @@ class MainActivity : AppCompatActivity() {
5386
5414
  "center" -> ImageView.ScaleType.CENTER
5387
5415
  else -> ImageView.ScaleType.CENTER_INSIDE
5388
5416
  }
5389
- craftNativeOptionalString(style.opt("tintColor"))
5390
- ?.let { image.setColorFilter(color(it, Color.TRANSPARENT)) }
5391
- ?: image.clearColorFilter()
5417
+ val tint = craftNativeOptionalString(style.opt("tintColor"))
5418
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5419
+ if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
5392
5420
  val onLoadStart = craftNativeEventName(events, "onLoadStart")
5393
5421
  if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
5394
5422
  val onLoad = craftNativeEventName(events, "onLoad")
@@ -5458,7 +5486,7 @@ class MainActivity : AppCompatActivity() {
5458
5486
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5459
5487
  }
5460
5488
  setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5461
- setRefreshAppearance(refreshProps, resources.displayMetrics.density)
5489
+ setRefreshAppearance(refreshProps, resources.displayMetrics.density, refreshTintFallback(screen))
5462
5490
  val onScroll = craftNativeEventName(events, "onScroll")
5463
5491
  if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
5464
5492
  val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
@@ -5528,9 +5556,15 @@ class MainActivity : AppCompatActivity() {
5528
5556
  }
5529
5557
  "FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
5530
5558
  val nativeList = list
5531
- configureRefresh(craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
5559
+ val propRefresh = props.optJSONObject("refreshControl") ?: JSONObject()
5560
+ val propRefreshEvents = propRefresh.optJSONObject("events") ?: JSONObject()
5561
+ val refreshHandlerName = craftNativeEventName(propRefreshEvents, "onRefresh")
5562
+ .takeIf { it.isNotBlank() }
5563
+ ?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
5564
+ configureRefresh(refreshHandlerName?.let { handler ->
5532
5565
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5533
- }, props.optBoolean("refreshing", false))
5566
+ }, propRefresh.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5567
+ setRefreshAppearance(propRefresh, refreshTintFallback(screen))
5534
5568
  nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5535
5569
  nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
5536
5570
  nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
@@ -5800,8 +5834,7 @@ class MainActivity : AppCompatActivity() {
5800
5834
  is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
5801
5835
  is String -> TextView(this).apply {
5802
5836
  includeFontPadding = false
5803
- text = child
5804
- setTextColor(defaultTextColor)
5837
+ configureText(this, child, craftNativeTypography(style))
5805
5838
  }
5806
5839
  else -> null
5807
5840
  }
@@ -6046,10 +6079,13 @@ class MainActivity : AppCompatActivity() {
6046
6079
  }
6047
6080
  if (view is ImageView) {
6048
6081
  val tint = style.opt("tintColor") as? String
6049
- if (tint.isNullOrBlank() || tint == "null" || tint == "undefined") {
6082
+ val resolvedTint = tint
6083
+ ?.takeUnless { it.isBlank() || it == "null" || it == "undefined" }
6084
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
6085
+ if (resolvedTint == null) {
6050
6086
  view.clearColorFilter()
6051
6087
  } else {
6052
- view.setColorFilter(color(tint, Color.TRANSPARENT))
6088
+ view.setColorFilter(resolvedTint)
6053
6089
  }
6054
6090
  }
6055
6091
  }
@@ -6120,6 +6156,22 @@ class MainActivity : AppCompatActivity() {
6120
6156
  configureTextStyle(view, style)
6121
6157
  }
6122
6158
 
6159
+ /**
6160
+ * Raw strings inside a native container are implicit Text nodes. Keep
6161
+ * their typography, but not the parent's padding or layout, just like
6162
+ * the iOS reconciler does for the same child shape.
6163
+ */
6164
+ private fun craftNativeTypography(style: JSONObject): JSONObject = JSONObject().also { typography ->
6165
+ listOf(
6166
+ "color", "letterSpacing", "lineHeight", "fontSize", "fontFamily",
6167
+ "fontWeight", "fontStyle", "textAlign", "textTransform",
6168
+ "textDecorationLine", "textDecorationStyle", "textDecorationColor",
6169
+ "textShadowColor", "textShadowRadius", "textShadowOffset",
6170
+ ).forEach { key ->
6171
+ if (style.has(key) && !style.isNull(key)) typography.put(key, style.opt(key))
6172
+ }
6173
+ }
6174
+
6123
6175
  private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
6124
6176
  val symbol = craftNativeOptionalString(props.opt("symbol"))
6125
6177
  ?: craftNativeOptionalString(props.opt("name")).orEmpty()
@@ -6466,7 +6518,7 @@ class MainActivity : AppCompatActivity() {
6466
6518
  }
6467
6519
  ViewCompat.setStateDescription(
6468
6520
  view,
6469
- if (type == "Checkbox" && props.optBoolean("indeterminate", false)) "mixed" else null,
6521
+ if (type == "Checkbox" && (props.opt("indeterminate") as? Boolean) == true) "mixed" else null,
6470
6522
  )
6471
6523
  ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
6472
6524
  override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {