@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382

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.
Files changed (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
@@ -28,11 +28,19 @@ class HybridNativeList(context: ThemedReactContext) : HybridNativeListSpec() {
28
28
  set(value) {
29
29
  if (field == value) return
30
30
  field = value
31
- dispatchToUi { hostView.setContainerSlotHeightsJson(value) }
31
+ slotHeightsChanged = true
32
+ }
33
+
34
+ override var sidecarLayoutJson: String = ""
35
+ set(value) {
36
+ if (field == value) return
37
+ field = value
38
+ dispatchToUi { hostView.setSidecarLayoutJson(value) }
32
39
  }
33
40
 
34
41
  private var fontsChanged = false
35
42
  private var snapshotChanged = false
43
+ private var slotHeightsChanged = false
36
44
 
37
45
  override var fontFamiliesJson: String = "{}"
38
46
  set(value) {
@@ -44,11 +52,14 @@ class HybridNativeList(context: ThemedReactContext) : HybridNativeListSpec() {
44
52
  override fun afterUpdate() {
45
53
  super.afterUpdate()
46
54
  val fonts = if (fontsChanged) fontFamiliesJson else null
55
+ val slotHeights = if (slotHeightsChanged) containerSlotHeightsJson else null
47
56
  val snapshot = if (snapshotChanged) snapshotJson else null
48
57
  fontsChanged = false
58
+ slotHeightsChanged = false
49
59
  snapshotChanged = false
50
60
  dispatchToUi {
51
61
  fonts?.let { hostView.setFontFamiliesJson(it) }
62
+ slotHeights?.let { hostView.setContainerSlotHeightsJson(it, snapshot) }
52
63
  snapshot?.let { hostView.applySnapshot(it) }
53
64
  }
54
65
  }
@@ -90,6 +101,8 @@ class HybridNativeList(context: ThemedReactContext) : HybridNativeListSpec() {
90
101
  set(value) { field = value; hostView.onEndReached = value }
91
102
  override var onVisibleRangeChanged: ((payloadJson: String) -> Unit)? = null
92
103
  set(value) { field = value; hostView.onVisibleRangeChanged = value }
104
+ override var onStructuralTransitionSettled: ((payloadJson: String) -> Unit)? = null
105
+ set(value) { field = value; hostView.onStructuralTransitionSettled = value }
93
106
 
94
107
  override fun applySnapshot(snapshotJson: String) {
95
108
  dispatchToUi { hostView.applySnapshot(snapshotJson) }
@@ -8,11 +8,14 @@ import android.graphics.drawable.StateListDrawable
8
8
  import android.text.Spannable
9
9
  import android.text.SpannableStringBuilder
10
10
  import android.text.TextUtils
11
+ import android.text.TextPaint
11
12
  import android.text.style.AbsoluteSizeSpan
12
13
  import android.text.style.ForegroundColorSpan
14
+ import android.text.style.MetricAffectingSpan
13
15
  import android.util.TypedValue
14
16
  import android.view.Gravity
15
17
  import android.view.View
18
+ import android.view.MotionEvent
16
19
  import android.widget.LinearLayout
17
20
  import android.widget.ProgressBar
18
21
  import android.widget.TextView
@@ -29,11 +32,14 @@ import org.json.JSONObject
29
32
  // Bounded controls only. Event ownership and epochs stay with the row host.
30
33
  internal class NativeListAccessoryStack(context: android.content.Context, val fonts: NativeListFonts) : LinearLayout(context) {
31
34
  private val trailingViews = List(2) { NativeListTextView(context) }
35
+ private val buttonDownEpochs = IntArray(2) { -1 }
36
+ private var bindingEpoch = 0
32
37
  private val trailingIcons = List(2) { OneKeyIconView(context) }
33
38
  private val checkbox = OneKeyCheckboxView(context)
34
39
  private val spinner = ProgressBar(context)
35
40
  private var boundCheckboxData: JSONObject? = null
36
41
  private val semanticValueViews = mutableListOf<TextView>()
42
+ private val segmentedValuePairs = mutableMapOf<TextView, JSONObject>()
37
43
  private var selectorUsesSourceScale = false
38
44
  private var checkboxUsesSelectorStyle = false
39
45
  private var checkboxCheckedColor = Color.BLACK
@@ -66,6 +72,19 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
66
72
  info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
67
73
  }
68
74
  }
75
+ private val buttonAccessibilityDelegate =
76
+ object : View.AccessibilityDelegate() {
77
+ override fun onInitializeAccessibilityNodeInfo(
78
+ host: View,
79
+ info: android.view.accessibility.AccessibilityNodeInfo,
80
+ ) {
81
+ super.onInitializeAccessibilityNodeInfo(host, info)
82
+ info.className = android.widget.Button::class.java.name
83
+ info.isEnabled = host.isEnabled
84
+ info.isClickable = host.isEnabled && host.isClickable
85
+ info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
86
+ }
87
+ }
69
88
 
70
89
  private val trailingPaintFlags = trailingViews.map { it.paintFlags }
71
90
 
@@ -130,7 +149,12 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
130
149
  (checkbox.layoutParams as LayoutParams).marginStart = dp(12)
131
150
  for ((index, key) in listOf("value", "valueSecondary").withIndex()) {
132
151
  style.optJSONObject(key)?.let { value ->
133
- semanticValueViews.getOrNull(index)?.let { applyStyledText(it, value) }
152
+ semanticValueViews.getOrNull(index)?.let { view ->
153
+ applyStyledText(view, value)
154
+ if (value.has("fontSize")) segmentedValuePairs[view]?.let { accessory ->
155
+ restorePairSubscriptSizes(view, accessory, value.optDouble("fontSize").toFloat())
156
+ }
157
+ }
134
158
  }
135
159
  }
136
160
  if (style.has("trailingGap")) {
@@ -146,6 +170,13 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
146
170
  }
147
171
  }
148
172
  }
173
+ val pairedChevron = horizontal && (kinds == listOf("valuePair", "chevron") ||
174
+ (kinds == listOf("icon", "chevron") &&
175
+ descriptors.getJSONObject(0).optString("name") == "RefreshCcwOutline" &&
176
+ descriptors.getJSONObject(0).optString("actionKey").isEmpty()))
177
+ if (pairedChevron) {
178
+ (trailingIcons[1].layoutParams as LayoutParams).marginEnd = 0
179
+ }
149
180
  visibility =
150
181
  if ((0 until childCount).any { getChildAt(it).visibility != GONE }) VISIBLE else GONE
151
182
  }
@@ -167,37 +198,53 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
167
198
 
168
199
  fun anchorInset(view: View): Int =
169
200
  if (
170
- (tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" &&
171
- view in trailingIcons &&
172
- (view.layoutParams as MarginLayoutParams).marginStart < 0
201
+ view in trailingIcons &&
202
+ (view.layoutParams as MarginLayoutParams).marginStart < 0 &&
203
+ ((tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" ||
204
+ (tag as? NativeListItem)?.json?.optJSONArray("trailing")?.let { trailing ->
205
+ (0 until trailing.length()).any { index ->
206
+ trailing.optJSONObject(index)?.optString("buttonStyle") == "tertiary"
207
+ }
208
+ } == true)
173
209
  )
174
210
  dp(7)
175
211
  else 0
176
212
 
177
213
  fun reset() {
214
+ bindingEpoch += 1
178
215
  tag = null
179
216
  endInset = null
180
217
  requestsUnclippedHost = false
181
218
  clipChildren = true
182
219
  clipToPadding = true
183
220
  semanticValueViews.clear()
221
+ segmentedValuePairs.clear()
184
222
  boundCheckboxData = null
185
223
  trailingViews.forEachIndexed { index, v ->
224
+ buttonDownEpochs[index] = -1
186
225
  v.paintFlags = trailingPaintFlags[index]
187
226
  v.visibility = GONE
188
227
  v.text = ""
189
228
  v.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
190
229
  v.setOnClickListener(null)
191
- // setOnClickListener(null) leaves the view clickable, and a clickable
192
- // child consumes the touch, so a tap on a row value such as a network
193
- // balance never reached the row click (OK-65494). showTrailing enables
194
- // the click again only for a text accessory that carries an action.
230
+ // A clickable value consumes the row press; reset it until an action binds.
231
+ v.setOnTouchListener(null)
232
+ v.isPressed = false
195
233
  v.isClickable = false
234
+ v.isEnabled = true
235
+ v.alpha = 1f
236
+ v.contentDescription = null
237
+ v.setTag(com.facebook.react.R.id.react_test_id, null)
238
+ v.accessibilityDelegate = null
196
239
  v.background = null
197
240
  v.setPadding(0, 0, 0, 0)
198
241
  // Legacy trailing text: END gravity, one line, no ellipsis.
199
242
  v.gravity = Gravity.END
200
243
  v.opticalOffsetY = 0f
244
+ v.helpBindingGeneration += 1
245
+ v.guardsHelpTouches = false
246
+ v.primaryHelpOnly = false
247
+ v.showsHelpUnderline = false
201
248
  v.maxLines = 1
202
249
  v.setHorizontallyScrolling(false)
203
250
  v.ellipsize = null
@@ -257,6 +304,83 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
257
304
  for (index in 0 until minOf(2, accessories.length())) {
258
305
  val accessory = accessories.getJSONObject(index)
259
306
  when (accessory.optString("kind")) {
307
+ "button" -> {
308
+ if (textIndex !in trailingViews.indices) continue
309
+ val view = trailingViews[textIndex]
310
+ val buttonIndex = textIndex
311
+ val epoch = bindingEpoch
312
+ val disabled =
313
+ item.json.optBoolean("disabled", false) || accessory.optBoolean("disabled", false)
314
+ showTrailing(textIndex, accessory.optString("label"), false)
315
+ view.typeface = fonts.medium(context)
316
+ view.gravity = Gravity.CENTER
317
+ view.setTextColor(
318
+ safeColor(
319
+ accessory.optString("textColor").takeIf(String::isNotEmpty),
320
+ color(theme, "primaryText", "#000000DF"),
321
+ )
322
+ )
323
+ view.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, dp(30))
324
+ view.setPadding(dp(11), dp(4), dp(11), dp(4))
325
+ view.background =
326
+ StateListDrawable().apply {
327
+ fun fill(value: Int) =
328
+ GradientDrawable().apply {
329
+ setColor(value)
330
+ cornerRadius = dp(15).toFloat()
331
+ setStroke(dp(1), Color.TRANSPARENT)
332
+ }
333
+ addState(
334
+ intArrayOf(android.R.attr.state_pressed),
335
+ fill(safeColor(accessory.optString("pressedBackgroundColor").takeIf(String::isNotEmpty),
336
+ color(theme, "rowPressedBackground", "#00000017"))),
337
+ )
338
+ addState(
339
+ intArrayOf(),
340
+ fill(safeColor(accessory.optString("backgroundColor").takeIf(String::isNotEmpty),
341
+ color(theme, "strongBackground", "#0000000F"))),
342
+ )
343
+ }
344
+ view.setTag(
345
+ com.facebook.react.R.id.react_test_id,
346
+ accessory.optString("testID").takeIf(String::isNotEmpty),
347
+ )
348
+ view.accessibilityDelegate = buttonAccessibilityDelegate
349
+ view.contentDescription =
350
+ accessory.optString("accessibilityLabel").ifEmpty { accessory.optString("label") }
351
+ view.isEnabled = !disabled
352
+ view.alpha = if (disabled) 0.4f else 1f
353
+ view.isClickable = true
354
+ view.setOnTouchListener { _, event ->
355
+ when (event.actionMasked) {
356
+ MotionEvent.ACTION_DOWN -> {
357
+ buttonDownEpochs[buttonIndex] = epoch
358
+ false
359
+ }
360
+ MotionEvent.ACTION_UP -> {
361
+ if (buttonDownEpochs[buttonIndex] != bindingEpoch) {
362
+ view.isPressed = false
363
+ true
364
+ } else false
365
+ }
366
+ MotionEvent.ACTION_CANCEL -> {
367
+ buttonDownEpochs[buttonIndex] = -1
368
+ false
369
+ }
370
+ else -> false
371
+ }
372
+ }
373
+ view.setOnClickListener {
374
+ if (epoch != bindingEpoch) return@setOnClickListener
375
+ (tag as? NativeListItem)?.takeUnless { it.json.optBoolean("disabled", false) }
376
+ ?.takeUnless { accessory.optBoolean("disabled", false) }
377
+ ?.let {
378
+ emitAction(it, accessory.optString("actionKey"), null, view,
379
+ "trailingAccessory", index)
380
+ }
381
+ }
382
+ textIndex++
383
+ }
260
384
  "value" -> {
261
385
  showTrailing(
262
386
  textIndex,
@@ -269,6 +393,21 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
269
393
  }
270
394
  "valuePair" -> {
271
395
  showTrailingValuePair(textIndex, accessory, theme)
396
+ accessory.optJSONObject("primaryHelp")?.let { help ->
397
+ val view = trailingViews[textIndex]
398
+ val epoch = bindingEpoch
399
+ view.primaryHelpOnly = true
400
+ view.guardsHelpTouches = true
401
+ view.showsHelpUnderline = help.optString("underline") == "dashed"
402
+ if (help.optString("presentation") == "popover") {
403
+ view.setOnClickListener {
404
+ if (epoch == bindingEpoch && !item.json.optBoolean("disabled")) {
405
+ emitAction(item, help.optString("actionKey"), null, view, "trailingAccessory", index)
406
+ }
407
+ }
408
+ }
409
+ }
410
+ segmentedValuePairs[trailingViews[textIndex]] = accessory
272
411
  semanticValueViews.add(trailingViews[textIndex++])
273
412
  }
274
413
  "checkbox" -> bindCheckbox(item, accessory, checkboxState)
@@ -428,8 +567,10 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
428
567
 
429
568
  private fun showTrailingValuePair(index: Int, accessory: JSONObject, theme: JSONObject?) {
430
569
  if (index !in trailingViews.indices) return
431
- val primary = accessory.optString("primary")
432
- val secondary = accessory.optString("secondary")
570
+ val primarySegments = accessory.optJSONArray("primaryTextSegments")
571
+ val secondarySegments = accessory.optJSONArray("secondaryTextSegments")
572
+ val primary = segmentTextOrFallback(primarySegments, accessory.optString("primary"))
573
+ val secondary = segmentTextOrFallback(secondarySegments, accessory.optString("secondary"))
433
574
  if (primary.isEmpty() && secondary.isEmpty()) return
434
575
  val value = SpannableStringBuilder()
435
576
  val primaryStart = value.length
@@ -449,6 +590,15 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
449
590
  value.length,
450
591
  Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
451
592
  )
593
+ if (primary.isNotEmpty() && accessory.optString("primaryFontWeight") == "medium") {
594
+ value.setSpan(
595
+ ValuePairTypefaceSpan(fonts.medium(context)),
596
+ primaryStart,
597
+ value.length,
598
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
599
+ )
600
+ }
601
+ applyPairSubscriptSizes(value, primarySegments, primaryStart, 16f)
452
602
  if (primary.isNotEmpty() && secondary.isNotEmpty()) value.append('\n')
453
603
  val secondaryStart = value.length
454
604
  value.append(secondary)
@@ -469,6 +619,7 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
469
619
  value.length,
470
620
  Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
471
621
  )
622
+ applyPairSubscriptSizes(value, secondarySegments, secondaryStart, 14f)
472
623
  val view = trailingViews[index]
473
624
  view.text = value
474
625
  view.maxLines = 2
@@ -480,6 +631,48 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
480
631
  view.visibility = VISIBLE
481
632
  }
482
633
 
634
+ private fun segmentTextOrFallback(segments: JSONArray?, fallback: String): String {
635
+ if (segments == null || segments.length() == 0) return fallback
636
+ return buildString {
637
+ for (index in 0 until segments.length()) append(segments.getJSONObject(index).optString("text"))
638
+ }
639
+ }
640
+
641
+ private fun applyPairSubscriptSizes(
642
+ value: SpannableStringBuilder,
643
+ segments: JSONArray?,
644
+ start: Int,
645
+ baseSize: Float,
646
+ ) {
647
+ if (segments == null) return
648
+ var offset = start
649
+ for (index in 0 until segments.length()) {
650
+ val segment = segments.getJSONObject(index)
651
+ val end = offset + segment.optString("text").length
652
+ if (end > offset && segment.optString("style") == "subscript")
653
+ value.setSpan(
654
+ AbsoluteSizeSpan(sp(kotlin.math.ceil(baseSize * 0.6).toFloat()).roundToInt(), true),
655
+ offset,
656
+ end,
657
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
658
+ )
659
+ offset = end
660
+ }
661
+ }
662
+
663
+ private fun restorePairSubscriptSizes(view: TextView, accessory: JSONObject, baseSize: Float) {
664
+ val primary = accessory.optJSONArray("primaryTextSegments")
665
+ val secondary = accessory.optJSONArray("secondaryTextSegments")
666
+ if ((primary?.length() ?: 0) == 0 && (secondary?.length() ?: 0) == 0) return
667
+ val value = SpannableStringBuilder(view.text)
668
+ applyPairSubscriptSizes(value, primary, 0, baseSize)
669
+ val primaryText = segmentTextOrFallback(primary, accessory.optString("primary"))
670
+ val secondaryStart = primaryText.length + if (primaryText.isNotEmpty() &&
671
+ segmentTextOrFallback(secondary, accessory.optString("secondary")).isNotEmpty()) 1 else 0
672
+ applyPairSubscriptSizes(value, secondary, secondaryStart, baseSize)
673
+ view.text = value
674
+ }
675
+
483
676
  private fun showTrailingMenu(index: Int, actionKey: String) {
484
677
  showTrailing(index, "⋮", true, actionKey)
485
678
  if (index !in trailingViews.indices) return
@@ -519,7 +712,26 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
519
712
  )
520
713
  icon.accessibilityDelegate = selectorAccessibilityDelegate
521
714
  icon.contentDescription = data.optString("accessibilityLabel").takeIf { it.isNotEmpty() }
522
- if (item.json.optString("presentation") == "accountSelector") {
715
+ if (data.optString("buttonStyle") == "tertiary") {
716
+ icon.glyphSizeDp = 24
717
+ icon.layoutParams = LayoutParams(dp(38), dp(38)).apply {
718
+ gravity = Gravity.CENTER_VERTICAL
719
+ marginStart = -dp(7)
720
+ marginEnd = -dp(7)
721
+ }
722
+ icon.background = StateListDrawable().apply {
723
+ addState(intArrayOf(android.R.attr.state_pressed),
724
+ roundedFill(color(theme, "rowPressedBackground", "#00000017"), 19f).apply {
725
+ setStroke(dp(1), Color.TRANSPARENT)
726
+ })
727
+ addState(intArrayOf(), roundedFill(Color.TRANSPARENT, 19f).apply {
728
+ setStroke(dp(1), Color.TRANSPARENT)
729
+ })
730
+ }
731
+ clipChildren = false
732
+ clipToPadding = false
733
+ requestsUnclippedHost = true
734
+ } else if (item.json.optString("presentation") == "accountSelector") {
523
735
  icon.glyphSizeDp = 24
524
736
  val isSourceMenu = item.hasExplicitHeight && icon.iconName == "DotHorOutline"
525
737
  val size = if (item.hasExplicitHeight && icon.iconName == "PlusSmallOutline") 36 else 38
@@ -545,6 +757,13 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
545
757
  addState(intArrayOf(), roundedFill(Color.TRANSPARENT, 19f))
546
758
  }
547
759
  }
760
+ } else if (icon.iconName == "RefreshCcwOutline" && data.optString("actionKey").isEmpty()) {
761
+ icon.glyphSizeDp = 16
762
+ icon.isClickable = false
763
+ icon.isFocusable = false
764
+ icon.layoutParams = LayoutParams(dp(16), dp(16)).apply {
765
+ gravity = Gravity.CENTER_VERTICAL
766
+ }
548
767
  } else if (icon.iconName == "ChevronRightSmallOutline") {
549
768
  icon.glyphSizeDp = null
550
769
  // ListItem.DrillIn is a 24dp icon with mx=-6, for a 12dp layout footprint.
@@ -716,3 +935,13 @@ internal class NativeListAccessoryStack(context: android.content.Context, val fo
716
935
  cornerRadius = scaledDp(radiusDp)
717
936
  }
718
937
  }
938
+
939
+ private class ValuePairTypefaceSpan(private val face: Typeface) : MetricAffectingSpan() {
940
+ override fun updateDrawState(paint: TextPaint) {
941
+ paint.typeface = face
942
+ }
943
+
944
+ override fun updateMeasureState(paint: TextPaint) {
945
+ paint.typeface = face
946
+ }
947
+ }
@@ -70,10 +70,13 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
70
70
  val style = data.optJSONObject("style") ?: JSONObject()
71
71
  val image = style.optJSONObject("image") ?: JSONObject()
72
72
  val table = data.optString("presentation") == "table"
73
+ val hideLeading = data.optBoolean("hideLeading")
73
74
  val gap = if (style.has("leadingGap")) px(style.optDouble("leadingGap")) else dp(12)
74
75
  val iw = if (image.has("width")) px(image.optDouble("width")) else dp(40)
75
- visual.layoutParams = LayoutParams(iw, if (image.has("height")) px(image.optDouble("height")) else dp(40)).apply { marginEnd = gap }
76
- visual.bind(data.optJSONObject("leading") ?: JSONObject(), image, item.key, theme, false, sourceScale, data.optJSONObject("secondaryLeading"))
76
+ visual.layoutParams = LayoutParams(iw, if (image.has("height")) px(image.optDouble("height")) else dp(40)).apply { marginEnd = if (hideLeading) 0 else gap }
77
+ visual.visibility = if (hideLeading) GONE else VISIBLE
78
+ if (hideLeading) visual.recycle()
79
+ else visual.bind(data.optJSONObject("leading") ?: JSONObject(), image, item.key, theme, false, sourceScale, data.optJSONObject("secondaryLeading"))
77
80
  amounts.layoutParams = if (table) LayoutParams(0, -2, 1f).apply { marginStart = gap } else LayoutParams(-2, -2).apply { marginStart = gap }
78
81
  text(title, data.optString("title"), style.optJSONObject("title"), 16f, color("primary", theme), 24, "medium")
79
82
  text(description, data.optString("description"), style.optJSONObject("description"), 14f, color("secondary", theme), 20, lines = 2)
@@ -117,7 +120,7 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
117
120
  actions.removeAllViews()
118
121
  val actionData = data.optJSONArray("footerActions")
119
122
  actions.visibility = if ((actionData?.length() ?: 0) == 0) GONE else VISIBLE
120
- actions.layoutParams = LayoutParams(-1, -2).apply { marginStart = iw + gap; topMargin = dp(8) }
123
+ actions.layoutParams = LayoutParams(-1, -2).apply { marginStart = if (hideLeading) 0 else iw + gap; topMargin = dp(8) }
121
124
  for (index in 0 until (actionData?.length() ?: 0)) {
122
125
  val descriptor = actionData!!.getJSONObject(index)
123
126
  val button = NativeListTextView(context)
@@ -146,7 +149,7 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
146
149
  }
147
150
  label.text = styled
148
151
  }
149
- fun recycle() { visual.recycle(); amountViews.forEach { it.recycle() }; description.setOnClickListener(null); actions.removeAllViews() }
152
+ fun recycle() { visual.recycle(); visual.visibility = VISIBLE; amountViews.forEach { it.recycle() }; description.setOnClickListener(null); actions.removeAllViews() }
150
153
  fun dispose() { recycle(); visual.dispose(); amountViews.forEach { it.dispose() } }
151
154
 
152
155
  private inner class AmountView : LinearLayout(context) {
@@ -81,6 +81,7 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
81
81
  details.recycle()
82
82
  val style = data.optJSONObject("style") ?: JSONObject()
83
83
  val image = style.optJSONObject("image") ?: JSONObject()
84
+ val hideLeading = data.optBoolean("hideLeading")
84
85
  fun color(name: String, fallback: String) =
85
86
  parseNativeListColor(theme?.optString(name, fallback) ?: fallback)
86
87
  fun fill(value: Int, radius: Int) =
@@ -143,8 +144,10 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
143
144
  val iw = if (image.has("width")) stylePx(image.optDouble("width")) else dp(40)
144
145
  val ih = if (image.has("height")) stylePx(image.optDouble("height")) else dp(40)
145
146
  val gap = if (style.has("leadingGap")) stylePx(style.optDouble("leadingGap")) else dp(12)
146
- visual.layoutParams = LayoutParams(iw, ih).apply { marginEnd = gap }
147
- visual.bind(
147
+ visual.layoutParams = LayoutParams(iw, ih).apply { marginEnd = if (hideLeading) 0 else gap }
148
+ visual.visibility = if (hideLeading) GONE else VISIBLE
149
+ if (hideLeading) visual.recycle()
150
+ else visual.bind(
148
151
  // Legacy: a missing visual still reserves an empty, transparent slot.
149
152
  data.optJSONObject("leading") ?: JSONObject().put("backgroundColor", "#00000000"),
150
153
  image,
@@ -212,7 +215,7 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
212
215
  actions.visibility = if (count == 0) GONE else VISIBLE
213
216
  actions.layoutParams =
214
217
  LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
215
- marginStart = if (style.has("leadingGap") || image.has("width")) iw + gap else dp(52)
218
+ marginStart = if (hideLeading) 0 else if (style.has("leadingGap") || image.has("width")) iw + gap else dp(52)
216
219
  topMargin = if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else dp(8)
217
220
  bottomMargin = dp(4)
218
221
  }
@@ -0,0 +1,102 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.content.res.ColorStateList
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.graphics.drawable.StateListDrawable
7
+ import android.view.Gravity
8
+ import android.view.View
9
+ import android.widget.FrameLayout
10
+ import android.widget.LinearLayout
11
+ import android.widget.ProgressBar
12
+ import androidx.core.widget.TextViewCompat
13
+ import com.facebook.react.uimanager.ThemedReactContext
14
+ import org.json.JSONObject
15
+
16
+ internal class NativeListButtonGroupRowView(context: ThemedReactContext, fonts: NativeListFonts) :
17
+ NativeListRendererRowView(context, fonts) {
18
+ private var generation = 0
19
+ private var rowOpacity = 1f
20
+
21
+ override fun appearance() {
22
+ super.appearance()
23
+ alpha = rowOpacity
24
+ }
25
+ override fun defaultHeight(item: NativeListItem, layout: String) = 64
26
+
27
+ override fun bindContent(
28
+ item: NativeListItem,
29
+ theme: JSONObject?,
30
+ layout: String,
31
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
32
+ ) {
33
+ recycleContent()
34
+ rowOpacity = (item.json.optJSONObject("style")?.optJSONObject("container")?.optDouble("opacity", item.json.optDouble("opacity", 1.0)) ?: item.json.optDouble("opacity", 1.0)).toFloat()
35
+ orientation = HORIZONTAL
36
+ gravity = Gravity.CENTER_VERTICAL
37
+ setPadding(0, dp(8), 0, dp(8))
38
+ val info = parseNativeListColor(theme?.optString("info", "#0F69F6") ?: "#0F69F6")
39
+ val descriptors = item.json.optJSONArray("buttons") ?: return
40
+ val epoch = generation
41
+ for (index in 0 until descriptors.length()) {
42
+ val descriptor = descriptors.getJSONObject(index)
43
+ val enabled = !item.json.optBoolean("disabled") && !descriptor.optBoolean("disabled") && !descriptor.optBoolean("loading")
44
+ val button = FrameLayout(context).apply {
45
+ isEnabled = enabled
46
+ alpha = if (enabled) 1f else 0.4f
47
+ contentDescription = descriptor.optString("accessibilityLabel", descriptor.optString("label"))
48
+ setTag(com.facebook.react.R.id.react_test_id, descriptor.optString("testID").takeIf { it.isNotEmpty() })
49
+ background = StateListDrawable().apply {
50
+ fun fill(pressed: Boolean) = GradientDrawable().apply {
51
+ setColor(if (pressed) (info and 0x00FFFFFF) or (43 shl 24) else Color.TRANSPARENT)
52
+ cornerRadius = dp(12).toFloat()
53
+ setStroke(dp(1), (info and 0x00FFFFFF) or (89 shl 24))
54
+ }
55
+ addState(intArrayOf(android.R.attr.state_pressed), fill(true))
56
+ addState(intArrayOf(), fill(false))
57
+ }
58
+ }
59
+ val label = NativeListTextView(context).apply {
60
+ text = descriptor.optString("label")
61
+ textSize = NativeListScale.font(resources, 14f)
62
+ typeface = fonts.medium(context)
63
+ setTextColor(info)
64
+ gravity = Gravity.CENTER
65
+ includeFontPadding = false
66
+ maxLines = 1
67
+ ellipsize = android.text.TextUtils.TruncateAt.END
68
+ TextViewCompat.setLineHeight(this, dp(20))
69
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
70
+ }
71
+ val content = LinearLayout(context).apply {
72
+ orientation = HORIZONTAL
73
+ gravity = Gravity.CENTER
74
+ setPadding(dp(8), dp(12), dp(8), dp(12))
75
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
76
+ }
77
+ button.addView(content, FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
78
+ if (descriptor.optBoolean("loading")) {
79
+ val spinner = ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
80
+ isIndeterminate = true
81
+ indeterminateTintList = ColorStateList.valueOf(info)
82
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
83
+ }
84
+ content.addView(spinner, LayoutParams(dp(20), dp(20)).apply { marginEnd = dp(8) })
85
+ }
86
+ content.addView(label, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
87
+ button.setOnClickListener {
88
+ if (generation == epoch && enabled) emitAction(descriptor.optString("actionKey"), button, "buttonGroup", index)
89
+ }
90
+ button.isClickable = enabled
91
+ addView(button, LayoutParams(0, dp(48), 1f).apply { if (index > 0) marginStart = dp(10) })
92
+ }
93
+ }
94
+
95
+ override fun recycleContent() {
96
+ generation += 1
97
+ for (index in 0 until childCount) getChildAt(index).setOnClickListener(null)
98
+ removeAllViews()
99
+ }
100
+
101
+ override fun disposeContent() = recycleContent()
102
+ }
@@ -11,6 +11,15 @@ internal class NativeListContainerSlotAdapter : RecyclerView.Adapter<NativeListC
11
11
  private var content: View? = null
12
12
  private var height = 0
13
13
  private var enabled = true
14
+ private var boundHolder: Holder? = null
15
+ private var visualTranslationY = 0f
16
+
17
+ fun contentView(): View? = content
18
+
19
+ fun setVisualTranslationY(value: Float) {
20
+ visualTranslationY = value
21
+ boundHolder?.container?.translationY = value
22
+ }
14
23
 
15
24
  fun update(view: View?, heightPx: Int, visible: Boolean) {
16
25
  val previousCount = itemCount
@@ -39,6 +48,8 @@ internal class NativeListContainerSlotAdapter : RecyclerView.Adapter<NativeListC
39
48
  }
40
49
 
41
50
  override fun onBindViewHolder(holder: Holder, position: Int) {
51
+ boundHolder = holder
52
+ holder.container.translationY = visualTranslationY
42
53
  holder.container.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
43
54
  holder.container.removeAllViews()
44
55
  content?.let { view ->
@@ -48,6 +59,7 @@ internal class NativeListContainerSlotAdapter : RecyclerView.Adapter<NativeListC
48
59
  }
49
60
 
50
61
  override fun onViewRecycled(holder: Holder) {
62
+ if (boundHolder === holder) boundHolder = null
51
63
  holder.container.removeAllViews()
52
64
  }
53
65
  }