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

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,551 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Outline
5
+ import android.graphics.Typeface
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.text.TextUtils
8
+ import android.view.Gravity
9
+ import android.view.View
10
+ import android.view.ViewOutlineProvider
11
+ import android.widget.LinearLayout
12
+ import android.widget.TextView
13
+ import androidx.core.widget.TextViewCompat
14
+ import com.facebook.react.uimanager.ThemedReactContext
15
+ import org.json.JSONArray
16
+ import org.json.JSONObject
17
+
18
+ internal class NativeListMetricCardRowView(context: ThemedReactContext) :
19
+ NativeListRendererRowView(context) {
20
+ private val visual = NativeListLeadingVisual(context)
21
+ private val column = LinearLayout(context).apply { orientation = VERTICAL }
22
+ private val titleLine =
23
+ LinearLayout(context).apply {
24
+ orientation = HORIZONTAL
25
+ gravity = Gravity.CENTER_VERTICAL
26
+ }
27
+ private val title = NativeListTextView(context)
28
+ private val label = NativeListTextView(context)
29
+ private val trend = NativeListTextView(context)
30
+ private val detail = NativeListTextView(context)
31
+ private val badge = NativeListTextView(context)
32
+ private val metricImages = List(5) { NativeListImageSlot(context) }
33
+ private val usedImages = mutableSetOf<Int>()
34
+ private var rowKey = ""
35
+ private var compositeMetricTitle: TextView? = null
36
+ override val assetFields = listOf("visual", "metrics")
37
+ override val defaultCornerRadius = 12
38
+
39
+ override fun unselectedBackground(
40
+ item: NativeListItem,
41
+ theme: JSONObject?,
42
+ layout: String,
43
+ itemIndex: Int?,
44
+ ) =
45
+ if (layout == "sectioned" && !item.json.optBoolean("selected"))
46
+ super.unselectedBackground(item, theme, layout, itemIndex)
47
+ else color(theme, "subduedBackground", "#F9F9F9")
48
+
49
+ override fun defaultHeight(item: NativeListItem, layout: String) =
50
+ if (item.json.optString("variant") in setOf("activity", "performance")) 0 else 132
51
+
52
+ init {
53
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
54
+ titleLine.addView(badge)
55
+ column.addView(titleLine)
56
+ column.addView(label)
57
+ column.addView(trend)
58
+ column.addView(detail)
59
+ }
60
+
61
+ private fun color(theme: JSONObject?, name: String, fallback: String) =
62
+ parseNativeListColor(theme?.optString(name, fallback) ?: fallback)
63
+
64
+ private fun safeColor(value: String, fallback: Int) =
65
+ runCatching { parseNativeListColor(value) }.getOrDefault(fallback)
66
+
67
+ private fun roundedFill(color: Int, radius: Float) =
68
+ GradientDrawable().apply {
69
+ setColor(color)
70
+ cornerRadius = dp(radius.toInt()).toFloat()
71
+ }
72
+
73
+ private fun sp(value: Float) = if (sourceScale) value else NativeListScale.font(resources, value)
74
+
75
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
76
+
77
+ private val circleOutlineProvider =
78
+ object : ViewOutlineProvider() {
79
+ override fun getOutline(view: View, outline: Outline) {
80
+ outline.setOval(0, 0, view.width, view.height)
81
+ }
82
+ }
83
+
84
+ private fun visualSources(visual: JSONObject): List<Pair<JSONObject, String>> {
85
+ val kind = visual.optString("kind")
86
+ val variant =
87
+ when (kind) {
88
+ "token" -> "token"
89
+ "network" -> "network"
90
+ "account",
91
+ "wallet" -> "avatar"
92
+ else -> "generic"
93
+ }
94
+ if (kind == "icon") return emptyList()
95
+ if (kind == "stackedImages") {
96
+ val images = visual.optJSONArray("images") ?: return emptyList()
97
+ return (0 until images.length()).map { images.getJSONObject(it) to "generic" }
98
+ }
99
+ return visual.optJSONObject("image")?.let { listOf(it to variant) } ?: emptyList()
100
+ }
101
+
102
+ private fun dataTextColor(tone: String, theme: JSONObject?): Int =
103
+ when (tone) {
104
+ "positive" -> color(theme, "positive", "#00713FDE")
105
+ "negative" -> color(theme, "negative", "#C40006D3")
106
+ "secondary" -> color(theme, "secondaryText", "#0000009B")
107
+ else -> color(theme, "primaryText", "#000000DF")
108
+ }
109
+
110
+ override fun bindContent(
111
+ item: NativeListItem,
112
+ theme: JSONObject?,
113
+ layout: String,
114
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
115
+ ) {
116
+ removeAllViews()
117
+ usedImages.clear()
118
+ rowKey = item.key
119
+ compositeMetricTitle = null
120
+ orientation = VERTICAL
121
+ gravity = Gravity.START
122
+ val data = item.json
123
+ val style = data.optJSONObject("style") ?: JSONObject()
124
+ val image = style.optJSONObject("image") ?: JSONObject()
125
+ val hp =
126
+ if (style.has("horizontalPadding")) stylePx(style.optDouble("horizontalPadding")) else dp(14)
127
+ val vp =
128
+ if (style.has("verticalPadding")) stylePx(style.optDouble("verticalPadding")) else dp(14)
129
+ setPadding(hp, vp, hp, vp)
130
+ fun text(
131
+ view: NativeListTextView,
132
+ value: String,
133
+ slot: String,
134
+ size: Float,
135
+ weight: String = "regular",
136
+ ink: Int = color(theme, "secondaryText", "#0000009B"),
137
+ ) {
138
+ NativeListResolvedText.resolve(
139
+ context,
140
+ value,
141
+ style.optJSONObject(slot),
142
+ size,
143
+ weight,
144
+ ink,
145
+ 0,
146
+ 1,
147
+ sourceScale || slot == "subtitle",
148
+ )
149
+ .bind(view)
150
+ if (style.optJSONObject(slot)?.optString("truncate") != "clip")
151
+ view.ellipsize = TextUtils.TruncateAt.END
152
+ }
153
+ if (data.optString("variant") in setOf("activity", "performance")) {
154
+ visual.recycle()
155
+ bindCompositeMetricCard(item, theme, data.optString("variant"))
156
+ val heading = compositeMetricTitle as? NativeListTextView
157
+ if (heading != null) {
158
+ val spacing = heading.letterSpacing
159
+ NativeListResolvedText.resolve(
160
+ context,
161
+ data.optString("title").uppercase(),
162
+ style.optJSONObject("title"),
163
+ 11f,
164
+ "regular",
165
+ color(theme, "disabledText", "#00000072"),
166
+ 14,
167
+ 1,
168
+ sourceScale,
169
+ )
170
+ .bind(heading)
171
+ heading.letterSpacing = spacing
172
+ // Legacy metricText headings always ellipsize unless a style clips them.
173
+ if (style.optJSONObject("title")?.optString("truncate") != "clip")
174
+ heading.ellipsize = TextUtils.TruncateAt.END
175
+ }
176
+ if (style.has("lineGap")) {
177
+ val gap = stylePx(style.optDouble("lineGap"))
178
+ for (index in 1 until childCount) {
179
+ val params = getChildAt(index).layoutParams as LayoutParams
180
+ params.topMargin = gap
181
+ params.bottomMargin = 0
182
+ getChildAt(index).layoutParams = params
183
+ }
184
+ }
185
+ } else {
186
+ data.optJSONObject("visual")?.let { descriptor ->
187
+ (visual.parent as? android.view.ViewGroup)?.removeView(visual)
188
+ visual.bind(descriptor, image, item.key, theme, false, sourceScale)
189
+ addView(
190
+ visual,
191
+ LayoutParams(
192
+ if (image.has("width")) stylePx(image.optDouble("width")) else dp(32),
193
+ if (image.has("height")) stylePx(image.optDouble("height")) else dp(32),
194
+ )
195
+ .apply {
196
+ marginEnd = dp(12)
197
+ bottomMargin =
198
+ if (style.has("leadingGap")) stylePx(style.optDouble("leadingGap"))
199
+ else if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else 0
200
+ },
201
+ )
202
+ } ?: visual.recycle()
203
+ text(
204
+ title,
205
+ data.optString("value"),
206
+ "value",
207
+ if (data.optString("size") == "large") 24f else 18f,
208
+ "semibold",
209
+ color(theme, "primaryText", "#000000DF"),
210
+ )
211
+ text(
212
+ label,
213
+ data.optString("title"),
214
+ "title",
215
+ 11f,
216
+ ink = color(theme, "disabledText", "#00000072"),
217
+ )
218
+ text(
219
+ trend,
220
+ data.optString("trend"),
221
+ "trend",
222
+ 12f,
223
+ ink =
224
+ when (data.optString("trendTone")) {
225
+ "positive" -> color(theme, "positive", "#00713FDE")
226
+ "negative" -> color(theme, "negative", "#C40006D3")
227
+ else -> color(theme, "secondaryText", "#0000009B")
228
+ },
229
+ )
230
+ text(detail, data.optString("subtitle"), "subtitle", 14f)
231
+ NativeListResolvedText.resolve(
232
+ context,
233
+ data.optJSONObject("badge")?.optString("text") ?: "",
234
+ null,
235
+ 12f,
236
+ "medium",
237
+ color(theme, "accent", "#0D8200FC"),
238
+ 0,
239
+ 1,
240
+ sourceScale,
241
+ )
242
+ .bind(badge)
243
+ // Legacy: the badge follows the weighted title without an extra margin.
244
+ (badge.layoutParams as LayoutParams).marginStart = 0
245
+ for (view in listOf(label, trend, detail)) (view.layoutParams as LayoutParams).topMargin =
246
+ if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else 0
247
+ (column.parent as? android.view.ViewGroup)?.removeView(column)
248
+ addView(column, weightedWidth())
249
+ }
250
+ for (index in metricImages.indices) if (index !in usedImages) metricImages[index].recycle()
251
+ style
252
+ .optJSONObject("container")
253
+ ?.optString("contentVerticalAlignment")
254
+ ?.takeIf { it.isNotEmpty() }
255
+ ?.let {
256
+ gravity =
257
+ Gravity.START or
258
+ when (it) {
259
+ "top" -> Gravity.TOP
260
+ "bottom" -> Gravity.BOTTOM
261
+ else -> Gravity.CENTER_VERTICAL
262
+ }
263
+ }
264
+ }
265
+
266
+ override fun recycleContent() {
267
+ visual.recycle()
268
+ metricImages.forEach { it.recycle() }
269
+ removeAllViews()
270
+ }
271
+
272
+ override fun disposeContent() {
273
+ visual.dispose()
274
+ metricImages.forEach { it.dispose() }
275
+ removeAllViews()
276
+ }
277
+
278
+ private fun bindCompositeMetricCard(item: NativeListItem, theme: JSONObject?, variant: String) {
279
+ val metrics = item.json.optJSONArray("metrics") ?: JSONArray()
280
+ addView(
281
+ metricText(
282
+ value = item.json.optString("title").uppercase(),
283
+ size = 11f,
284
+ lineHeight = 14,
285
+ typeface = NativeListFonts.regular(context),
286
+ textColor = color(theme, "disabledText", "#00000072"),
287
+ letterSpacingDp = 1.2f,
288
+ )
289
+ .also { compositeMetricTitle = it },
290
+ weightedWidth(),
291
+ )
292
+ if (variant == "activity") {
293
+ addView(activityHeroMetrics(metrics, theme), weightedWidth().apply { topMargin = dp(14) })
294
+ addView(
295
+ View(context).apply { setBackgroundColor(color(theme, "separator", "#0000001F")) },
296
+ LayoutParams(LayoutParams.MATCH_PARENT, 1).apply {
297
+ topMargin = dp(14)
298
+ bottomMargin = dp(14)
299
+ },
300
+ )
301
+ addView(activityCompactMetrics(metrics, theme), weightedWidth())
302
+ } else {
303
+ val winRateRow =
304
+ LinearLayout(context).apply {
305
+ orientation = HORIZONTAL
306
+ gravity = Gravity.BOTTOM
307
+ if (metrics.length() > 0) {
308
+ addView(
309
+ makeMetricColumn(
310
+ metric = metrics.getJSONObject(0),
311
+ visualSlot = 0,
312
+ theme = theme,
313
+ alignment = Gravity.START,
314
+ valueSize = 18f,
315
+ valueLineHeight = 24,
316
+ valueTypeface = NativeListFonts.semibold(context),
317
+ ),
318
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
319
+ )
320
+ }
321
+ if (metrics.length() > 1) {
322
+ addView(
323
+ makeMetricColumn(
324
+ metric = metrics.getJSONObject(1),
325
+ visualSlot = 1,
326
+ theme = theme,
327
+ alignment = Gravity.END,
328
+ valueSize = 14f,
329
+ valueLineHeight = 20,
330
+ valueTypeface = NativeListFonts.semibold(context),
331
+ ),
332
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
333
+ )
334
+ }
335
+ }
336
+ addView(winRateRow, weightedWidth().apply { topMargin = dp(14) })
337
+ val progressValue = item.json.optDouble("progress", 0.0).coerceIn(0.0, 1.0)
338
+ val progress =
339
+ LinearLayout(context).apply {
340
+ orientation = HORIZONTAL
341
+ clipToOutline = true
342
+ outlineProvider =
343
+ object : ViewOutlineProvider() {
344
+ override fun getOutline(view: View, outline: Outline) {
345
+ outline.setRoundRect(0, 0, view.width, view.height, dp(2).toFloat())
346
+ }
347
+ }
348
+ if (progressValue > 0.0) {
349
+ addView(
350
+ View(context).apply { setBackgroundColor(parseNativeListColor("#22AB15")) },
351
+ LayoutParams(0, LayoutParams.MATCH_PARENT, progressValue.toFloat()),
352
+ )
353
+ }
354
+ if (progressValue < 1.0) {
355
+ addView(
356
+ View(context).apply { setBackgroundColor(parseNativeListColor("#E5484D")) },
357
+ LayoutParams(0, LayoutParams.MATCH_PARENT, (1.0 - progressValue).toFloat()),
358
+ )
359
+ }
360
+ }
361
+ addView(progress, LayoutParams(LayoutParams.MATCH_PARENT, dp(4)).apply { topMargin = dp(8) })
362
+ addView(
363
+ performanceAverageMetrics(metrics, theme),
364
+ weightedWidth().apply { topMargin = dp(14) },
365
+ )
366
+ }
367
+ }
368
+
369
+ private fun activityHeroMetrics(metrics: JSONArray, theme: JSONObject?) =
370
+ LinearLayout(context).apply {
371
+ orientation = HORIZONTAL
372
+ gravity = Gravity.BOTTOM
373
+ if (metrics.length() > 0) {
374
+ addView(
375
+ makeMetricColumn(
376
+ metric = metrics.getJSONObject(0),
377
+ visualSlot = 0,
378
+ theme = theme,
379
+ alignment = Gravity.START,
380
+ valueSize = 16f,
381
+ valueLineHeight = 24,
382
+ valueTypeface = NativeListFonts.semibold(context),
383
+ ),
384
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
385
+ )
386
+ }
387
+ if (metrics.length() > 1) {
388
+ addView(
389
+ makeMetricColumn(
390
+ metric = metrics.getJSONObject(1),
391
+ visualSlot = 1,
392
+ theme = theme,
393
+ alignment = Gravity.END,
394
+ valueSize = 14f,
395
+ valueLineHeight = 20,
396
+ valueTypeface = NativeListFonts.semibold(context),
397
+ ),
398
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
399
+ )
400
+ }
401
+ }
402
+
403
+ private fun activityCompactMetrics(metrics: JSONArray, theme: JSONObject?) =
404
+ LinearLayout(context).apply {
405
+ orientation = HORIZONTAL
406
+ gravity = Gravity.CENTER_VERTICAL
407
+ for (index in 2 until metrics.length()) {
408
+ val alignment =
409
+ when (index) {
410
+ 2 -> Gravity.START
411
+ metrics.length() - 1 -> Gravity.END
412
+ else -> Gravity.CENTER_HORIZONTAL
413
+ }
414
+ addView(
415
+ makeMetricColumn(
416
+ metric = metrics.getJSONObject(index),
417
+ visualSlot = index,
418
+ theme = theme,
419
+ alignment = alignment,
420
+ valueSize = 14f,
421
+ valueLineHeight = 20,
422
+ valueTypeface = NativeListFonts.medium(context),
423
+ ),
424
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
425
+ )
426
+ }
427
+ }
428
+
429
+ private fun performanceAverageMetrics(metrics: JSONArray, theme: JSONObject?) =
430
+ LinearLayout(context).apply {
431
+ orientation = HORIZONTAL
432
+ val indices =
433
+ listOf(2, metrics.length() - 1).distinct().filter { it in 0 until metrics.length() }
434
+ indices.forEachIndexed { position, index ->
435
+ val metric = metrics.getJSONObject(index)
436
+ val card =
437
+ makeMetricColumn(
438
+ metric = metric,
439
+ visualSlot = index,
440
+ theme = theme,
441
+ alignment = Gravity.START,
442
+ valueSize = 14f,
443
+ valueLineHeight = 20,
444
+ valueTypeface = NativeListFonts.medium(context),
445
+ )
446
+ .apply {
447
+ setPadding(dp(10), dp(10), dp(10), dp(10))
448
+ background = roundedFill(color(theme, "strongBackground", "#0000000F"), 8f)
449
+ }
450
+ addView(
451
+ card,
452
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f).apply {
453
+ if (position > 0) marginStart = dp(8)
454
+ },
455
+ )
456
+ }
457
+ }
458
+
459
+ private fun makeMetricColumn(
460
+ metric: JSONObject,
461
+ visualSlot: Int,
462
+ theme: JSONObject?,
463
+ alignment: Int,
464
+ valueSize: Float,
465
+ valueLineHeight: Int,
466
+ valueTypeface: Typeface,
467
+ ) =
468
+ LinearLayout(context).apply {
469
+ orientation = VERTICAL
470
+ gravity = alignment
471
+ addView(
472
+ metricText(
473
+ value = metric.optString("label"),
474
+ size = 11f,
475
+ lineHeight = 14,
476
+ typeface = NativeListFonts.regular(context),
477
+ textColor = color(theme, "disabledText", "#00000072"),
478
+ gravity = alignment,
479
+ )
480
+ )
481
+ val valueText =
482
+ metricText(
483
+ value = metric.optString("value"),
484
+ size = valueSize,
485
+ lineHeight = valueLineHeight,
486
+ typeface = valueTypeface,
487
+ textColor = dataTextColor(metric.optString("tone"), theme),
488
+ gravity = alignment,
489
+ )
490
+ val valueVisual = metric.optJSONObject("visual")
491
+ val valueView =
492
+ if (valueVisual != null && visualSlot in metricImages.indices) {
493
+ LinearLayout(context).apply {
494
+ orientation = HORIZONTAL
495
+ gravity = Gravity.CENTER_VERTICAL
496
+ val slot = metricImages[visualSlot]
497
+ usedImages.add(visualSlot)
498
+ val image = slot.view
499
+ (image.parent as? android.view.ViewGroup)?.removeView(image)
500
+ image.background = null
501
+ image.visibility = VISIBLE
502
+ image.outlineProvider = circleOutlineProvider
503
+ image.clipToOutline = true
504
+ valueVisual.optString("backgroundColor").takeIf(String::isNotEmpty)?.let {
505
+ backgroundColor ->
506
+ image.background = roundedFill(safeColor(backgroundColor, Color.WHITE), 8f)
507
+ }
508
+ visualSources(valueVisual).firstOrNull()?.let { (source, variant) ->
509
+ slot.bind(source, rowKey + ":" + visualSlot + ":" + metric.optString("key"), variant)
510
+ } ?: slot.recycle()
511
+ addView(image, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
512
+ addView(valueText, wrap())
513
+ }
514
+ } else {
515
+ valueText
516
+ }
517
+ addView(
518
+ valueView,
519
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
520
+ topMargin = dp(2)
521
+ },
522
+ )
523
+ }
524
+
525
+ private fun metricText(
526
+ value: String,
527
+ size: Float,
528
+ lineHeight: Int,
529
+ typeface: Typeface,
530
+ textColor: Int,
531
+ letterSpacingDp: Float = 0f,
532
+ gravity: Int = Gravity.START,
533
+ ) =
534
+ NativeListTextView(context).apply {
535
+ includeFontPadding = false
536
+ text = value
537
+ textSize = sp(size)
538
+ this.typeface = typeface
539
+ setTextColor(textColor)
540
+ this.gravity = gravity
541
+ maxLines = 1
542
+ ellipsize = TextUtils.TruncateAt.END
543
+ fontFeatureSettings = "tnum"
544
+ TextViewCompat.setLineHeight(this, dp(lineHeight))
545
+ if (letterSpacingDp > 0f) {
546
+ letterSpacing = letterSpacingDp / sp(size)
547
+ }
548
+ }
549
+
550
+ private fun weightedWidth() = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
551
+ }
@@ -6,6 +6,8 @@ import java.nio.ByteBuffer
6
6
  import java.security.MessageDigest
