@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,748 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Paint
4
+ import android.graphics.drawable.GradientDrawable
5
+ import android.text.Spannable
6
+ import android.text.SpannableStringBuilder
7
+ import android.text.TextUtils
8
+ import android.text.style.AbsoluteSizeSpan
9
+ import android.text.style.ForegroundColorSpan
10
+ import android.util.TypedValue
11
+ import android.view.Gravity
12
+ import android.view.View
13
+ import android.view.ViewGroup
14
+ import android.widget.LinearLayout
15
+ import android.widget.TextView
16
+ import androidx.core.widget.TextViewCompat
17
+ import com.facebook.react.uimanager.ThemedReactContext
18
+ import kotlin.math.roundToInt
19
+ import org.json.JSONArray
20
+ import org.json.JSONObject
21
+
22
+ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
23
+ NativeListRendererRowView(context) {
24
+ private val mainColumn = LinearLayout(context)
25
+ private val titleLine = PackedTitleLineLayout(context)
26
+ private val title = DottedUnderlineTextView(context)
27
+ private val subtitle = NativeListTextView(context)
28
+ private val trailingColumn = LinearLayout(context)
29
+ private val trailingViews = listOf(NativeListTextView(context))
30
+ private val checkboxControl = NativeListAccessoryStack(context)
31
+ private val headerTitleIcon = OneKeyIconView(context)
32
+ private val headerValueIcon = OneKeyIconView(context)
33
+ private var currentItem: NativeListItem? = null
34
+ private var currentTheme: JSONObject? = null
35
+ private var currentLayout = "linear"
36
+ private val selectorTextMetrics = mutableMapOf<TextView, Pair<Float, Int>>()
37
+ private val paintDefaults = mutableMapOf<TextView, Int>()
38
+ private val selectorAccessibilityDelegate =
39
+ object : View.AccessibilityDelegate() {
40
+ override fun onInitializeAccessibilityNodeInfo(
41
+ host: View,
42
+ info: android.view.accessibility.AccessibilityNodeInfo,
43
+ ) {
44
+ super.onInitializeAccessibilityNodeInfo(host, info)
45
+ info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
46
+ }
47
+ }
48
+
49
+ override fun usesSourceScale(item: NativeListItem, provided: Boolean) =
50
+ item.usesSelectorSourceScale || provided
51
+
52
+ override fun backgroundGroupPosition(
53
+ item: NativeListItem,
54
+ layout: String,
55
+ itemIndex: Int?,
56
+ selected: Boolean,
57
+ ) =
58
+ if (layout == "sectioned" && !item.json.optBoolean("selected")) ""
59
+ else item.json.optString("groupPosition")
60
+
61
+ override fun defaultHeight(item: NativeListItem, layout: String) =
62
+ when {
63
+ item.json.optString("presentation") == "networkSelector" -> 44
64
+ layout == "table" -> 28
65
+ item.json.optString("variant") == "summary" -> 80
66
+ item.json.optString("variant") == "gallery" -> 32
67
+ item.json.optString("variant") == "history" -> 16
68
+ item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null -> 40
69
+ else -> 36
70
+ }
71
+
72
+ override fun minimumContentHeight(item: NativeListItem, layout: String, sizeDelta: Int) =
73
+ dp(
74
+ (defaultHeight(item, layout) +
75
+ if (item.json.optString("variant") in setOf("summary", "gallery")) 0 else sizeDelta)
76
+ .coerceAtLeast(0)
77
+ )
78
+
79
+ override fun modelHeight(item: NativeListItem, layout: String): Int? {
80
+ if (!item.json.has("height")) return null
81
+ if (item.json.optString("heightRounding") in setOf("floor", "nearest"))
82
+ return super.modelHeight(item, layout)
83
+ if (
84
+ item.json.optString("presentation") == "networkSelector" &&
85
+ item.json.optString("variant") != "summary" &&
86
+ item.json.optJSONObject("checkbox") == null
87
+ ) {
88
+ val px = item.json.optDouble("height") * resources.displayMetrics.density
89
+ return if (item.json.optString("titleActionKey").isEmpty()) px.toInt()
90
+ else
91
+ kotlin.math
92
+ .ceil(px * (if (sourceScale) 1.0 else NativeListScale.factor(resources).toDouble()))
93
+ .toInt()
94
+ }
95
+ return super.modelHeight(item, layout)
96
+ }
97
+
98
+ init {
99
+ mainColumn.orientation = VERTICAL
100
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
101
+ titleLine.orientation = HORIZONTAL
102
+ titleLine.gravity = Gravity.CENTER_VERTICAL
103
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
104
+ mainColumn.addView(titleLine)
105
+ mainColumn.addView(subtitle)
106
+ trailingColumn.addView(trailingViews[0])
107
+ trailingColumn.addView(checkboxControl)
108
+ checkboxControl.onAction = { key, view, slot, target ->
109
+ emitAction(key, view, "trailingAccessory", slot, target)
110
+ }
111
+ }
112
+
113
+ private fun color(theme: JSONObject?, key: String, fallback: String) =
114
+ parseNativeListColor(theme?.optString(key, fallback) ?: fallback)
115
+
116
+ private fun safeColor(value: String?, fallback: Int) =
117
+ runCatching { parseNativeListColor(value ?: "") }.getOrDefault(fallback)
118
+
119
+ private fun roundedFill(color: Int, radius: Float) =
120
+ GradientDrawable().apply {
121
+ setColor(color)
122
+ cornerRadius =
123
+ if (sourceScale) radius * resources.displayMetrics.density
124
+ else NativeListScale.dp(resources, radius)
125
+ }
126
+
127
+ private fun sp(value: Float) = if (sourceScale) value else NativeListScale.font(resources, value)
128
+
129
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
130
+
131
+ private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
132
+
133
+ private fun showText(view: TextView, text: String, lines: Int) {
134
+ view.text = text
135
+ view.maxLines = lines
136
+ view.visibility = if (text.isEmpty()) GONE else VISIBLE
137
+ }
138
+
139
+ private fun emitAction(
140
+ item: NativeListItem,
141
+ key: String,
142
+ target: NativeSelectionTarget?,
143
+ view: View,
144
+ source: String,
145
+ slot: Int? = null,
146
+ ) {
147
+ emitAction(key, view, source, slot, target)
148
+ }
149
+
150
+ override fun bindContent(
151
+ item: NativeListItem,
152
+ theme: JSONObject?,
153
+ layout: String,
154
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
155
+ ) {
156
+ selectorTextMetrics.clear()
157
+ currentItem = item
158
+ currentTheme = theme
159
+ currentLayout = layout
160
+ removeAllViews()
161
+ titleLine.removeView(headerTitleIcon)
162
+ trailingColumn.removeView(headerValueIcon)
163
+ paintDefaults.forEach { (view, flags) -> view.paintFlags = flags }
164
+ paintDefaults.clear()
165
+ orientation = HORIZONTAL
166
+ gravity = Gravity.CENTER_VERTICAL
167
+ setPadding(
168
+ dp(if (layout == "table") 16 else 12),
169
+ dp(8),
170
+ dp(if (layout == "table") 16 else 12),
171
+ dp(8),
172
+ )
173
+ titleLine.packsChildrenAtStart = false
174
+ titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
175
+ title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
176
+ for (view in listOf(title, subtitle) + trailingViews) {
177
+ view.text = ""
178
+ view.visibility = GONE
179
+ view.setLineSpacing(0f, 1f)
180
+ view.maxLines = 1
181
+ view.ellipsize = null
182
+ view.setHorizontallyScrolling(false)
183
+ view.opticalOffsetY = 0f
184
+ view.gravity = Gravity.START
185
+ view.letterSpacing = 0f
186
+ view.fontFeatureSettings = "tnum"
187
+ view.includeFontPadding = false
188
+ view.background = null
189
+ view.setPadding(0, 0, 0, 0)
190
+ view.setOnClickListener(null)
191
+ view.isClickable = false
192
+ }
193
+ title.useSourceScale = sourceScale
194
+ title.showsDottedUnderline = false
195
+ subtitle.textSize = sp(14f)
196
+ subtitle.typeface = NativeListFonts.regular(context)
197
+ subtitle.setTextColor(color(theme, "secondaryText", "#0000009B"))
198
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
199
+ trailingViews[0].apply {
200
+ typeface = NativeListFonts.medium(context)
201
+ textSize = sp(16f)
202
+ gravity = Gravity.END or Gravity.CENTER_VERTICAL
203
+ setTextColor(color(theme, "primaryText", "#000000DF"))
204
+ setTag(com.facebook.react.R.id.react_test_id, null)
205
+ layoutParams = wrap()
206
+ }
207
+ trailingColumn.orientation = VERTICAL
208
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
209
+ checkboxControl.reset()
210
+ checkboxControl.visibility = GONE
211
+ checkboxControl.layoutParams = wrap()
212
+ headerTitleIcon.useSourceScale = sourceScale
213
+ headerValueIcon.useSourceScale = sourceScale
214
+ bindSectionHeader(item, theme, checkboxState)
215
+ val variant = item.json.optString("variant")
216
+ val network = item.json.optString("presentation") == "networkSelector"
217
+ title.textSize =
218
+ sp(
219
+ when {
220
+ network && variant != "summary" -> 14f
221
+ variant == "gallery" -> 18f
222
+ variant == "summary" -> 16f
223
+ layout == "table" -> 11f
224
+ variant == "history" -> 12f
225
+ else -> 14f
226
+ }
227
+ )
228
+ title.typeface =
229
+ when {
230
+ network &&
231
+ item.hasExplicitHeight &&
232
+ variant != "summary" &&
233
+ (item.json.optJSONObject("checkbox") != null ||
234
+ item.json.optString("titleActionKey").isEmpty()) -> NativeListFonts.semibold(context)
235
+ network -> NativeListFonts.medium(context)
236
+ layout == "table" -> NativeListFonts.regular(context)
237
+ variant == "summary" -> NativeListFonts.medium(context)
238
+ else -> NativeListFonts.semibold(context)
239
+ }
240
+ TextViewCompat.setLineHeight(
241
+ title,
242
+ dp(
243
+ when {
244
+ network && variant != "summary" -> 20
245
+ variant in setOf("gallery", "summary") -> 24
246
+ layout == "table" -> 14
247
+ variant == "history" -> 16
248
+ else -> 20
249
+ }
250
+ ),
251
+ )
252
+ if (layout == "table") TextViewCompat.setLineHeight(trailingViews[0], dp(14))
253
+ if (
254
+ network &&
255
+ item.hasExplicitHeight &&
256
+ variant != "summary" &&
257
+ item.json.optString("titleActionKey").isEmpty() &&
258
+ item.json.optJSONObject("checkbox") == null
259
+ ) {
260
+ val inset = (20 * resources.displayMetrics.density).toInt() - dp(8)
261
+ setPadding(inset, 0, inset, 0)
262
+ }
263
+ val style = item.json.optJSONObject("style") ?: JSONObject()
264
+ if (style.has("horizontalPadding")) {
265
+ val hp = stylePx(style.optDouble("horizontalPadding"))
266
+ setPadding(hp, paddingTop, hp, paddingBottom)
267
+ }
268
+ if (style.has("verticalPadding")) {
269
+ val vp = stylePx(style.optDouble("verticalPadding"))
270
+ setPadding(paddingLeft, vp, paddingRight, vp)
271
+ }
272
+ if (style.has("lineGap"))
273
+ (subtitle.layoutParams as LayoutParams).topMargin = stylePx(style.optDouble("lineGap"))
274
+ if (style.has("trailingGap")) {
275
+ var previous = false
276
+ for (i in 0 until trailingColumn.childCount) {
277
+ val child = trailingColumn.getChildAt(i)
278
+ if (child.visibility == GONE) continue
279
+ val params = child.layoutParams as LayoutParams
280
+ if (trailingColumn.orientation == HORIZONTAL)
281
+ params.marginStart = if (previous) stylePx(style.optDouble("trailingGap")) else 0
282
+ else params.topMargin = if (previous) stylePx(style.optDouble("trailingGap")) else 0
283
+ previous = true
284
+ }
285
+ }
286
+ applyTextStyle(item)
287
+ style
288
+ .optJSONObject("container")
289
+ ?.optString("contentVerticalAlignment")
290
+ ?.takeIf(String::isNotEmpty)
291
+ ?.let {
292
+ gravity =
293
+ when (it) {
294
+ "top" -> Gravity.TOP
295
+ "bottom" -> Gravity.BOTTOM
296
+ else -> Gravity.CENTER_VERTICAL
297
+ }
298
+ }
299
+ applySelectorTypography(item)
300
+ }
301
+
302
+ private fun applyTextStyle(item: NativeListItem) {
303
+ val style = item.json.optJSONObject("style") ?: return
304
+ for ((slot, view) in
305
+ listOf("title" to title, "subtitle" to subtitle, "value" to trailingViews[0])) style
306
+ .optJSONObject(slot)
307
+ ?.let { applyStyledText(view, it) }
308
+ }
309
+
310
+ private fun bindCheckbox(
311
+ item: NativeListItem,
312
+ data: JSONObject,
313
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
314
+ ) {
315
+ checkboxControl.bind(
316
+ item,
317
+ JSONArray().put(data),
318
+ currentTheme,
319
+ JSONObject(),
320
+ sourceScale,
321
+ checkboxState,
322
+ )
323
+ }
324
+
325
+ override fun bindSelectionContent(
326
+ item: NativeListItem,
327
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
328
+ ) {
329
+ currentItem = item
330
+ checkboxControl.updateSelection(item, checkboxState)
331
+ }
332
+
333
+ override fun bindStableSummary(item: NativeListItem) {
334
+ if (item.json.optString("variant") != "summary" || !retainBoundItem(item)) return
335
+ currentItem = item
336
+ contentDescription = accessibilityText(item)
337
+ // The retained item makes the next full bind a no-op, so this path must
338
+ // leave text and visibility exactly as bindSectionHeader would.
339
+ showText(title, item.json.optString("title"), 1)
340
+ val value = item.json.optString("value")
341
+ if (value.isEmpty()) {
342
+ trailingViews[0].text = ""
343
+ trailingViews[0].visibility = GONE
344
+ } else {
345
+ showTrailing(
346
+ 0,
347
+ value,
348
+ true,
349
+ item.json.optString("valueActionKey"),
350
+ color(currentTheme, "secondaryText", "#0000009B"),
351
+ )
352
+ }
353
+ applyValueSegments(trailingViews[0], item.json.optJSONArray("valueSegments"))
354
+ applyTextStyle(item)
355
+ applySelectorTypography(item)
356
+ }
357
+
358
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
359
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
360
+ val item = currentItem ?: return
361
+ if (
362
+ item.hasExplicitHeight &&
363
+ item.json.optString("presentation") == "networkSelector" &&
364
+ checkboxControl.visibility == VISIBLE &&
365
+ trailingColumn.parent === this
366
+ ) {
367
+ val density = resources.displayMetrics.density
368
+ val values = trailingViews.filter { it.visibility == VISIBLE }
369
+ val trailing = values.sumOf { it.measuredWidth } + (20 + 12 * values.size) * density
370
+ val sourceLeft = (measuredWidth - 12 * density - trailing).roundToInt()
371
+ val measuredLeft = measuredWidth - paddingRight - trailingColumn.measuredWidth
372
+ mainColumn.measure(
373
+ MeasureSpec.makeMeasureSpec(
374
+ (mainColumn.measuredWidth + sourceLeft - measuredLeft).coerceAtLeast(0),
375
+ MeasureSpec.EXACTLY,
376
+ ),
377
+ MeasureSpec.makeMeasureSpec(mainColumn.measuredHeight, MeasureSpec.EXACTLY),
378
+ )
379
+ }
380
+ }
381
+
382
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
383
+ super.onLayout(changed, left, top, right, bottom)
384
+ val item = currentItem ?: return
385
+ if (
386
+ item.hasExplicitHeight &&
387
+ item.json.optString("presentation") == "networkSelector" &&
388
+ item.json.optString("variant") == "summary" &&
389
+ item.json
390
+ .optJSONObject("style")
391
+ ?.optJSONObject("container")
392
+ ?.has("contentVerticalAlignment") != true
393
+ ) {
394
+ val margins = trailingColumn.layoutParams as MarginLayoutParams
395
+ val available = height - paddingTop - paddingBottom - margins.topMargin - margins.bottomMargin
396
+ trailingColumn.offsetTopAndBottom(
397
+ paddingTop + margins.topMargin + (available - trailingColumn.height + 1) / 2 -
398
+ trailingColumn.top
399
+ )
400
+ }
401
+ }
402
+
403
+ override fun recycleContent() {
404
+ checkboxControl.reset()
405
+ currentItem = null
406
+ }
407
+
408
+ override fun disposeContent() {
409
+ recycleContent()
410
+ }
411
+
412
+ private fun bindSectionHeader(
413
+ item: NativeListItem,
414
+ theme: JSONObject?,
415
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
416
+ ) {
417
+ val variant = item.json.optString("variant")
418
+ val isSummary = variant == "summary"
419
+ val isGallery = variant == "gallery"
420
+ val isTable = currentLayout == "table"
421
+ clipToPadding = true
422
+ // OneKey patch: title help emits its own frame instead of toggling the section.
423
+ item.json
424
+ .optString("titleActionKey")
425
+ .takeIf { it.isNotEmpty() }
426
+ ?.let { key ->
427
+ title.setOnClickListener { emitAction(item, key, null, title, "leadingAction") }
428
+ }
429
+ val isNetworkSelector = item.json.optString("presentation") == "networkSelector"
430
+ val isHistory = variant == "history"
431
+ val isExplicitNetworkHeader = isNetworkSelector && item.hasExplicitHeight
432
+ val hasDottedTitle =
433
+ isSummary ||
434
+ (isNetworkSelector &&
435
+ (!isExplicitNetworkHeader || item.json.optString("titleActionKey").isNotEmpty())) ||
436
+ (item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null)
437
+ // Linear/sectioned snapshots reserve the ListItem mx=8 at RecyclerView
438
+ // level, so header-local insets below are source px minus that outer inset.
439
+ val headerHorizontalInset = 12
440
+ addView(mainColumn, weighted())
441
+ showText(title, item.json.optString("title"), 1)
442
+ if (hasDottedTitle) {
443
+ title.showsDottedUnderline = true
444
+ title.dottedUnderlineColor = color(theme, "secondaryText", "#0000009B")
445
+ title.setPadding(0, 0, 0, dp(3))
446
+ }
447
+ title.setTextColor(
448
+ color(
449
+ theme,
450
+ if (isSummary || isGallery) "primaryText" else "secondaryText",
451
+ if (isSummary || isGallery) "#000000DF" else "#0000009B",
452
+ )
453
+ )
454
+ if (isNetworkSelector) {
455
+ val verticalInset =
456
+ if (isExplicitNetworkHeader && item.json.optString("titleActionKey").isEmpty()) 8 else 12
457
+ setPadding(
458
+ dp(headerHorizontalInset),
459
+ dp(verticalInset),
460
+ dp(headerHorizontalInset),
461
+ dp(verticalInset),
462
+ )
463
+ title.textSize = sp(14f)
464
+ title.typeface = NativeListFonts.medium(context)
465
+ TextViewCompat.setLineHeight(title, dp(20))
466
+ } else if (isHistory) {
467
+ setPadding(0, 0, 0, 0)
468
+ title.text = item.json.optString("title").uppercase()
469
+ title.textSize = sp(12f)
470
+ title.typeface = NativeListFonts.semibold(context)
471
+ title.letterSpacing = 0.8f / sp(12f)
472
+ TextViewCompat.setLineHeight(title, dp(16))
473
+ } else if (!isTable && !isGallery && !isSummary) {
474
+ // SectionList.SectionHeader: h=36, px=20, headingSm 14/20 semibold.
475
+ setPadding(dp(headerHorizontalInset), dp(8), dp(headerHorizontalInset), dp(8))
476
+ TextViewCompat.setLineHeight(title, dp(20))
477
+ } else if (isTable) {
478
+ setPadding(dp(16), dp(12), dp(16), dp(2))
479
+ title.includeFontPadding = false
480
+ title.layoutParams = wrap()
481
+ titleLine.layoutParams = wrap()
482
+ item.json.optJSONObject("titleIcon")?.let { icon ->
483
+ headerTitleIcon.visibility = VISIBLE
484
+ headerTitleIcon.iconName = icon.optString("name")
485
+ headerTitleIcon.tintColor =
486
+ safeColor(icon.optString("tintColor"), color(theme, "iconSubdued", "#00000072"))
487
+ titleLine.addView(
488
+ headerTitleIcon,
489
+ LayoutParams(dp(12), dp(12)).apply { marginStart = dp(4) },
490
+ )
491
+ if (icon.optString("name") != "ChevronGrabberVerOutline") {
492
+ title.setTextColor(color(theme, "primaryText", "#000000DF"))
493
+ }
494
+ }
495
+ }
496
+ showText(subtitle, item.json.optString("subtitle"), 1)
497
+ if (isGallery) {
498
+ setPadding(dp(12), 0, dp(12), dp(8))
499
+ } else if (isSummary) {
500
+ addView(trailingColumn, wrap())
501
+ // NetworkListHeader: outer mt=16/pb=12, inner XStack px=20/py=8.
502
+ setPadding(dp(headerHorizontalInset), dp(24), dp(headerHorizontalInset), dp(20))
503
+ showTrailing(
504
+ 0,
505
+ item.json.optString("value"),
506
+ true,
507
+ item.json.optString("valueActionKey"),
508
+ color(theme, "secondaryText", "#0000009B"),
509
+ )
510
+ trailingViews[0].setTag(
511
+ com.facebook.react.R.id.react_test_id,
512
+ item.json.optString("valueActionTestID").takeIf { it.isNotEmpty() },
513
+ )
514
+ trailingViews[0].accessibilityDelegate = selectorAccessibilityDelegate
515
+ trailingViews[0].textSize = sp(16f)
516
+ trailingViews[0].typeface = NativeListFonts.medium(context)
517
+ TextViewCompat.setLineHeight(trailingViews[0], dp(24))
518
+ // OneKey patch: the migrated media button shares the original 24-point text box.
519
+ if (isExplicitNetworkHeader) trailingViews[0].setPadding(0, 0, 0, 0)
520
+ else trailingViews[0].setPadding(dp(14), dp(6), dp(14), dp(6))
521
+ trailingViews[0].layoutParams = wrap()
522
+ } else if (!isGallery && !isHistory) {
523
+ val value = item.json.optString("value")
524
+ val checkboxData = item.json.optJSONObject("checkbox")
525
+ if (isExplicitNetworkHeader && checkboxData != null) {
526
+ // OneKey patch: the asset section title reserves its original 8dp trailing margin.
527
+ (mainColumn.layoutParams as LayoutParams).marginEnd = dp(8)
528
+ }
529
+ if (checkboxData != null && value.isNotEmpty()) {
530
+ // Value and checkbox share the trailing edge as one compound accessory.
531
+ trailingColumn.orientation = HORIZONTAL
532
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
533
+ trailingViews[0].layoutParams = wrap()
534
+ checkboxControl.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginStart = dp(12) }
535
+ }
536
+ addView(trailingColumn, wrap())
537
+ showTrailing(0, value, true)
538
+ if (isTable) {
539
+ trailingColumn.orientation = HORIZONTAL
540
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
541
+ trailingViews[0].includeFontPadding = false
542
+ trailingViews[0].textSize = sp(11f)
543
+ trailingViews[0].typeface = NativeListFonts.regular(context)
544
+ trailingViews[0].setTextColor(color(theme, "secondaryText", "#0000009B"))
545
+ item.json.optJSONObject("valueIcon")?.let { icon ->
546
+ headerValueIcon.visibility = VISIBLE
547
+ headerValueIcon.iconName = icon.optString("name")
548
+ headerValueIcon.tintColor =
549
+ safeColor(icon.optString("tintColor"), color(theme, "iconSubdued", "#00000072"))
550
+ trailingColumn.addView(
551
+ headerValueIcon,
552
+ LayoutParams(dp(12), dp(12)).apply { marginStart = dp(4) },
553
+ )
554
+ if (icon.optString("name") != "ChevronGrabberVerOutline") {
555
+ trailingViews[0].setTextColor(color(theme, "primaryText", "#000000DF"))
556
+ }
557
+ }
558
+ }
559
+ checkboxData?.let { bindCheckbox(item, it, checkboxState) }
560
+ // Legacy: selector checkbox borders may round into the row padding.
561
+ if (checkboxControl.requestsUnclippedHost) clipToPadding = false
562
+ }
563
+ applyValueSegments(trailingViews[0], item.json.optJSONArray("valueSegments"))
564
+ }
565
+
566
+ private fun showTrailing(
567
+ index: Int,
568
+ value: String,
569
+ primary: Boolean,
570
+ actionKey: String? = null,
571
+ textColor: Int? = null,
572
+ ) {
573
+ if (index !in trailingViews.indices || value.isEmpty()) return
574
+ val view = trailingViews[index]
575
+ view.maxLines = 1
576
+ view.text = value
577
+ view.textSize = sp(if (primary) 16f else 14f)
578
+ view.typeface =
579
+ if (primary) NativeListFonts.medium(context) else NativeListFonts.regular(context)
580
+ view.includeFontPadding = false
581
+ view.fontFeatureSettings = "tnum"
582
+ TextViewCompat.setLineHeight(view, dp(if (primary) 24 else 20))
583
+ textColor?.let(view::setTextColor)
584
+ view.visibility = VISIBLE
585
+ if (!actionKey.isNullOrEmpty()) {
586
+ view.setOnClickListener {
587
+ currentItem
588
+ ?.takeUnless { item -> item.json.optBoolean("disabled", false) }
589
+ ?.let { item -> emitAction(item, actionKey, null, view, "trailingAccessory", index) }
590
+ }
591
+ }
592
+ }
593
+
594
+ private fun applyValueSegments(
595
+ view: TextView,
596
+ segments: JSONArray?,
597
+ fontSize: Int = 16,
598
+ lineHeight: Int = 24,
599
+ medium: Boolean = true,
600
+ ) {
601
+ if (segments == null || segments.length() == 0) return
602
+ val value = SpannableStringBuilder()
603
+ for (index in 0 until segments.length()) {
604
+ val segment = segments.getJSONObject(index)
605
+ val start = value.length
606
+ value.append(segment.optString("text"))
607
+ if (segment.optString("style") == "subscript") {
608
+ val size = kotlin.math.ceil(fontSize * 0.6).toFloat()
609
+ val span =
610
+ if (sourceScale)
611
+ AbsoluteSizeSpan(
612
+ kotlin.math.ceil((size * resources.displayMetrics.density).toDouble()).toInt(),
613
+ false,
614
+ )
615
+ else AbsoluteSizeSpan(sp(size).roundToInt(), true)
616
+ value.setSpan(span, start, value.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
617
+ }
618
+ }
619
+ view.textSize = sp(fontSize.toFloat())
620
+ view.typeface =
621
+ if (medium) NativeListFonts.medium(context) else NativeListFonts.regular(context)
622
+ TextViewCompat.setLineHeight(view, dp(lineHeight))
623
+ view.text = value
624
+ }
625
+
626
+ private fun applyStyledText(view: TextView, style: JSONObject) {
627
+ if (style.has("fontSize")) {
628
+ view.setTextSize(
629
+ TypedValue.COMPLEX_UNIT_PX,
630
+ (style.optDouble("fontSize") * resources.displayMetrics.density).toFloat(),
631
+ )
632
+ val text = SpannableStringBuilder(view.text)
633
+ text.getSpans(0, text.length, AbsoluteSizeSpan::class.java).forEach(text::removeSpan)
634
+ view.text = text
635
+ }
636
+ style.optString("fontWeight").takeIf(String::isNotEmpty)?.let {
637
+ view.typeface =
638
+ when (it) {
639
+ "medium" -> NativeListFonts.medium(context)
640
+ "semibold" -> NativeListFonts.semibold(context)
641
+ "bold" -> NativeListFonts.bold(context)
642
+ else -> NativeListFonts.regular(context)
643
+ }
644
+ }
645
+ style.optString("color").takeIf(String::isNotEmpty)?.let {
646
+ val color = safeColor(it, view.currentTextColor)
647
+ view.setTextColor(color)
648
+ val text = SpannableStringBuilder(view.text)
649
+ text.getSpans(0, text.length, ForegroundColorSpan::class.java).forEach(text::removeSpan)
650
+ view.text = text
651
+ }
652
+ if (style.has("lineHeight")) {
653
+ val text = SpannableStringBuilder(view.text)
654
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
655
+ text.setSpan(
656
+ NativeListLineHeightSpan(stylePx(style.optDouble("lineHeight"))),
657
+ 0,
658
+ text.length,
659
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
660
+ )
661
+ view.setLineSpacing(0f, 1f)
662
+ view.text = text
663
+ }
664
+ if (style.has("lines")) {
665
+ view.maxLines = style.optInt("lines").coerceIn(1, 3)
666
+ view.ellipsize = TextUtils.TruncateAt.END
667
+ }
668
+ if (style.has("truncate") || style.has("lines")) {
669
+ view.setHorizontallyScrolling(view.maxLines == 1)
670
+ view.ellipsize =
671
+ if (style.optString("truncate", "tail") == "clip") null else TextUtils.TruncateAt.END
672
+ if (view.maxLines == 1) {
673
+ val text = SpannableStringBuilder(view.text)
674
+ Regex("\\r\\n|[\\r\\n]").findAll(text).toList().asReversed().forEach { match ->
675
+ text.replace(match.range.first, match.range.last + 1, " ")
676
+ }
677
+ view.text = text
678
+ }
679
+ }
680
+ if (style.has("verticalAlignment")) {
681
+ view.gravity =
682
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
683
+ when (style.optString("verticalAlignment")) {
684
+ "top" -> Gravity.TOP
685
+ "bottom" -> Gravity.BOTTOM
686
+ else -> Gravity.CENTER_VERTICAL
687
+ }
688
+ }
689
+ if (style.has("offsetY"))
690
+ (view as? NativeListTextView)?.opticalOffsetY =
691
+ (style.optDouble("offsetY") * resources.displayMetrics.density).toFloat()
692
+ style.optString("alignment").takeIf(String::isNotEmpty)?.let {
693
+ view.gravity =
694
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) or
695
+ when (it) {
696
+ "center" -> Gravity.CENTER_HORIZONTAL
697
+ "end" -> Gravity.END
698
+ else -> Gravity.START
699
+ }
700
+ }
701
+ }
702
+
703
+ private fun applySelectorTypography(item: NativeListItem) {
704
+ if (
705
+ item.json.optString("presentation") !in
706
+ setOf("accountSelector", "networkSelector", "walletSidebar")
707
+ )
708
+ return
709
+ fun visit(view: View) {
710
+ if (view is TextView) {
711
+ view.fontFeatureSettings = "tnum"
712
+ if (sourceScale) {
713
+ paintDefaults.putIfAbsent(view, view.paintFlags)
714
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
715
+ val (fontSize, line) =
716
+ selectorTextMetrics.getOrPut(view) { view.textSize to view.lineHeight }
717
+ view.setTextSize(
718
+ TypedValue.COMPLEX_UNIT_PX,
719
+ kotlin.math
720
+ .ceil(
721
+ (fontSize * resources.displayMetrics.density /
722
+ resources.displayMetrics.scaledDensity)
723
+ .toDouble()
724
+ )
725
+ .toFloat(),
726
+ )
727
+ view.letterSpacing = 0f
728
+ if (view.text.isNotEmpty()) {
729
+ val text = SpannableStringBuilder(view.text)
730
+ text
731
+ .getSpans(0, text.length, NativeListLineHeightSpan::class.java)
732
+ .forEach(text::removeSpan)
733
+ text.setSpan(
734
+ NativeListLineHeightSpan(line),
735
+ 0,
736
+ text.length,
737
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
738
+ )
739
+ view.setLineSpacing(0f, 1f)
740
+ view.text = text
741
+ }
742
+ }
743
+ }
744
+ if (view is ViewGroup) for (i in 0 until view.childCount) visit(view.getChildAt(i))
745
+ }
746
+ visit(this)
747
+ }
748
+ }