@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.250

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 (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -0,0 +1,711 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ // OneKey patch: selector checkboxes share the source React Native border/background renderer.
4
+ import android.graphics.Color
5
+ import android.graphics.Typeface
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.graphics.drawable.StateListDrawable
8
+ import android.text.Spannable
9
+ import android.text.SpannableStringBuilder
10
+ import android.text.TextUtils
11
+ import android.text.style.AbsoluteSizeSpan
12
+ import android.text.style.ForegroundColorSpan
13
+ import android.util.TypedValue
14
+ import android.view.Gravity
15
+ import android.view.View
16
+ import android.widget.LinearLayout
17
+ import android.widget.ProgressBar
18
+ import android.widget.TextView
19
+ import androidx.core.widget.TextViewCompat
20
+ import com.facebook.react.uimanager.BackgroundStyleApplicator
21
+ import com.facebook.react.uimanager.LengthPercentage
22
+ import com.facebook.react.uimanager.LengthPercentageType
23
+ import com.facebook.react.uimanager.style.BorderRadiusProp
24
+ import com.facebook.react.uimanager.style.LogicalEdge
25
+ import kotlin.math.roundToInt
26
+ import org.json.JSONArray
27
+ import org.json.JSONObject
28
+
29
+ // Bounded controls only. Event ownership and epochs stay with the row host.
30
+ internal class NativeListAccessoryStack(context: android.content.Context) : LinearLayout(context) {
31
+ private val trailingViews = List(2) { NativeListTextView(context) }
32
+ private val trailingIcons = List(2) { OneKeyIconView(context) }
33
+ private val checkbox = OneKeyCheckboxView(context)
34
+ private val spinner = ProgressBar(context)
35
+ private var boundCheckboxData: JSONObject? = null
36
+ private val semanticValueViews = mutableListOf<TextView>()
37
+ private var selectorUsesSourceScale = false
38
+ private var checkboxUsesSelectorStyle = false
39
+ private var checkboxCheckedColor = Color.BLACK
40
+ private var checkboxUncheckedColor = Color.WHITE
41
+ private var checkboxIconColor = Color.WHITE
42
+ private var checkboxBorderColor = Color.LTGRAY
43
+ private var iconSubduedColor = Color.GRAY
44
+ val firstVisibleIcon: View?
45
+ get() = trailingIcons.firstOrNull { it.visibility == VISIBLE }
46
+
47
+ val hasCheckbox: Boolean
48
+ get() = checkbox.visibility == VISIBLE
49
+
50
+ val visibleValues: List<TextView>
51
+ get() = trailingViews.filter { it.visibility == VISIBLE }
52
+
53
+ var endInset: Int? = null
54
+
55
+ /** Legacy: overhanging selector controls require the host row not to clip its padding. */
56
+ var requestsUnclippedHost = false
57
+ private set
58
+ var onAction: ((String, View, Int?, NativeSelectionTarget?) -> Unit)? = null
59
+ private val selectorAccessibilityDelegate =
60
+ object : View.AccessibilityDelegate() {
61
+ override fun onInitializeAccessibilityNodeInfo(
62
+ host: View,
63
+ info: android.view.accessibility.AccessibilityNodeInfo,
64
+ ) {
65
+ super.onInitializeAccessibilityNodeInfo(host, info)
66
+ info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
67
+ }
68
+ }
69
+
70
+ private val trailingPaintFlags = trailingViews.map { it.paintFlags }
71
+
72
+ /** The caller's `value` / `valueSecondary` text style bound to [view], if any. */
73
+ fun explicitTextStyle(view: TextView, style: JSONObject?): JSONObject? =
74
+ when (semanticValueViews.indexOf(view)) {
75
+ 0 -> style?.optJSONObject("value")
76
+ 1 -> style?.optJSONObject("valueSecondary")
77
+ else -> null
78
+ }
79
+
80
+ /** Legacy selector source-scale typography for every visible trailing text. */
81
+ fun applySourceTypography(style: JSONObject?) {
82
+ trailingViews.forEach { view ->
83
+ if (view.visibility == VISIBLE)
84
+ applyNativeListSourceTypography(view, explicitTextStyle(view, style))
85
+ }
86
+ }
87
+
88
+ init {
89
+ orientation = VERTICAL
90
+ gravity = Gravity.END or Gravity.CENTER_VERTICAL
91
+ trailingViews.forEach { addView(it) }
92
+ trailingIcons.forEach { addView(it) }
93
+ addView(checkbox)
94
+ addView(spinner)
95
+ }
96
+
97
+ fun bind(
98
+ item: NativeListItem,
99
+ descriptors: JSONArray,
100
+ theme: JSONObject?,
101
+ style: JSONObject,
102
+ sourceScale: Boolean,
103
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
104
+ horizontal: Boolean = false,
105
+ ) {
106
+ selectorUsesSourceScale = sourceScale
107
+ reset()
108
+ tag = item
109
+ val primaryText = color(theme, "primaryText", "#000000DF")
110
+ checkboxCheckedColor = primaryText
111
+ checkboxUncheckedColor = color(theme, "inverseText", "#FCFCFC")
112
+ checkboxIconColor = checkboxUncheckedColor
113
+ checkboxBorderColor = Color.argb(0x31, 0, 0, 0)
114
+ iconSubduedColor = color(theme, "iconSubdued", "#00000072")
115
+ checkboxUsesSelectorStyle = item.json.optString("presentation") == "networkSelector"
116
+ if (checkboxUsesSelectorStyle) {
117
+ checkboxCheckedColor = color(theme, "checkboxBackground", "#202020")
118
+ checkboxBorderColor = color(theme, "checkboxBorder", "#00000031")
119
+ checkboxIconColor = color(theme, "checkboxIcon", "#FFFFFF")
120
+ checkboxUncheckedColor = checkboxIconColor
121
+ }
122
+ val kinds =
123
+ (0 until descriptors.length()).map { descriptors.getJSONObject(it).optString("kind") }
124
+ orientation = if (horizontal) HORIZONTAL else VERTICAL
125
+ gravity = Gravity.END or Gravity.CENTER_VERTICAL
126
+ // Legacy: trailing text uses primaryText, never the selector checkbox fill.
127
+ trailingViews.forEach { it.setTextColor(primaryText) }
128
+ bindAccessories(item, descriptors, theme, checkboxState)
129
+ if (horizontal && "checkbox" in kinds)
130
+ (checkbox.layoutParams as LayoutParams).marginStart = dp(12)
131
+ for ((index, key) in listOf("value", "valueSecondary").withIndex()) {
132
+ style.optJSONObject(key)?.let { value ->
133
+ semanticValueViews.getOrNull(index)?.let { applyStyledText(it, value) }
134
+ }
135
+ }
136
+ if (style.has("trailingGap")) {
137
+ var previous = false
138
+ for (i in 0 until childCount) {
139
+ val v = getChildAt(i)
140
+ if (v.visibility != GONE) {
141
+ val params = v.layoutParams as LayoutParams
142
+ if (orientation == HORIZONTAL)
143
+ params.marginStart = if (previous) styleDp(style.optDouble("trailingGap")) else 0
144
+ else params.topMargin = if (previous) styleDp(style.optDouble("trailingGap")) else 0
145
+ previous = true
146
+ }
147
+ }
148
+ }
149
+ visibility =
150
+ if ((0 until childCount).any { getChildAt(it).visibility != GONE }) VISIBLE else GONE
151
+ }
152
+
153
+ fun updateSelection(
154
+ item: NativeListItem,
155
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
156
+ ) {
157
+ tag = item
158
+ if (boundCheckboxData != null) {
159
+ val descriptors = item.json.optJSONArray("trailing")
160
+ val latest =
161
+ (0 until (descriptors?.length() ?: 0))
162
+ .mapNotNull { descriptors?.optJSONObject(it) }
163
+ .lastOrNull { it.optString("kind") == "checkbox" } ?: item.json.optJSONObject("checkbox")
164
+ latest?.let { bindCheckbox(item, it, checkboxState) }
165
+ }
166
+ }
167
+
168
+ fun anchorInset(view: View): Int =
169
+ if (
170
+ (tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" &&
171
+ view in trailingIcons &&
172
+ (view.layoutParams as MarginLayoutParams).marginStart < 0
173
+ )
174
+ dp(7)
175
+ else 0
176
+
177
+ fun reset() {
178
+ tag = null
179
+ endInset = null
180
+ requestsUnclippedHost = false
181
+ clipChildren = true
182
+ clipToPadding = true
183
+ semanticValueViews.clear()
184
+ boundCheckboxData = null
185
+ trailingViews.forEachIndexed { index, v ->
186
+ v.paintFlags = trailingPaintFlags[index]
187
+ v.visibility = GONE
188
+ v.text = ""
189
+ v.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
190
+ v.setOnClickListener(null)
191
+ v.background = null
192
+ v.setPadding(0, 0, 0, 0)
193
+ // Legacy trailing text: END gravity, one line, no ellipsis.
194
+ v.gravity = Gravity.END
195
+ v.opticalOffsetY = 0f
196
+ v.maxLines = 1
197
+ v.setHorizontallyScrolling(false)
198
+ v.ellipsize = null
199
+ v.setLineSpacing(0f, 1f)
200
+ v.letterSpacing = 0f
201
+ }
202
+ trailingIcons.forEach {
203
+ it.visibility = GONE
204
+ it.setOnClickListener(null)
205
+ it.background = null
206
+ it.useSourceScale = selectorUsesSourceScale
207
+ it.glyphSizeDp = null
208
+ it.alpha = 1f
209
+ }
210
+ checkbox.visibility = GONE
211
+ checkbox.setOnClickListener(null)
212
+ checkbox.alpha = 1f
213
+ checkbox.layoutParams = LayoutParams(dp(20), dp(20))
214
+ spinner.visibility = GONE
215
+ spinner.alpha = 1f
216
+ spinner.layoutParams = LayoutParams(dp(20), dp(20))
217
+ }
218
+
219
+ private fun emitAction(
220
+ item: NativeListItem,
221
+ key: String,
222
+ target: NativeSelectionTarget?,
223
+ view: View,
224
+ source: String,
225
+ slot: Int? = null,
226
+ ) {
227
+ // Row-level disabled gating is per control (see showTrailing/bindCheckbox), as in legacy.
228
+ onAction?.invoke(key, view, slot, target)
229
+ }
230
+
231
+ private fun dp(value: Int) =
232
+ if (selectorUsesSourceScale) (value * resources.displayMetrics.density).roundToInt()
233
+ else NativeListScale.dp(resources, value)
234
+
235
+ private fun scaledDp(value: Float) =
236
+ if (selectorUsesSourceScale) value * resources.displayMetrics.density
237
+ else NativeListScale.dp(resources, value)
238
+
239
+ private fun sp(value: Float) =
240
+ if (selectorUsesSourceScale) value else NativeListScale.font(resources, value)
241
+
242
+ private fun styleDp(value: Double) = (value * resources.displayMetrics.density).roundToInt()
243
+
244
+ private fun bindAccessories(
245
+ item: NativeListItem,
246
+ accessories: JSONArray?,
247
+ theme: JSONObject?,
248
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
249
+ ) {
250
+ if (accessories == null) return
251
+ var textIndex = 0
252
+ for (index in 0 until minOf(2, accessories.length())) {
253
+ val accessory = accessories.getJSONObject(index)
254
+ when (accessory.optString("kind")) {
255
+ "value" -> {
256
+ showTrailing(
257
+ textIndex,
258
+ accessory.optString("text"),
259
+ !accessory.optBoolean("secondary", false),
260
+ )
261
+ semanticValueViews.add(trailingViews[textIndex])
262
+ applyValueSegments(trailingViews[textIndex], accessory.optJSONArray("textSegments"))
263
+ textIndex++
264
+ }
265
+ "valuePair" -> {
266
+ showTrailingValuePair(textIndex, accessory, theme)
267
+ semanticValueViews.add(trailingViews[textIndex++])
268
+ }
269
+ "checkbox" -> bindCheckbox(item, accessory, checkboxState)
270
+ "radio" ->
271
+ showTrailing(
272
+ textIndex++,
273
+ if (accessory.optBoolean("checked")) "●" else "○",
274
+ true,
275
+ accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
276
+ )
277
+ "switch" ->
278
+ showTrailing(
279
+ textIndex++,
280
+ if (accessory.optBoolean("value")) "ON" else "OFF",
281
+ true,
282
+ accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
283
+ )
284
+ "chevron" ->
285
+ showTrailingIcon(
286
+ textIndex++,
287
+ item,
288
+ JSONObject(accessory.toString()).apply {
289
+ put("name", "ChevronRightSmallOutline")
290
+ if (optString("actionKey").isEmpty()) put("actionKey", "press")
291
+ },
292
+ theme,
293
+ )
294
+ "menu" -> showTrailingMenu(textIndex++, accessory.optString("actionKey"))
295
+ "drag" ->
296
+ showTrailingIcon(
297
+ textIndex++,
298
+ item,
299
+ JSONObject(accessory.toString()).apply { put("name", "DragOutline") },
300
+ theme,
301
+ )
302
+ "icon" -> showTrailingIcon(textIndex++, item, accessory, theme)
303
+ "spinner" -> spinner.visibility = VISIBLE
304
+ "progress" ->
305
+ showTrailing(textIndex++, "${(accessory.optDouble("value") * 100).toInt()}%", false)
306
+ }
307
+ }
308
+ }
309
+
310
+ private fun bindCheckbox(
311
+ item: NativeListItem,
312
+ json: JSONObject,
313
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
314
+ ) {
315
+ boundCheckboxData = json
316
+ val targetJson = json.optJSONObject("target")
317
+ val scope = targetJson?.optString("scope", "row") ?: "row"
318
+ val target =
319
+ NativeSelectionTarget(
320
+ scope,
321
+ when (scope) {
322
+ "section" -> targetJson?.optString("sectionKey")
323
+ "row" -> item.key
324
+ else -> null
325
+ },
326
+ )
327
+ val state = checkboxState(item, target, json.optString("state", "unchecked"))
328
+ val accessoryDisabled = json.optBoolean("disabled", false)
329
+ if (json.optBoolean("loading", false)) {
330
+ checkbox.visibility = GONE
331
+ checkbox.setOnClickListener(null)
332
+ spinner.visibility = VISIBLE
333
+ spinner.alpha =
334
+ if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
335
+ return
336
+ }
337
+ checkbox.visibility = VISIBLE
338
+ checkbox.setState(state, checkboxIconColor)
339
+ val usesSourceCheckboxGeometry = checkboxUsesSelectorStyle && item.hasExplicitHeight
340
+ checkbox.usesSelectorGeometry = usesSourceCheckboxGeometry
341
+ if (usesSourceCheckboxGeometry) {
342
+ // OneKey patch: source Yoga children may round into padding; retain their complete border.
343
+ clipToPadding = false
344
+ requestsUnclippedHost = true
345
+ // OneKey patch: even a transparent source border changes RN's background clipping path.
346
+ checkbox.background = null
347
+ BackgroundStyleApplicator.setBackgroundColor(
348
+ checkbox,
349
+ if (state == "unchecked") checkboxUncheckedColor else checkboxCheckedColor,
350
+ )
351
+ BackgroundStyleApplicator.setBorderWidth(checkbox, LogicalEdge.ALL, 2f)
352
+ BackgroundStyleApplicator.setBorderColor(
353
+ checkbox,
354
+ LogicalEdge.ALL,
355
+ if (state == "unchecked") checkboxBorderColor else Color.TRANSPARENT,
356
+ )
357
+ BackgroundStyleApplicator.setBorderRadius(
358
+ checkbox,
359
+ BorderRadiusProp.BORDER_RADIUS,
360
+ LengthPercentage(4f, LengthPercentageType.POINT),
361
+ )
362
+ } else {
363
+ checkbox.background =
364
+ if (state == "unchecked") {
365
+ if (checkboxUsesSelectorStyle)
366
+ GradientDrawable().apply {
367
+ setColor(checkboxUncheckedColor)
368
+ setStroke(dp(2), checkboxBorderColor)
369
+ cornerRadius = scaledDp(4f)
370
+ }
371
+ else roundedStroke(checkboxBorderColor, checkboxUncheckedColor, 4f)
372
+ } else {
373
+ roundedFill(checkboxCheckedColor, 4f)
374
+ }
375
+ }
376
+ // Row-level disabled opacity already applies to this child. Only apply a
377
+ // local 0.5 when the accessory alone is disabled, never 0.5 * 0.5.
378
+ checkbox.alpha = if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
379
+ checkbox.isEnabled =
380
+ !item.json.optBoolean("disabled", false) &&
381
+ !accessoryDisabled &&
382
+ !json.optBoolean("loading", false)
383
+ checkbox.setOnClickListener {
384
+ emitAction(
385
+ item,
386
+ json.optString("actionKey", "selection"),
387
+ target,
388
+ checkbox,
389
+ "trailingAccessory",
390
+ )
391
+ }
392
+ }
393
+
394
+ private fun showTrailing(
395
+ index: Int,
396
+ value: String,
397
+ primary: Boolean,
398
+ actionKey: String? = null,
399
+ textColor: Int? = null,
400
+ ) {
401
+ if (index !in trailingViews.indices || value.isEmpty()) return
402
+ val view = trailingViews[index]
403
+ view.maxLines = 1
404
+ view.text = value
405
+ view.textSize = sp(if (primary) 16f else 14f)
406
+ view.typeface =
407
+ if (primary) NativeListFonts.medium(context) else NativeListFonts.regular(context)
408
+ view.includeFontPadding = false
409
+ view.fontFeatureSettings = "tnum"
410
+ TextViewCompat.setLineHeight(view, dp(if (primary) 24 else 20))
411
+ textColor?.let(view::setTextColor)
412
+ view.visibility = VISIBLE
413
+ if (!actionKey.isNullOrEmpty()) {
414
+ view.setOnClickListener {
415
+ (tag as? NativeListItem)
416
+ ?.takeUnless { item -> item.json.optBoolean("disabled", false) }
417
+ ?.let { item -> emitAction(item, actionKey, null, view, "trailingAccessory", index) }
418
+ }
419
+ }
420
+ }
421
+
422
+ private fun showTrailingValuePair(index: Int, accessory: JSONObject, theme: JSONObject?) {
423
+ if (index !in trailingViews.indices) return
424
+ val primary = accessory.optString("primary")
425
+ val secondary = accessory.optString("secondary")
426
+ if (primary.isEmpty() && secondary.isEmpty()) return
427
+ val value = SpannableStringBuilder()
428
+ val primaryStart = value.length
429
+ value.append(primary)
430
+ value.setSpan(
431
+ ForegroundColorSpan(accessoryTextColor(accessory.optString("primaryTone"), "primary", theme)),
432
+ primaryStart,
433
+ value.length,
434
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
435
+ )
436
+ value.setSpan(
437
+ AbsoluteSizeSpan(
438
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(16f), resources.displayMetrics)
439
+ .roundToInt()
440
+ ),
441
+ primaryStart,
442
+ value.length,
443
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
444
+ )
445
+ if (primary.isNotEmpty() && secondary.isNotEmpty()) value.append('\n')
446
+ val secondaryStart = value.length
447
+ value.append(secondary)
448
+ value.setSpan(
449
+ ForegroundColorSpan(
450
+ accessoryTextColor(accessory.optString("secondaryTone"), "secondary", theme)
451
+ ),
452
+ secondaryStart,
453
+ value.length,
454
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
455
+ )
456
+ value.setSpan(
457
+ AbsoluteSizeSpan(
458
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(14f), resources.displayMetrics)
459
+ .roundToInt()
460
+ ),
461
+ secondaryStart,
462
+ value.length,
463
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
464
+ )
465
+ val view = trailingViews[index]
466
+ view.text = value
467
+ view.maxLines = 2
468
+ view.gravity = Gravity.END
469
+ view.includeFontPadding = false
470
+ view.typeface = NativeListFonts.regular(context)
471
+ view.fontFeatureSettings = "tnum"
472
+ TextViewCompat.setLineHeight(view, dp(20))
473
+ view.visibility = VISIBLE
474
+ }
475
+
476
+ private fun showTrailingMenu(index: Int, actionKey: String) {
477
+ showTrailing(index, "⋮", true, actionKey)
478
+ if (index !in trailingViews.indices) return
479
+ trailingViews[index].apply {
480
+ gravity = Gravity.CENTER
481
+ textSize = sp(20f)
482
+ layoutParams =
483
+ LayoutParams(dp(24), dp(24)).apply {
484
+ if (this@NativeListAccessoryStack.orientation == HORIZONTAL && index > 0)
485
+ marginStart = dp(8)
486
+ }
487
+ }
488
+ }
489
+
490
+ private fun accessoryTextColor(tone: String, defaultTone: String, theme: JSONObject?): Int =
491
+ when (tone.ifEmpty { defaultTone }) {
492
+ "positive" -> color(theme, "positive", "#00713FDE")
493
+ "negative" -> color(theme, "negative", "#C40006D3")
494
+ "secondary" -> color(theme, "secondaryText", "#0000009B")
495
+ else -> color(theme, "primaryText", "#000000DF")
496
+ }
497
+
498
+ private fun showTrailingIcon(
499
+ index: Int,
500
+ item: NativeListItem,
501
+ data: JSONObject,
502
+ theme: JSONObject?,
503
+ ) {
504
+ if (index !in trailingIcons.indices) return
505
+ val icon = trailingIcons[index]
506
+ icon.iconName = data.optString("name")
507
+ icon.tintColor = safeColor(data.optString("tintColor"), iconSubduedColor)
508
+ // OneKey patch: preserve the original 38dp press target around its 24dp layout slot.
509
+ icon.setTag(
510
+ com.facebook.react.R.id.react_test_id,
511
+ data.optString("testID").takeIf { it.isNotEmpty() },
512
+ )
513
+ icon.accessibilityDelegate = selectorAccessibilityDelegate
514
+ icon.contentDescription = data.optString("accessibilityLabel").takeIf { it.isNotEmpty() }
515
+ if (item.json.optString("presentation") == "accountSelector") {
516
+ icon.glyphSizeDp = 24
517
+ val isSourceMenu = item.hasExplicitHeight && icon.iconName == "DotHorOutline"
518
+ val size = if (item.hasExplicitHeight && icon.iconName == "PlusSmallOutline") 36 else 38
519
+ icon.layoutParams =
520
+ LayoutParams(dp(size), dp(size)).apply {
521
+ gravity = Gravity.CENTER_VERTICAL
522
+ marginStart = -dp(7)
523
+ marginEnd = -dp(7)
524
+ }
525
+ // The frame overhangs its 24dp layout slot by 7dp on each side. The column
526
+ // clips children and the row clips to padding by default, which would cut
527
+ // the pressed circle down to a 24dp-wide pill.
528
+ this@NativeListAccessoryStack.clipChildren = false
529
+ clipToPadding = false
530
+ requestsUnclippedHost = true
531
+ if (isSourceMenu && data.optString("actionKey").isNotEmpty()) {
532
+ icon.background =
533
+ StateListDrawable().apply {
534
+ addState(
535
+ intArrayOf(android.R.attr.state_pressed),
536
+ roundedFill(color(theme, "rowPressedBackground", "#00000017"), 19f),
537
+ )
538
+ addState(intArrayOf(), roundedFill(Color.TRANSPARENT, 19f))
539
+ }
540
+ }
541
+ } else if (icon.iconName == "ChevronRightSmallOutline") {
542
+ icon.glyphSizeDp = null
543
+ // ListItem.DrillIn is a 24dp icon with mx=-6, for a 12dp layout footprint.
544
+ icon.layoutParams =
545
+ LayoutParams(dp(24), dp(24)).apply {
546
+ marginStart = -dp(6)
547
+ marginEnd = -dp(6)
548
+ }
549
+ } else {
550
+ // ListItem.IconButton medium keeps a 24dp glyph in a 36dp frame. Its
551
+ // m=-7 moves the last frame 7dp through the row's trailing padding. For
552
+ // the Bookmark XStack, gap=$6 plus both negative margins leaves 10dp
553
+ // between the physical frames (46dp between glyph centers).
554
+ icon.glyphSizeDp = 24
555
+ endInset = dp(5)
556
+ icon.layoutParams =
557
+ LayoutParams(dp(36), dp(36)).apply {
558
+ gravity = Gravity.END
559
+ if (this@NativeListAccessoryStack.orientation == HORIZONTAL && index > 0) {
560
+ marginStart = dp(10)
561
+ }
562
+ }
563
+ }
564
+ icon.visibility = VISIBLE
565
+ icon.isEnabled = !data.optBoolean("disabled", false)
566
+ icon.alpha = if (icon.isEnabled) 1f else 0.4f
567
+ val actionKey = data.optString("actionKey")
568
+ if (icon.isEnabled && actionKey.isNotEmpty()) {
569
+ icon.setOnClickListener {
570
+ emitAction(item, actionKey, null, icon, "trailingAccessory", index)
571
+ }
572
+ }
573
+ }
574
+
575
+ private fun applyValueSegments(
576
+ view: TextView,
577
+ segments: JSONArray?,
578
+ fontSize: Int = 16,
579
+ lineHeight: Int = 24,
580
+ medium: Boolean = true,
581
+ ) {
582
+ if (segments == null || segments.length() == 0) return
583
+ val value = SpannableStringBuilder()
584
+ for (index in 0 until segments.length()) {
585
+ val segment = segments.getJSONObject(index)
586
+ val start = value.length
587
+ value.append(segment.optString("text"))
588
+ if (segment.optString("style") == "subscript") {
589
+ val size = kotlin.math.ceil(fontSize * 0.6).toFloat()
590
+ val span =
591
+ if (selectorUsesSourceScale)
592
+ AbsoluteSizeSpan(
593
+ kotlin.math.ceil((size * resources.displayMetrics.density).toDouble()).toInt(),
594
+ false,
595
+ )
596
+ else AbsoluteSizeSpan(sp(size).roundToInt(), true)
597
+ value.setSpan(span, start, value.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
598
+ }
599
+ }
600
+ view.textSize = sp(fontSize.toFloat())
601
+ view.typeface =
602
+ if (medium) NativeListFonts.medium(context) else NativeListFonts.regular(context)
603
+ TextViewCompat.setLineHeight(view, dp(lineHeight))
604
+ view.text = value
605
+ }
606
+
607
+ private fun applyStyledText(view: TextView, style: JSONObject) {
608
+ if (style.has("fontSize")) {
609
+ view.setTextSize(
610
+ TypedValue.COMPLEX_UNIT_PX,
611
+ (style.optDouble("fontSize") * resources.displayMetrics.density).toFloat(),
612
+ )
613
+ val text = SpannableStringBuilder(view.text)
614
+ text.getSpans(0, text.length, AbsoluteSizeSpan::class.java).forEach(text::removeSpan)
615
+ view.text = text
616
+ }
617
+ style.optString("fontWeight").takeIf(String::isNotEmpty)?.let {
618
+ view.typeface = marketTypeface(it, "regular")
619
+ }
620
+ style.optString("color").takeIf(String::isNotEmpty)?.let {
621
+ val color = safeColor(it, view.currentTextColor)
622
+ view.setTextColor(color)
623
+ val text = SpannableStringBuilder(view.text)
624
+ text.getSpans(0, text.length, ForegroundColorSpan::class.java).forEach(text::removeSpan)
625
+ view.text = text
626
+ }
627
+ if (style.has("lineHeight")) {
628
+ val text = SpannableStringBuilder(view.text)
629
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
630
+ text.setSpan(
631
+ NativeListLineHeightSpan(styleDp(style.optDouble("lineHeight"))),
632
+ 0,
633
+ text.length,
634
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
635
+ )
636
+ view.setLineSpacing(0f, 1f)
637
+ view.text = text
638
+ }
639
+ if (style.has("lines")) {
640
+ view.maxLines = style.optInt("lines").coerceIn(1, 3)
641
+ view.ellipsize = TextUtils.TruncateAt.END
642
+ }
643
+ if (style.has("truncate") || style.has("lines")) {
644
+ view.setHorizontallyScrolling(view.maxLines == 1)
645
+ view.ellipsize =
646
+ if (style.optString("truncate", "tail") == "clip") null else TextUtils.TruncateAt.END
647
+ if (view.maxLines == 1) {
648
+ val text = SpannableStringBuilder(view.text)
649
+ Regex("\\r\\n|[\\r\\n]").findAll(text).toList().asReversed().forEach { match ->
650
+ text.replace(match.range.first, match.range.last + 1, " ")
651
+ }
652
+ view.text = text
653
+ }
654
+ }
655
+ if (style.has("verticalAlignment")) {
656
+ view.gravity =
657
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
658
+ when (style.optString("verticalAlignment")) {
659
+ "top" -> Gravity.TOP
660
+ "bottom" -> Gravity.BOTTOM
661
+ else -> Gravity.CENTER_VERTICAL
662
+ }
663
+ }
664
+ if (style.has("offsetY"))
665
+ (view as? NativeListTextView)?.opticalOffsetY =
666
+ (style.optDouble("offsetY") * resources.displayMetrics.density).toFloat()
667
+ style.optString("alignment").takeIf(String::isNotEmpty)?.let {
668
+ view.gravity =
669
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) or
670
+ when (it) {
671
+ "center" -> Gravity.CENTER_HORIZONTAL
672
+ "end" -> Gravity.END
673
+ else -> Gravity.START
674
+ }
675
+ }
676
+ }
677
+
678
+ // OneKey patch: keep a small idle member pool after reuse or a direct rebind.
679
+ // The compact proxy/expansion keeps every member until it leaves that state.
680
+
681
+ private fun marketTypeface(weight: String, fallback: String): Typeface =
682
+ when (weight.ifEmpty { fallback }) {
683
+ "regular" -> NativeListFonts.regular(context)
684
+ "semibold" -> NativeListFonts.semibold(context)
685
+ "bold" -> NativeListFonts.bold(context)
686
+ else -> NativeListFonts.medium(context)
687
+ }
688
+
689
+ private fun safeColor(value: String?, fallback: Int): Int =
690
+ try {
691
+ if (value.isNullOrEmpty()) fallback else parseNativeListColor(value)
692
+ } catch (_: IllegalArgumentException) {
693
+ fallback
694
+ }
695
+
696
+ private fun color(theme: JSONObject?, key: String, fallback: String): Int =
697
+ safeColor(theme?.optString(key, fallback), parseNativeListColor(fallback))
698
+
699
+ private fun roundedFill(color: Int, radiusDp: Float) =
700
+ GradientDrawable().apply {
701
+ setColor(color)
702
+ cornerRadius = scaledDp(radiusDp)
703
+ }
704
+
705
+ private fun roundedStroke(stroke: Int, fill: Int, radiusDp: Float) =
706
+ GradientDrawable().apply {
707
+ setColor(fill)
708
+ setStroke(dp(2), stroke)
709
+ cornerRadius = scaledDp(radiusDp)
710
+ }
711
+ }