@onekeyfe/react-native-native-list 3.0.157-alpha.248 → 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,820 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.drawable.GradientDrawable
4
+ import android.text.Spannable
5
+ import android.text.SpannableStringBuilder
6
+ import android.text.TextUtils
7
+ import android.text.style.AbsoluteSizeSpan
8
+ import android.text.style.ForegroundColorSpan
9
+ import android.util.TypedValue
10
+ import android.view.Gravity
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.widget.LinearLayout
14
+ import android.widget.TextView
15
+ import androidx.core.widget.TextViewCompat
16
+ import com.facebook.react.uimanager.ThemedReactContext
17
+ import kotlin.math.roundToInt
18
+ import org.json.JSONArray
19
+ import org.json.JSONObject
20
+
21
+ internal class NativeListIdentityRowView(context: ThemedReactContext) :
22
+ NativeListRendererRowView(context) {
23
+ private val leadingFrame = NativeListLeadingVisual(context)
24
+ private val leadingFallback
25
+ get() = leadingFrame.fallbackTextView
26
+
27
+ private val leadingActionIcon = OneKeyIconView(context)
28
+ private val mainColumn = LinearLayout(context)
29
+ private val titleLine = PackedTitleLineLayout(context)
30
+ private val title = NativeListTextView(context)
31
+ private val subtitle = NativeListTextView(context)
32
+ private val tertiary = NativeListTextView(context)
33
+ private val badgeLine = NativeListTextView(context)
34
+ private val trailingColumn = NativeListAccessoryStack(context)
35
+ private val semanticBadgeLabels = mutableListOf<TextView>()
36
+ private val semanticSubtitleLabels = mutableListOf<TextView>()
37
+ private var walletBadgeLine: View? = null
38
+ private var currentItem: NativeListItem? = null
39
+ private var currentTheme: JSONObject? = null
40
+ private val paintDefaults = mutableMapOf<TextView, Int>()
41
+
42
+ override fun backgroundGroupPosition(
43
+ item: NativeListItem,
44
+ layout: String,
45
+ itemIndex: Int?,
46
+ selected: Boolean,
47
+ ) =
48
+ if (
49
+ item.json.optString("presentation") == "walletSidebar" ||
50
+ item.hasExplicitHeight &&
51
+ item.json.optString("presentation") in setOf("accountSelector", "networkSelector")
52
+ )
53
+ "single"
54
+ else item.json.optString("groupPosition")
55
+
56
+ override val assetFields = listOf("leading")
57
+
58
+ override fun usesSourceScale(item: NativeListItem, provided: Boolean) =
59
+ item.usesSelectorSourceScale || provided
60
+
61
+ override fun defaultHeight(item: NativeListItem, layout: String) =
62
+ when {
63
+ item.json.optString("presentation") == "walletSidebar" ->
64
+ if ((item.json.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68
65
+ item.json.optString("presentation") == "networkSelector" -> 47
66
+ item.json.optString("tertiary").isNotEmpty() -> 72
67
+ item.json.optString("subtitle").isNotEmpty() -> 60
68
+ else -> 56
69
+ }
70
+
71
+ override fun minimumContentHeight(item: NativeListItem, layout: String, sizeDelta: Int) =
72
+ dp(
73
+ (defaultHeight(item, layout) +
74
+ if (item.json.optString("presentation") == "networkSelector") 0 else sizeDelta)
75
+ .coerceAtLeast(0)
76
+ )
77
+
78
+ override val defaultSeparatorInset = 60
79
+
80
+ init {
81
+ mainColumn.orientation = VERTICAL
82
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
83
+ titleLine.orientation = HORIZONTAL
84
+ titleLine.gravity = Gravity.CENTER_VERTICAL
85
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
86
+ titleLine.addView(badgeLine)
87
+ trailingColumn.onAction = { key, view, slot, target ->
88
+ emitAction(key, view, "trailingAccessory", slot, target, trailingColumn.anchorInset(view))
89
+ }
90
+ }
91
+
92
+ private fun color(theme: JSONObject?, key: String, fallback: String) =
93
+ parseNativeListColor(theme?.optString(key, fallback) ?: fallback)
94
+
95
+ private fun safeColor(value: String?, fallback: Int) =
96
+ runCatching { parseNativeListColor(value ?: "") }.getOrDefault(fallback)
97
+
98
+ private fun roundedFill(color: Int, radius: Float) =
99
+ GradientDrawable().apply {
100
+ setColor(color)
101
+ cornerRadius =
102
+ if (sourceScale) radius * resources.displayMetrics.density
103
+ else NativeListScale.dp(resources, radius)
104
+ }
105
+
106
+ private fun sp(value: Float) = if (sourceScale) value else NativeListScale.font(resources, value)
107
+
108
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
109
+
110
+ private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
111
+
112
+ private fun showText(view: TextView, text: String, lines: Int) {
113
+ view.text = text
114
+ view.maxLines = lines
115
+ view.visibility = if (text.isEmpty()) GONE else VISIBLE
116
+ }
117
+
118
+ private fun emitAction(
119
+ item: NativeListItem,
120
+ key: String,
121
+ target: NativeSelectionTarget?,
122
+ view: View,
123
+ source: String,
124
+ slot: Int? = null,
125
+ ) {
126
+ emitAction(key, view, source, slot, target)
127
+ }
128
+
129
+ private fun JSONArray?.hasAccessory(kind: String) =
130
+ this != null && (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
131
+
132
+ override fun bindContent(
133
+ item: NativeListItem,
134
+ theme: JSONObject?,
135
+ layout: String,
136
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
137
+ ) {
138
+ currentItem = item
139
+ currentTheme = theme
140
+ removeAllViews()
141
+ mainColumn.removeAllViews()
142
+ semanticBadgeLabels.clear()
143
+ semanticSubtitleLabels.clear()
144
+ walletBadgeLine = null
145
+ paintDefaults.forEach { (view, flags) -> view.paintFlags = flags }
146
+ paintDefaults.clear()
147
+ orientation = HORIZONTAL
148
+ gravity = Gravity.CENTER_VERTICAL
149
+ setPadding(
150
+ dp(if (layout == "table") 16 else 12),
151
+ dp(8),
152
+ dp(if (layout == "table") 16 else 12),
153
+ dp(8),
154
+ )
155
+ clipChildren = true
156
+ clipToPadding = true
157
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
158
+ titleLine.gravity = Gravity.CENTER_VERTICAL
159
+ titleLine.packsChildrenAtStart = false
160
+ titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
161
+ title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
162
+ for (view in listOf(title, subtitle, tertiary, badgeLine)) {
163
+ view.text = ""
164
+ view.visibility = GONE
165
+ view.setLineSpacing(0f, 1f)
166
+ view.maxLines = 1
167
+ view.ellipsize = null
168
+ view.setHorizontallyScrolling(false)
169
+ view.opticalOffsetY = 0f
170
+ view.gravity = Gravity.START
171
+ view.letterSpacing = 0f
172
+ view.fontFeatureSettings = "tnum"
173
+ view.includeFontPadding = false
174
+ view.background = null
175
+ view.setPadding(0, 0, 0, 0)
176
+ }
177
+ title.typeface =
178
+ if (item.json.optString("presentation") in setOf("accountSelector", "walletSidebar"))
179
+ NativeListFonts.regular(context)
180
+ else NativeListFonts.medium(context)
181
+ title.textSize = sp(if (item.json.optString("presentation") == "walletSidebar") 12f else 16f)
182
+ title.setTextColor(color(theme, "primaryText", "#000000DF"))
183
+ subtitle.typeface = NativeListFonts.regular(context)
184
+ subtitle.textSize = sp(14f)
185
+ subtitle.setTextColor(color(theme, "secondaryText", "#0000009B"))
186
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
187
+ tertiary.typeface = NativeListFonts.regular(context)
188
+ tertiary.textSize = sp(14f)
189
+ tertiary.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
190
+ badgeLine.typeface = NativeListFonts.medium(context)
191
+ badgeLine.textSize = sp(12f)
192
+ // Legacy: tertiary always ellipsizes at the end.
193
+ tertiary.ellipsize = TextUtils.TruncateAt.END
194
+ mainColumn.addView(titleLine)
195
+ mainColumn.addView(subtitle)
196
+ mainColumn.addView(tertiary)
197
+ trailingColumn.orientation = VERTICAL
198
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
199
+ leadingActionIcon.useSourceScale = sourceScale
200
+ leadingActionIcon.setOnClickListener(null)
201
+ bindIdentity(item, theme, item.json.optBoolean("selected"), checkboxState)
202
+ TextViewCompat.setLineHeight(
203
+ title,
204
+ dp(if (item.json.optString("presentation") == "walletSidebar") 16 else 24),
205
+ )
206
+ TextViewCompat.setLineHeight(subtitle, dp(20))
207
+ TextViewCompat.setLineHeight(tertiary, dp(20))
208
+ val style = item.json.optJSONObject("style") ?: JSONObject()
209
+ if (style.has("horizontalPadding")) {
210
+ val hp = stylePx(style.optDouble("horizontalPadding"))
211
+ setPadding(hp, paddingTop, hp, paddingBottom)
212
+ }
213
+ if (style.has("verticalPadding")) {
214
+ val vp = stylePx(style.optDouble("verticalPadding"))
215
+ setPadding(paddingLeft, vp, paddingRight, vp)
216
+ }
217
+ if (style.has("lineGap")) styleGap(mainColumn, stylePx(style.optDouble("lineGap")))
218
+ if (style.has("leadingGap") && leadingFrame.parent === this) {
219
+ if (orientation == VERTICAL)
220
+ (mainColumn.layoutParams as LayoutParams).topMargin = stylePx(style.optDouble("leadingGap"))
221
+ else
222
+ (leadingFrame.layoutParams as LayoutParams).marginEnd =
223
+ stylePx(style.optDouble("leadingGap"))
224
+ }
225
+ if (style.has("titleBadgeGap")) {
226
+ val gap = stylePx(style.optDouble("titleBadgeGap"))
227
+ if (walletBadgeLine != null) (walletBadgeLine!!.layoutParams as LayoutParams).topMargin = gap
228
+ else (badgeLine.layoutParams as LayoutParams).marginStart = gap
229
+ }
230
+ val explicitTextStyles = mutableMapOf<View, JSONObject>()
231
+ for ((slot, targets) in
232
+ listOf(
233
+ "title" to listOf(title),
234
+ "subtitle" to semanticSubtitleLabels.ifEmpty { listOf(subtitle) },
235
+ "tertiary" to listOf(tertiary),
236
+ "badge" to semanticBadgeLabels.ifEmpty { listOf(badgeLine) },
237
+ )) {
238
+ style.optJSONObject(slot)?.let { text ->
239
+ targets.forEach {
240
+ applyStyledText(it, text)
241
+ explicitTextStyles[it] = text
242
+ }
243
+ }
244
+ }
245
+ val alignment = style.optJSONObject("container")?.optString("contentVerticalAlignment")
246
+ if (!alignment.isNullOrEmpty())
247
+ gravity =
248
+ (gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
249
+ when (alignment) {
250
+ "top" -> Gravity.TOP
251
+ "bottom" -> Gravity.BOTTOM
252
+ else -> Gravity.CENTER_VERTICAL
253
+ }
254
+ applySelectorTypography(item) { view ->
255
+ explicitTextStyles[view] ?: trailingColumn.explicitTextStyle(view, style)
256
+ }
257
+ // Legacy: overhanging selector icons/checkboxes must not be clipped by row padding.
258
+ if (trailingColumn.parent === this && trailingColumn.requestsUnclippedHost) clipToPadding = false
259
+ }
260
+
261
+ private fun styleGap(column: LinearLayout, gap: Int) {
262
+ var previous = false
263
+ for (i in 0 until column.childCount) {
264
+ val child = column.getChildAt(i)
265
+ if (child.visibility == GONE) continue
266
+ (child.layoutParams as LayoutParams).topMargin = if (previous) gap else 0
267
+ previous = true
268
+ }
269
+ }
270
+
271
+ private fun applySelectorTypography(
272
+ item: NativeListItem,
273
+ explicitStyle: (TextView) -> JSONObject?,
274
+ ) {
275
+ if (
276
+ item.json.optString("presentation") !in
277
+ setOf("accountSelector", "networkSelector", "walletSidebar")
278
+ )
279
+ return
280
+ fun visit(view: View) {
281
+ if (view is TextView) {
282
+ view.fontFeatureSettings = "tnum"
283
+ if (sourceScale) {
284
+ paintDefaults.putIfAbsent(view, view.paintFlags)
285
+ // Explicit style fontSize/lineHeight are kept as the caller set them.
286
+ applyNativeListSourceTypography(view, explicitStyle(view))
287
+ }
288
+ }
289
+ if (view is ViewGroup) for (i in 0 until view.childCount) visit(view.getChildAt(i))
290
+ }
291
+ visit(this)
292
+ }
293
+
294
+ private fun addLeading(visual: JSONObject?, sizeDp: Int, spacingDp: Int = 12) {
295
+ // Legacy: a missing visual still reserves an empty, transparent leading slot.
296
+ val descriptor = visual ?: JSONObject().put("backgroundColor", "#00000000")
297
+ if (visual == null) leadingFrame.recycle()
298
+ val item = currentItem!!
299
+ val style = item.json.optJSONObject("style")?.optJSONObject("image") ?: JSONObject()
300
+ // Legacy wallet-sidebar glyph/overlay sizing applies with source scaling.
301
+ val walletSource = item.json.optString("presentation") == "walletSidebar" && sourceScale
302
+ val fallbackIconName =
303
+ descriptor.optJSONObject("fallbackIcon")?.takeIf { descriptor.optJSONObject("image") == null }?.optString("name")
304
+ leadingFrame.glyphSize =
305
+ if (walletSource && fallbackIconName == "LockSolid") 40
306
+ else if (walletSource && fallbackIconName == "PlusSmallOutline") 24
307
+ else 18
308
+ leadingFrame.walletTextOverlays = walletSource
309
+ leadingFrame.roundedImageRadius =
310
+ if (item.json.optString("presentation") == "accountSelector" && item.hasExplicitHeight) 8
311
+ else 10
312
+ leadingFrame.bind(descriptor, style, item.key, currentTheme, false, sourceScale)
313
+ leadingFallback.typeface = NativeListFonts.bold(context)
314
+ leadingFallback.textSize = sp(13f)
315
+ leadingFallback.setLineSpacing(0f, 1f)
316
+ addView(
317
+ leadingFrame,
318
+ LayoutParams(
319
+ if (style.has("width")) stylePx(style.optDouble("width")) else dp(sizeDp),
320
+ if (style.has("height")) stylePx(style.optDouble("height")) else dp(sizeDp),
321
+ )
322
+ .apply {
323
+ // Legacy: Yoga rounds cumulative selector edges, not each gap separately.
324
+ marginEnd =
325
+ if (
326
+ item.hasExplicitHeight &&
327
+ item.json.optString("presentation") in setOf("accountSelector", "networkSelector")
328
+ )
329
+ dp(12 + sizeDp + spacingDp) - dp(12) - dp(sizeDp)
330
+ else dp(spacingDp)
331
+ },
332
+ )
333
+ }
334
+
335
+ override fun bindSelectionContent(
336
+ item: NativeListItem,
337
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
338
+ ) {
339
+ currentItem = item
340
+ trailingColumn.updateSelection(item, checkboxState)
341
+ if (item.json.optString("presentation") == "walletSidebar") {
342
+ title.setTextColor(
343
+ color(
344
+ currentTheme,
345
+ if (rowSelected) "primaryText" else "secondaryText",
346
+ if (rowSelected) "#FFFFFFED" else "#FFFFFFAF",
347
+ )
348
+ )
349
+ item.json
350
+ .optJSONObject("style")
351
+ ?.optJSONObject("title")
352
+ ?.optString("color")
353
+ ?.takeIf(String::isNotEmpty)
354
+ ?.let { title.setTextColor(safeColor(it, title.currentTextColor)) }
355
+ }
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
+ trailingColumn.hasCheckbox &&
365
+ trailingColumn.parent === this
366
+ ) {
367
+ val density = resources.displayMetrics.density
368
+ val values = trailingColumn.visibleValues
369
+ val width = values.sumOf { it.measuredWidth } + (20 + 12 * values.size) * density
370
+ val sourceLeft = (measuredWidth - 12 * density - width).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.json
387
+ .optJSONObject("style")
388
+ ?.optJSONObject("container")
389
+ ?.has("contentVerticalAlignment") == true
390
+ )
391
+ return
392
+ if (!item.hasExplicitHeight) return
393
+ val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
394
+ if (
395
+ item.json.optString("presentation") == "accountSelector" &&
396
+ accessory?.optString("kind") == "icon" &&
397
+ accessory.optString("name") == "PlusSmallOutline"
398
+ ) {
399
+ trailingColumn.firstVisibleIcon?.let {
400
+ trailingColumn.offsetTopAndBottom(dp(18) - dp(7) - trailingColumn.top - it.top)
401
+ }
402
+ }
403
+ if (item.json.optString("presentation") == "networkSelector") {
404
+ for (column in listOf(mainColumn, trailingColumn)) {
405
+ if (column.parent !== this || column.visibility == GONE) continue
406
+ val margins = column.layoutParams as MarginLayoutParams
407
+ val available =
408
+ height - paddingTop - paddingBottom - margins.topMargin - margins.bottomMargin
409
+ column.offsetTopAndBottom(
410
+ paddingTop + margins.topMargin + (available - column.height + 1) / 2 - column.top
411
+ )
412
+ }
413
+ }
414
+ }
415
+
416
+ override fun recycleContent() {
417
+ leadingFrame.recycle()
418
+ trailingColumn.reset()
419
+ currentItem = null
420
+ }
421
+
422
+ override fun disposeContent() {
423
+ recycleContent()
424
+ leadingFrame.dispose()
425
+ }
426
+
427
+ private fun bindIdentity(
428
+ item: NativeListItem,
429
+ theme: JSONObject?,
430
+ selected: Boolean,
431
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
432
+ ) {
433
+ if (item.json.optString("presentation") == "walletSidebar") {
434
+ orientation = VERTICAL
435
+ gravity = Gravity.CENTER
436
+ setPadding(dp(4), dp(4), dp(4), dp(4))
437
+ addLeading(item.json.optJSONObject("leading"), 40, spacingDp = 0)
438
+ leadingFallback.textSize = sp(28f)
439
+ leadingFallback.typeface = NativeListFonts.regular(context)
440
+ mainColumn.gravity = Gravity.CENTER
441
+ titleLine.gravity = Gravity.CENTER
442
+ titleLine.packsChildrenAtStart = false
443
+ titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
444
+ // OneKey patch: the original wallet title ellipsizes within the full inner row width.
445
+ title.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
446
+ title.gravity = Gravity.CENTER
447
+ // OneKey patch: this branch returns before the common identity ellipsis setup.
448
+ if (item.hasExplicitHeight) title.ellipsize = TextUtils.TruncateAt.END
449
+ showText(title, item.json.optString("title"), 1)
450
+ title.setTextColor(
451
+ color(
452
+ theme,
453
+ if (selected) "primaryText" else "secondaryText",
454
+ if (selected) "#FFFFFFED" else "#FFFFFFAF",
455
+ )
456
+ )
457
+ addView(
458
+ mainColumn,
459
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
460
+ // OneKey patch: snap the source 4 + 40 + 4 sequence once instead of rounding each gap.
461
+ topMargin = if (item.hasExplicitHeight) dp(48) - dp(44) else dp(4)
462
+ },
463
+ )
464
+ // OneKey patch: wallet tags are a centered line below the wallet name.
465
+ item.json
466
+ .optJSONArray("badges")
467
+ ?.takeIf { it.length() > 0 }
468
+ ?.let { badges ->
469
+ val isSelector = item.hasExplicitHeight
470
+ val badgeLineHeight = if (isSelector) 14 else 16
471
+ val badgeHeight = badgeLineHeight + 4
472
+ val line =
473
+ LinearLayout(context).apply {
474
+ orientation = HORIZONTAL
475
+ gravity = Gravity.CENTER
476
+ }
477
+ for (index in 0 until badges.length()) {
478
+ val badge =
479
+ NativeListTextView(context).apply {
480
+ text = badges.getJSONObject(index).optString("text")
481
+ textSize = sp(if (isSelector) 11f else 12f)
482
+ typeface = NativeListFonts.regular(context)
483
+ includeFontPadding = false
484
+ maxLines = 1
485
+ ellipsize = TextUtils.TruncateAt.END
486
+ val warning =
487
+ isSelector && badges.getJSONObject(index).optString("tone") == "warning"
488
+ setTextColor(
489
+ color(
490
+ theme,
491
+ if (warning) "caution" else "secondaryText",
492
+ if (warning) "#AB6400" else "#0000009B",
493
+ )
494
+ )
495
+ background =
496
+ roundedFill(
497
+ color(
498
+ theme,
499
+ if (warning) "cautionBackground"
500
+ else if (isSelector) "subduedBackground" else "strongBackground",
501
+ if (warning) "#FFF4D5" else "#00000006",
502
+ ),
503
+ 4f,
504
+ )
505
+ setPadding(dp(if (isSelector) 6 else 4), dp(2), dp(if (isSelector) 6 else 4), dp(2))
506
+ }
507
+ TextViewCompat.setLineHeight(badge, dp(badgeLineHeight))
508
+ semanticBadgeLabels.add(badge)
509
+ line.addView(
510
+ badge,
511
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply {
512
+ if (index > 0) marginStart = dp(4)
513
+ },
514
+ )
515
+ }
516
+ walletBadgeLine = line
517
+ mainColumn.addView(
518
+ line,
519
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply { topMargin = dp(4) },
520
+ )
521
+ }
522
+ val accessories = item.json.optJSONArray("trailing") ?: JSONArray()
523
+ if (accessories.length() > 0) {
524
+ addView(trailingColumn, wrap().apply { topMargin = dp(4) })
525
+ trailingColumn.bind(
526
+ item,
527
+ accessories,
528
+ theme,
529
+ item.json.optJSONObject("style") ?: JSONObject(),
530
+ sourceScale,
531
+ checkboxState,
532
+ horizontal = true,
533
+ )
534
+ }
535
+ return
536
+ }
537
+ if (item.json.optString("presentation") == "networkSelector") {
538
+ // OneKey patch: the explicit 48-point row centers its 32-point network icon.
539
+ // setPadding(dp(12), dp(7), dp(12), dp(8))
540
+ setPadding(dp(12), dp(if (item.hasExplicitHeight) 8 else 7), dp(12), dp(8))
541
+ }
542
+ val leading = item.json.optJSONObject("leading")
543
+ item.json.optJSONObject("leadingAction")?.let { action ->
544
+ leadingActionIcon.visibility = VISIBLE
545
+ leadingActionIcon.iconName = action.optString("name")
546
+ leadingActionIcon.glyphSizeDp = 24
547
+ leadingActionIcon.tintColor =
548
+ safeColor(action.optString("tintColor"), color(theme, "icon", "#0000009B"))
549
+ leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
550
+ leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
551
+ leadingActionIcon.setOnClickListener {
552
+ emitAction(item, action.optString("actionKey"), null, leadingActionIcon, "leadingAction")
553
+ }
554
+ // ListItem.IconButton is 36dp with 6dp inner padding and m=-7. Keep the
555
+ // full button frame but absorb its leading negative margin into the row
556
+ // padding and its trailing margin into the following gap.
557
+ setPadding(dp(5), paddingTop, paddingRight, paddingBottom)
558
+ addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
559
+ }
560
+ addLeading(
561
+ leading,
562
+ if (
563
+ leading?.optString("kind") == "network" ||
564
+ item.json.optString("presentation") == "accountSelector"
565
+ )
566
+ 32
567
+ else 40,
568
+ )
569
+ // OneKey patch: custom network initials retain LetterAvatar typography.
570
+ if (
571
+ item.json.optString("presentation") == "networkSelector" &&
572
+ leading?.optJSONObject("image") == null &&
573
+ leading?.optJSONObject("fallbackIcon") == null &&
574
+ !leading?.optString("fallbackText").isNullOrEmpty()
575
+ ) {
576
+ leadingFallback.textSize = sp(19f)
577
+ leadingFallback.typeface = NativeListFonts.semibold(context)
578
+ leadingFallback.setTextColor(color(theme, "inverseText", "#FCFCFC"))
579
+ TextViewCompat.setLineHeight(leadingFallback, dp(27))
580
+ }
581
+ addView(mainColumn, weighted())
582
+ titleLine.packsChildrenAtStart = true
583
+ title.ellipsize = TextUtils.TruncateAt.END
584
+ showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
585
+ if (item.json.optString("presentation") == "accountSelector") {
586
+ title.typeface = NativeListFonts.regular(context)
587
+ }
588
+ showText(subtitle, item.json.optString("subtitle"), item.json.optInt("subtitleLines", 2))
589
+ // OneKey patch: use separate labels for independently truncated balance/address.
590
+ item.json
591
+ .optJSONArray("subtitleSegments")
592
+ ?.takeIf { it.length() > 0 }
593
+ ?.let { segments ->
594
+ subtitle.visibility = GONE
595
+ val line =
596
+ SelectorSubtitleLayout(context).apply {
597
+ orientation = HORIZONTAL
598
+ gravity = Gravity.CENTER_VERTICAL
599
+ }
600
+ for (index in 0 until segments.length()) {
601
+ val segment = segments.getJSONObject(index)
602
+ if (segment.optBoolean("separatorBefore", false)) {
603
+ val dot =
604
+ View(context).apply {
605
+ background = roundedFill(color(theme, "disabledText", "#00000072"), 2f)
606
+ }
607
+ line.addView(
608
+ dot,
609
+ LayoutParams(dp(4), dp(4)).apply {
610
+ marginStart = dp(6)
611
+ marginEnd = dp(6)
612
+ },
613
+ )
614
+ }
615
+ val label =
616
+ NativeListTextView(context).apply {
617
+ text = segment.optString("text")
618
+ typeface = NativeListFonts.regular(context)
619
+ textSize = sp(14f)
620
+ includeFontPadding = false
621
+ maxLines = 1
622
+ ellipsize = TextUtils.TruncateAt.END
623
+ val toneKey =
624
+ when (segment.optString("tone")) {
625
+ "primary" -> "primaryText"
626
+ "disabled" -> "disabledText"
627
+ "caution" -> "caution"
628
+ "positive" -> "positive"
629
+ "negative" -> "negative"
630
+ else -> "secondaryText"
631
+ }
632
+ setTextColor(
633
+ color(theme, toneKey, if (toneKey == "caution") "#AB6400" else "#0000009B")
634
+ )
635
+ }
636
+ TextViewCompat.setLineHeight(label, dp(20))
637
+ applyValueSegments(label, segment.optJSONArray("textSegments"), 14, 20, false)
638
+ semanticSubtitleLabels.add(label)
639
+ line.addView(label, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
640
+ }
641
+ mainColumn.addView(line, 2, LayoutParams(LayoutParams.MATCH_PARENT, dp(20)))
642
+ }
643
+ item.json
644
+ .optJSONArray("titleMatch")
645
+ ?.takeIf { it.length() > 0 }
646
+ ?.let { matches ->
647
+ val highlighted = SpannableStringBuilder(title.text)
648
+ for (index in 0 until matches.length()) {
649
+ val match = matches.getJSONObject(index)
650
+ val start = match.optInt("start")
651
+ val end = match.optInt("end")
652
+ if (start >= 0 && end > start && end <= highlighted.length)
653
+ highlighted.setSpan(
654
+ ForegroundColorSpan(color(theme, "info", "#0D74CE")),
655
+ start,
656
+ end,
657
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
658
+ )
659
+ }
660
+ title.text = highlighted
661
+ }
662
+ showText(tertiary, item.json.optString("tertiary"), 1)
663
+ tertiary.setTextColor(
664
+ color(
665
+ theme,
666
+ if (item.json.optString("tertiaryTone") == "info") "info" else "secondaryText",
667
+ if (item.json.optString("tertiaryTone") == "info") "#006DCBF2" else "#0000009B",
668
+ )
669
+ )
670
+ val badges = item.json.optJSONArray("badges")
671
+ if (badges != null && badges.length() > 0) {
672
+ val texts =
673
+ (0 until minOf(2, badges.length())).map { badges.getJSONObject(it).optString("text") }
674
+ showText(badgeLine, texts.joinToString(" "), 1)
675
+ badgeLine.layoutParams = wrap().apply { marginStart = dp(8) }
676
+ badgeLine.setTextColor(color(theme, "secondaryText", "#0000009B"))
677
+ badgeLine.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 4f)
678
+ badgeLine.setPadding(dp(8), dp(2), dp(8), dp(2))
679
+ TextViewCompat.setLineHeight(badgeLine, dp(16))
680
+ }
681
+ addView(trailingColumn, wrap())
682
+ val accessories = item.json.optJSONArray("trailing") ?: JSONArray()
683
+ if (
684
+ item.hasExplicitHeight &&
685
+ item.json.optString("presentation") == "networkSelector" &&
686
+ accessories.hasAccessory("checkbox")
687
+ ) {
688
+ // OneKey patch: retain ListItem's title-to-accessory gap when measuring truncation.
689
+ (mainColumn.layoutParams as LayoutParams).marginEnd = dp(12)
690
+ }
691
+ trailingColumn.bind(
692
+ item,
693
+ accessories,
694
+ theme,
695
+ item.json.optJSONObject("style") ?: JSONObject(),
696
+ sourceScale,
697
+ checkboxState,
698
+ horizontal =
699
+ (0 until accessories.length())
700
+ .map { accessories.getJSONObject(it).optString("kind") }
701
+ .let { kinds ->
702
+ kinds.containsAll(listOf("value", "checkbox")) ||
703
+ kinds == listOf("valuePair", "menu") ||
704
+ (kinds == listOf("icon", "icon") &&
705
+ accessories.getJSONObject(0).optString("name") == "PencilOutline" &&
706
+ accessories.getJSONObject(1).optString("name") == "DragOutline")
707
+ },
708
+ )
709
+ trailingColumn.endInset?.let { setPadding(paddingLeft, paddingTop, it, paddingBottom) }
710
+ }
711
+
712
+ private fun applyValueSegments(
713
+ view: TextView,
714
+ segments: JSONArray?,
715
+ fontSize: Int = 16,
716
+ lineHeight: Int = 24,
717
+ medium: Boolean = true,
718
+ ) {
719
+ if (segments == null || segments.length() == 0) return
720
+ val value = SpannableStringBuilder()
721
+ for (index in 0 until segments.length()) {
722
+ val segment = segments.getJSONObject(index)
723
+ val start = value.length
724
+ value.append(segment.optString("text"))
725
+ if (segment.optString("style") == "subscript") {
726
+ val size = kotlin.math.ceil(fontSize * 0.6).toFloat()
727
+ val span =
728
+ if (sourceScale)
729
+ AbsoluteSizeSpan(
730
+ kotlin.math.ceil((size * resources.displayMetrics.density).toDouble()).toInt(),
731
+ false,
732
+ )
733
+ else AbsoluteSizeSpan(sp(size).roundToInt(), true)
734
+ value.setSpan(span, start, value.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
735
+ }
736
+ }
737
+ view.textSize = sp(fontSize.toFloat())
738
+ view.typeface =
739
+ if (medium) NativeListFonts.medium(context) else NativeListFonts.regular(context)
740
+ TextViewCompat.setLineHeight(view, dp(lineHeight))
741
+ view.text = value
742
+ }
743
+
744
+ private fun applyStyledText(view: TextView, style: JSONObject) {
745
+ if (style.has("fontSize")) {
746
+ view.setTextSize(
747
+ TypedValue.COMPLEX_UNIT_PX,
748
+ (style.optDouble("fontSize") * resources.displayMetrics.density).toFloat(),
749
+ )
750
+ val text = SpannableStringBuilder(view.text)
751
+ text.getSpans(0, text.length, AbsoluteSizeSpan::class.java).forEach(text::removeSpan)
752
+ view.text = text
753
+ }
754
+ style.optString("fontWeight").takeIf(String::isNotEmpty)?.let {
755
+ view.typeface =
756
+ when (it) {
757
+ "medium" -> NativeListFonts.medium(context)
758
+ "semibold" -> NativeListFonts.semibold(context)
759
+ "bold" -> NativeListFonts.bold(context)
760
+ else -> NativeListFonts.regular(context)
761
+ }
762
+ }
763
+ style.optString("color").takeIf(String::isNotEmpty)?.let {
764
+ val color = safeColor(it, view.currentTextColor)
765
+ view.setTextColor(color)
766
+ val text = SpannableStringBuilder(view.text)
767
+ text.getSpans(0, text.length, ForegroundColorSpan::class.java).forEach(text::removeSpan)
768
+ view.text = text
769
+ }
770
+ if (style.has("lineHeight")) {
771
+ val text = SpannableStringBuilder(view.text)
772
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
773
+ text.setSpan(
774
+ NativeListLineHeightSpan(stylePx(style.optDouble("lineHeight"))),
775
+ 0,
776
+ text.length,
777
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
778
+ )
779
+ view.setLineSpacing(0f, 1f)
780
+ view.text = text
781
+ }
782
+ if (style.has("lines")) {
783
+ view.maxLines = style.optInt("lines").coerceIn(1, 3)
784
+ view.ellipsize = TextUtils.TruncateAt.END
785
+ }
786
+ if (style.has("truncate") || style.has("lines")) {
787
+ view.setHorizontallyScrolling(view.maxLines == 1)
788
+ view.ellipsize =
789
+ if (style.optString("truncate", "tail") == "clip") null else TextUtils.TruncateAt.END
790
+ if (view.maxLines == 1) {
791
+ val text = SpannableStringBuilder(view.text)
792
+ Regex("\\r\\n|[\\r\\n]").findAll(text).toList().asReversed().forEach { match ->
793
+ text.replace(match.range.first, match.range.last + 1, " ")
794
+ }
795
+ view.text = text
796
+ }
797
+ }
798
+ if (style.has("verticalAlignment")) {
799
+ view.gravity =
800
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
801
+ when (style.optString("verticalAlignment")) {
802
+ "top" -> Gravity.TOP
803
+ "bottom" -> Gravity.BOTTOM
804
+ else -> Gravity.CENTER_VERTICAL
805
+ }
806
+ }
807
+ if (style.has("offsetY"))
808
+ (view as? NativeListTextView)?.opticalOffsetY =
809
+ (style.optDouble("offsetY") * resources.displayMetrics.density).toFloat()
810
+ style.optString("alignment").takeIf(String::isNotEmpty)?.let {
811
+ view.gravity =
812
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) or
813
+ when (it) {
814
+ "center" -> Gravity.CENTER_HORIZONTAL
815
+ "end" -> Gravity.END
816
+ else -> Gravity.START
817
+ }
818
+ }
819
+ }
820
+ }