7
7
  import java.util.Locale
8
8
 
9
+ internal data class NativeSelectionTarget(val scope: String, val key: String?)
10
+
9
11
  internal fun isNativeListRowPressEnabled(
10
12
  type: String,
11
13
  variant: String,
@@ -25,6 +27,13 @@ internal fun isNativeListWholeRowInteractive(
25
27
  pressDisabled = pressDisabled,
26
28
  )
27
29
 
30
+ internal fun resolveNativeListSelectorBackgroundColor(
31
+ rowType: String,
32
+ styleBackgroundColor: String?,
33
+ rowBackgroundColor: String?,
34
+ ): String? = styleBackgroundColor?.takeIf(String::isNotEmpty)
35
+ ?: rowBackgroundColor?.takeIf { rowType != "walletGroup" && it.isNotEmpty() }
36
+
28
37
  // The image fallback-state cache is process-wide, so it is keyed by a digest of the request
29
38
  // identity instead of the raw headers, which can carry credentials such as Authorization.
30
39
  internal fun nativeListSourceFallbackStateKey(uri: String, headers: Map<String, String>): String? {
@@ -47,6 +56,9 @@ private fun updateLengthPrefixed(digest: MessageDigest, value: String) {
47
56
  digest.update(bytes)
48
57
  }
49
58
 
59
+ // Template type alone partitions reuse; data keys and styles never select a renderer.
60
+ internal enum class NativeListRendererKey { MESSAGE, RAIL, MEDIA_TILE, ACTION, SYSTEM, ACTIVITY, DATA_ROW, METRIC_CARD, MARKET, IDENTITY, SECTION_HEADER, WALLET_GROUP }
61
+
50
62
  internal data class NativeListItem(
51
63
  val key: String,
52
64
  val type: String,
@@ -55,6 +67,15 @@ internal data class NativeListItem(
55
67
  val json: JSONObject,
56
68
  ) {
57
69
  val content: String = json.toString()
70
+ val rendererKey: NativeListRendererKey
71
+ get() = NativeListRendererRegistry.key(type)
72
+ val styledHeight: Double?
73
+ get() = json.optJSONObject("style")?.optJSONObject("container")
74
+ ?.takeIf { it.has("height") }?.optDouble("height")
75
+ // Selector presentations use their explicit-height geometry when either explicit height field
76
+ // is set (Web `hasExplicitRowHeight`). Row-local only; list-wide source scale stays row.height.
77
+ val hasExplicitHeight: Boolean
78
+ get() = styledHeight != null || json.has("height")
58
79
  // OneKey patch: only a host-validated stable snapshot can request a lightweight diff payload.
59
80
  var selectionUpdateFromContent: String? = null
60
81
 
@@ -107,6 +128,7 @@ internal data class NativeListItem(
107
128
  fun parse(json: JSONObject): NativeListItem {
108
129
  val key = json.getString("key")
109
130
  val type = json.getString("type")
131
+ NativeListRendererRegistry.key(type) // Reject unsupported templates at the model boundary.
110
132
  return NativeListItem(
111
133
  key = key,
112
134
  type = type,
@@ -141,6 +163,7 @@ internal data class NativeListConfig(
141
163
  val sectionIndexHapticsEnabled: Boolean,
142
164
  val sectionIndexCenteredInWindow: Boolean,
143
165
  val theme: JSONObject?,
166
+ val listStyle: JSONObject?,
144
167
  val fixedFooter: NativeListItem?,
145
168
  val items: List<NativeListItem>,
146
169
  ) {
@@ -190,6 +213,7 @@ internal data class NativeListConfig(
190
213
  sectionIndexHapticsEnabled = sectionIndex?.optBoolean("hapticsEnabled", true) ?: true,
191
214
  sectionIndexCenteredInWindow = sectionIndex?.optBoolean("centeredInWindow", false) ?: false,
192
215
  theme = root.optJSONObject("theme"),
216
+ listStyle = root.optJSONObject("listStyle"),
193
217
  fixedFooter = fixedFooter,
194
218
  items = items,
195
219
  )