@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,997 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Outline
5
+ import android.graphics.Paint
6
+ import android.graphics.Typeface
7
+ import android.graphics.drawable.GradientDrawable
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.text.Spannable
11
+ import android.text.SpannableStringBuilder
12
+ import android.text.TextUtils
13
+ import android.text.style.AbsoluteSizeSpan
14
+ import android.text.style.ForegroundColorSpan
15
+ import android.util.TypedValue
16
+ import android.view.Gravity
17
+ import android.view.MotionEvent
18
+ import android.view.View
19
+ import android.view.ViewOutlineProvider
20
+ import android.widget.LinearLayout
21
+ import android.widget.TextView
22
+ import androidx.core.widget.TextViewCompat
23
+ import androidx.recyclerview.widget.RecyclerView
24
+ import com.facebook.react.uimanager.ThemedReactContext
25
+ import kotlin.math.roundToInt
26
+ import org.json.JSONArray
27
+ import org.json.JSONObject
28
+
29
+ internal class NativeListMarketRowView(context: ThemedReactContext) :
30
+ NativeListRendererRowView(context) {
31
+ private val leadingFrame = NativeListLeadingVisual(context)
32
+ private val leadingActionIcon = OneKeyIconView(context)
33
+ private val mainColumn = LinearLayout(context)
34
+ private val titleLine = PackedTitleLineLayout(context)
35
+ private val title = NativeListTextView(context)
36
+ private val subtitle = NativeListTextView(context)
37
+ private val tertiary = NativeListTextView(context)
38
+ private val marketSubtitleLine = PackedTitleLineLayout(context)
39
+ private val trailingColumn = LinearLayout(context)
40
+ private val trailingViews = List(2) { NativeListTextView(context) }
41
+ private val marketBadgeViews = List(3) { LinearLayout(context) }
42
+ private val marketBadgeLabels = List(3) { TextView(context) }
43
+ private val badgeSlots = List(3) { NativeListImageSlot(context) }
44
+ private val marketBadgeImages
45
+ get() = badgeSlots.map { it.view }
46
+
47
+ private val marketBadgeGlyphs = List(3) { OneKeyIconView(context) }
48
+ private val marketOriginalPaintFlags = mutableMapOf<TextView, Int>()
49
+ private var imageSignature = ""
50
+ private var longPress: Runnable? = null
51
+ private val handler = Handler(Looper.getMainLooper())
52
+ private var held = false
53
+ private var longPressFired = false
54
+ private var startX = 0f
55
+ private var startY = 0f
56
+ private var touchX = 0f
57
+ private var touchY = 0f
58
+ override val assetFields = listOf("leading", "badges")
59
+
60
+ override fun usesSourceScale(item: NativeListItem, provided: Boolean) = true
61
+
62
+ override fun modelHeight(item: NativeListItem, layout: String) =
63
+ if (item.json.has("height")) stylePx(item.json.optDouble("height")) else null
64
+
65
+ override fun minimumContentHeight(item: NativeListItem, layout: String, sizeDelta: Int): Int {
66
+ val style = item.json.optJSONObject("style")
67
+ val stock = item.json.optString("variant") == "stock"
68
+ return stylePx(
69
+ maxOf(
70
+ if (stock) 72.0 else 68.0,
71
+ (style?.optJSONObject("image")?.optDouble("height", if (stock) 40.0 else 32.0)
72
+ ?: if (stock) 40.0 else 32.0) + 2 * (style?.optDouble("verticalPadding", 12.0) ?: 12.0),
73
+ )
74
+ )
75
+ }
76
+
77
+ init {
78
+ sourceScale = true
79
+ gravity = Gravity.CENTER_VERTICAL
80
+ clipChildren = false
81
+ clipToPadding = false
82
+ mainColumn.orientation = VERTICAL
83
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
84
+ titleLine.orientation = HORIZONTAL
85
+ titleLine.gravity = Gravity.CENTER_VERTICAL
86
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
87
+ mainColumn.addView(titleLine)
88
+ mainColumn.addView(subtitle)
89
+ mainColumn.addView(tertiary)
90
+ trailingViews.forEach(trailingColumn::addView)
91
+ leadingActionIcon.useSourceScale = true
92
+ marketBadgeViews.forEachIndexed { index, badge ->
93
+ badge.orientation = HORIZONTAL
94
+ badge.gravity = Gravity.CENTER_VERTICAL
95
+ marketBadgeGlyphs[index].useSourceScale = true
96
+ marketBadgeLabels[index].apply {
97
+ includeFontPadding = false
98
+ maxLines = 1
99
+ ellipsize = TextUtils.TruncateAt.END
100
+ }
101
+ badge.addView(marketBadgeGlyphs[index], LayoutParams(dp(16), dp(16)))
102
+ badge.addView(marketBadgeImages[index], LayoutParams(dp(14), dp(14)))
103
+ badge.addView(marketBadgeLabels[index], wrap())
104
+ }
105
+ }
106
+
107
+ private val circleOutlineProvider =
108
+ object : ViewOutlineProvider() {
109
+ override fun getOutline(view: View, outline: Outline) {
110
+ outline.setOval(0, 0, view.width, view.height)
111
+ }
112
+ }
113
+
114
+ private fun color(theme: JSONObject?, key: String, fallback: String) =
115
+ parseNativeListColor(theme?.optString(key, fallback) ?: fallback)
116
+
117
+ private fun safeColor(value: String?, fallback: Int) =
118
+ runCatching { parseNativeListColor(value ?: "") }.getOrDefault(fallback)
119
+
120
+ private fun roundedFill(color: Int, radius: Float) =
121
+ GradientDrawable().apply {
122
+ setColor(color)
123
+ cornerRadius = radius * resources.displayMetrics.density
124
+ }
125
+
126
+ private fun sp(value: Float) = value
127
+
128
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
129
+
130
+ private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
131
+
132
+ private fun showText(view: TextView, text: String, lines: Int) {
133
+ view.text = text
134
+ view.maxLines = lines
135
+ view.visibility = if (text.isEmpty()) GONE else VISIBLE
136
+ }
137
+
138
+ private fun emitAction(
139
+ item: NativeListItem,
140
+ key: String,
141
+ target: NativeSelectionTarget?,
142
+ view: View,
143
+ source: String,
144
+ slot: Int? = null,
145
+ ) {
146
+ emitAction(key, view, source, slot, target)
147
+ }
148
+
149
+ override fun accessibilityText(item: NativeListItem) =
150
+ item.json.optString(
151
+ "accessibilityLabel",
152
+ listOf(
153
+ item.json.optString("title"),
154
+ item.json.optString("subtitle"),
155
+ item.json.optString("price"),
156
+ item.json.optJSONObject("change")?.optString("text") ?: "",
157
+ )
158
+ .filter(String::isNotEmpty)
159
+ .joinToString(", "),
160
+ )
161
+
162
+ override fun bindContent(
163
+ item: NativeListItem,
164
+ theme: JSONObject?,
165
+ layout: String,
166
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
167
+ ) {
168
+ cancelMarketHold()
169
+ longPressFired = false
170
+ removeAllViews()
171
+ marketOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
172
+ marketOriginalPaintFlags.clear()
173
+ marketSubtitleLine.removeAllViews()
174
+ mainColumn.removeAllViews()
175
+ marketBadgeViews.forEachIndexed { index, badge ->
176
+ (badge.parent as? android.view.ViewGroup)?.removeView(badge)
177
+ badge.visibility = GONE
178
+ badge.setOnClickListener(null)
179
+ marketBadgeGlyphs[index].visibility = GONE
180
+ marketBadgeImages[index].visibility = GONE
181
+ marketBadgeLabels[index].setLineSpacing(0f, 1f)
182
+ marketBadgeLabels[index].layoutParams = wrap()
183
+ if (item.json.optJSONArray("badges")?.optJSONObject(index)?.optJSONObject("icon") == null)
184
+ badgeSlots[index].recycle()
185
+ }
186
+ for (view in listOf(title, subtitle, tertiary) + trailingViews) {
187
+ view.text = ""
188
+ view.visibility = GONE
189
+ view.maxLines = 1
190
+ view.ellipsize = TextUtils.TruncateAt.END
191
+ view.setLineSpacing(0f, 1f)
192
+ view.setHorizontallyScrolling(false)
193
+ view.opticalOffsetY = 0f
194
+ view.gravity = Gravity.CENTER_VERTICAL or Gravity.START
195
+ }
196
+ title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
197
+ mainColumn.addView(titleLine)
198
+ mainColumn.addView(subtitle)
199
+ mainColumn.addView(tertiary)
200
+ leadingActionIcon.iconName = ""
201
+ leadingActionIcon.setOnClickListener(null)
202
+ gravity = Gravity.CENTER_VERTICAL
203
+ bindMarket(item, theme)
204
+ item.json
205
+ .optJSONObject("style")
206
+ ?.optJSONObject("container")
207
+ ?.optString("contentVerticalAlignment")
208
+ ?.takeIf(String::isNotEmpty)
209
+ ?.let {
210
+ gravity =
211
+ when (it) {
212
+ "top" -> Gravity.TOP
213
+ "bottom" -> Gravity.BOTTOM
214
+ else -> Gravity.CENTER_VERTICAL
215
+ }
216
+ }
217
+ }
218
+
219
+ override fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {
220
+ if (retainBoundItem(item)) bindQuote(item, theme)
221
+ }
222
+
223
+ private fun cancelMarketHold() {
224
+ longPress?.let(handler::removeCallbacks)
225
+ longPress = null
226
+ }
227
+
228
+ override fun onRowTouch(event: MotionEvent) {
229
+ when (event.actionMasked) {
230
+ MotionEvent.ACTION_DOWN -> {
231
+ val item = tag as? NativeListItem
232
+ if (item?.isRowPressEnabled == true) {
233
+ held = true
234
+ longPressFired = false
235
+ startX = event.x
236
+ startY = event.y
237
+ touchX = event.x
238
+ touchY = event.y
239
+ item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let {
240
+ emitAction(it, this, "row")
241
+ }
242
+ item.json.optString("longPressActionKey").takeIf(String::isNotEmpty)?.let { key ->
243
+ val epoch = bindingEpoch
244
+ val task = Runnable {
245
+ val current = tag as? NativeListItem
246
+ if (held && current?.key == item.key && bindingEpoch == epoch) {
247
+ longPress = null
248
+ longPressFired = true
249
+ val location = IntArray(2)
250
+ getLocationInWindow(location)
251
+ onAction?.invoke(
252
+ current,
253
+ key,
254
+ null,
255
+ NativeListActionOrigin(
256
+ this,
257
+ this,
258
+ bindingEpoch,
259
+ "row",
260
+ windowPointPixels =
261
+ android.graphics.PointF(location[0] + touchX, location[1] + touchY),
262
+ ),
263
+ )
264
+ }
265
+ }
266
+ longPress = task
267
+ handler.postDelayed(task, 800)
268
+ }
269
+ }
270
+ }
271
+ MotionEvent.ACTION_MOVE -> {
272
+ touchX = event.x
273
+ touchY = event.y
274
+ if (
275
+ event.x < 0 ||
276
+ event.y < 0 ||
277
+ event.x >= width ||
278
+ event.y >= height ||
279
+ kotlin.math.abs(event.x - startX) > dp(10) ||
280
+ kotlin.math.abs(event.y - startY) > dp(10)
281
+ ) {
282
+ cancelMarketHold()
283
+ held = false
284
+ setTouchPressed(false)
285
+ }
286
+ }
287
+ MotionEvent.ACTION_UP,
288
+ MotionEvent.ACTION_CANCEL -> {
289
+ cancelMarketHold()
290
+ held = false
291
+ }
292
+ }
293
+ }
294
+
295
+ override fun consumesRowClick(): Boolean {
296
+ val value = longPressFired
297
+ longPressFired = false
298
+ return value
299
+ }
300
+
301
+ override fun recycleContent() {
302
+ cancelMarketHold()
303
+ held = false
304
+ longPressFired = false
305
+ leadingFrame.recycle()
306
+ badgeSlots.forEach { it.recycle() }
307
+ imageSignature = ""
308
+ }
309
+
310
+ override fun disposeContent() {
311
+ recycleContent()
312
+ leadingFrame.dispose()
313
+ badgeSlots.forEach { it.dispose() }
314
+ }
315
+
316
+ private fun marketTypeface(weight: String, fallback: String): Typeface =
317
+ when (weight.ifEmpty { fallback }) {
318
+ "regular" -> NativeListFonts.regular(context)
319
+ "semibold" -> NativeListFonts.semibold(context)
320
+ "bold" -> NativeListFonts.bold(context)
321
+ else -> NativeListFonts.medium(context)
322
+ }
323
+
324
+ private fun applyMarketTextStyle(
325
+ view: TextView,
326
+ style: JSONObject?,
327
+ defaultSize: Float,
328
+ defaultLineHeight: Int,
329
+ defaultWeight: String,
330
+ defaultColor: Int,
331
+ defaultAlignment: String,
332
+ ) {
333
+ view.includeFontPadding = false
334
+ view.fontFeatureSettings = "tnum"
335
+ view.textSize =
336
+ sp(style?.optDouble("fontSize", defaultSize.toDouble())?.toFloat() ?: defaultSize)
337
+ view.typeface = marketTypeface(style?.optString("fontWeight").orEmpty(), defaultWeight)
338
+ view.setTextColor(safeColor(style?.optString("color"), defaultColor))
339
+ val alignment = style?.optString("alignment", defaultAlignment) ?: defaultAlignment
340
+ view.gravity =
341
+ Gravity.CENTER_VERTICAL or
342
+ when (alignment) {
343
+ "center" -> Gravity.CENTER_HORIZONTAL
344
+ "end" -> Gravity.END
345
+ else -> Gravity.START
346
+ }
347
+ view.maxLines = style?.optInt("lines", 1)?.coerceIn(1, 3) ?: 1
348
+ view.ellipsize = TextUtils.TruncateAt.END
349
+ TextViewCompat.setLineHeight(
350
+ view,
351
+ dp(
352
+ style?.optDouble("lineHeight", defaultLineHeight.toDouble())?.roundToInt()
353
+ ?: defaultLineHeight
354
+ ),
355
+ )
356
+ }
357
+
358
+ // OneKey patch: opt-in Market text uses the same pixel rounding and line box as RN.
359
+ private fun applyMarketTextMetrics(view: TextView, style: JSONObject?) {
360
+ if (style == null || view.visibility != VISIBLE || view.text.isEmpty()) return
361
+ marketOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
362
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
363
+ if (style.has("fontSize")) {
364
+ val sourceSize = style.optDouble("fontSize").toFloat() * resources.displayMetrics.density
365
+ view.setTextSize(
366
+ TypedValue.COMPLEX_UNIT_PX,
367
+ kotlin.math.ceil(sourceSize.toDouble()).toFloat(),
368
+ )
369
+ }
370
+ // Legacy parity: keep marketText's subscript AbsoluteSizeSpan (ceil(0.6 x fontSize)).
371
+ val text = SpannableStringBuilder(view.text)
372
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
373
+ if (style.has("lineHeight")) {
374
+ val lineHeight = stylePx(style.optDouble("lineHeight"))
375
+ text.setSpan(
376
+ NativeListLineHeightSpan(lineHeight),
377
+ 0,
378
+ text.length,
379
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
380
+ )
381
+ view.setLineSpacing(0f, 1f)
382
+ }
383
+ view.text = text
384
+ val layout = JSONObject()
385
+ listOf("lines", "truncate", "verticalAlignment", "offsetY").forEach { key ->
386
+ if (style.has(key)) layout.put(key, style.get(key))
387
+ }
388
+ if (layout.length() > 0) {
389
+ applyStyledText(view, layout)
390
+ }
391
+ }
392
+
393
+ private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
394
+ if (segments == null || segments.length() == 0) return value
395
+ val result = SpannableStringBuilder()
396
+ for (index in 0 until segments.length()) {
397
+ val segment = segments.getJSONObject(index)
398
+ val start = result.length
399
+ result.append(segment.optString("text"))
400
+ if (segment.optString("style") == "subscript") {
401
+ result.setSpan(
402
+ AbsoluteSizeSpan(sp(kotlin.math.ceil(fontSize * 0.6f)).roundToInt(), true),
403
+ start,
404
+ result.length,
405
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
406
+ )
407
+ }
408
+ }
409
+ return result
410
+ }
411
+
412
+ private fun applyStyledText(view: TextView, style: JSONObject) {
413
+ if (style.has("fontSize")) {
414
+ view.setTextSize(
415
+ TypedValue.COMPLEX_UNIT_PX,
416
+ (style.optDouble("fontSize") * resources.displayMetrics.density).toFloat(),
417
+ )
418
+ val text = SpannableStringBuilder(view.text)
419
+ text.getSpans(0, text.length, AbsoluteSizeSpan::class.java).forEach(text::removeSpan)
420
+ view.text = text
421
+ }
422
+ style.optString("fontWeight").takeIf(String::isNotEmpty)?.let {
423
+ view.typeface = marketTypeface(it, "regular")
424
+ }
425
+ style.optString("color").takeIf(String::isNotEmpty)?.let {
426
+ val color = safeColor(it, view.currentTextColor)
427
+ view.setTextColor(color)
428
+ val text = SpannableStringBuilder(view.text)
429
+ text.getSpans(0, text.length, ForegroundColorSpan::class.java).forEach(text::removeSpan)
430
+ view.text = text
431
+ }
432
+ if (style.has("lineHeight")) {
433
+ val text = SpannableStringBuilder(view.text)
434
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
435
+ text.setSpan(
436
+ NativeListLineHeightSpan(stylePx(style.optDouble("lineHeight"))),
437
+ 0,
438
+ text.length,
439
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
440
+ )
441
+ view.setLineSpacing(0f, 1f)
442
+ view.text = text
443
+ }
444
+ if (style.has("lines")) {
445
+ view.maxLines = style.optInt("lines").coerceIn(1, 3)
446
+ view.ellipsize = TextUtils.TruncateAt.END
447
+ }
448
+ if (style.has("truncate") || style.has("lines")) {
449
+ view.setHorizontallyScrolling(view.maxLines == 1)
450
+ view.ellipsize =
451
+ if (style.optString("truncate", "tail") == "clip") null else TextUtils.TruncateAt.END
452
+ if (view.maxLines == 1) {
453
+ val text = SpannableStringBuilder(view.text)
454
+ Regex("\\r\\n|[\\r\\n]").findAll(text).toList().asReversed().forEach { match ->
455
+ text.replace(match.range.first, match.range.last + 1, " ")
456
+ }
457
+ view.text = text
458
+ }
459
+ }
460
+ if (style.has("verticalAlignment")) {
461
+ view.gravity =
462
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
463
+ when (style.optString("verticalAlignment")) {
464
+ "top" -> Gravity.TOP
465
+ "bottom" -> Gravity.BOTTOM
466
+ else -> Gravity.CENTER_VERTICAL
467
+ }
468
+ }
469
+ if (style.has("offsetY"))
470
+ (view as? NativeListTextView)?.opticalOffsetY =
471
+ (style.optDouble("offsetY") * resources.displayMetrics.density).toFloat()
472
+ style.optString("alignment").takeIf(String::isNotEmpty)?.let {
473
+ view.gravity =
474
+ (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) or
475
+ when (it) {
476
+ "center" -> Gravity.CENTER_HORIZONTAL
477
+ "end" -> Gravity.END
478
+ else -> Gravity.START
479
+ }
480
+ }
481
+ }
482
+
483
+ private fun bindMarket(item: NativeListItem, theme: JSONObject?) {
484
+ // Network badges extend beyond the leading image's frame.
485
+ clipChildren = false
486
+ val variant = item.json.optString("variant")
487
+ val style = item.json.optJSONObject("style")
488
+ val imageStyle = style?.optJSONObject("image")
489
+ val imageWidth =
490
+ imageStyle?.optDouble("width", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
491
+ ?: if (variant == "stock") 40 else 32
492
+ val imageHeight =
493
+ imageStyle?.optDouble("height", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
494
+ ?: if (variant == "stock") 40 else 32
495
+ val horizontalPadding =
496
+ style?.optDouble("horizontalPadding", if (variant == "perp") 16.0 else 20.0)?.roundToInt()
497
+ ?: if (variant == "perp") 16 else 20
498
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0)?.roundToInt() ?: 12
499
+ val leadingGap =
500
+ style?.optDouble("leadingGap", if (variant == "perp") 8.0 else 14.0)?.roundToInt()
501
+ ?: if (variant == "perp") 8 else 14
502
+ setPadding(
503
+ dp(horizontalPadding),
504
+ dp(verticalPadding),
505
+ dp(horizontalPadding),
506
+ dp(verticalPadding),
507
+ )
508
+
509
+ item.json.optJSONObject("leadingAction")?.let { action ->
510
+ leadingActionIcon.visibility = VISIBLE
511
+ leadingActionIcon.iconName = action.optString("name")
512
+ leadingActionIcon.glyphSizeDp = 24
513
+ leadingActionIcon.tintColor =
514
+ safeColor(action.optString("tintColor"), color(theme, "icon", "#0000009B"))
515
+ leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
516
+ leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
517
+ leadingActionIcon.setTag(
518
+ com.facebook.react.R.id.react_test_id,
519
+ action.optString("testID").takeIf(String::isNotEmpty),
520
+ )
521
+ leadingActionIcon.contentDescription = action.optString("accessibilityLabel")
522
+ leadingActionIcon.setOnClickListener {
523
+ emitAction(item, action.optString("actionKey"), null, leadingActionIcon, "leadingAction")
524
+ }
525
+ addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
526
+ }
527
+
528
+ val leading = JSONObject(item.json.getJSONObject("leading").toString())
529
+ imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
530
+ imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
531
+ leading.optJSONObject("image")?.put("contentFit", contentFit)
532
+ }
533
+ val shape =
534
+ imageStyle?.optString("shape", leading.optString("shape", "circle"))
535
+ ?: leading.optString("shape", "circle")
536
+ val cornerRadius =
537
+ imageStyle?.takeIf { it.has("cornerRadius") }?.optDouble("cornerRadius")?.toFloat()
538
+ ?: when (shape) {
539
+ "square" -> 0f
540
+ "rounded" -> 8f
541
+ else -> minOf(imageWidth, imageHeight) / 2f
542
+ }
543
+ val resolved = JSONObject(imageStyle?.toString() ?: "{}")
544
+ resolved.put("cornerRadius", cornerRadius.toDouble())
545
+ leadingFrame.bitmapBorderWidth = if (leading.optString("borderColor").isEmpty()) 0 else dp(1)
546
+ leadingFrame.roundFollowsVisualShape = true
547
+ leadingFrame.bind(leading, resolved, item.key, theme, false, true)
548
+ addView(
549
+ leadingFrame,
550
+ LayoutParams(
551
+ if (imageStyle?.has("width") == true) stylePx(imageStyle.optDouble("width"))
552
+ else dp(imageWidth),
553
+ if (imageStyle?.has("height") == true) stylePx(imageStyle.optDouble("height"))
554
+ else dp(imageHeight),
555
+ )
556
+ .apply {
557
+ marginEnd =
558
+ if (style?.has("leadingGap") == true) stylePx(style.optDouble("leadingGap"))
559
+ else dp(leadingGap)
560
+ },
561
+ )
562
+ // OneKey patch: retain the source gap without changing other templates.
563
+ // addView(mainColumn, weighted())
564
+ addView(
565
+ mainColumn,
566
+ weighted().apply {
567
+ marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
568
+ },
569
+ )
570
+ titleLine.packsChildrenAtStart = true
571
+ showText(
572
+ title,
573
+ item.json.optString("title"),
574
+ style?.optJSONObject("title")?.optInt("lines", 1) ?: 1,
575
+ )
576
+ applyMarketTextStyle(
577
+ title,
578
+ style?.optJSONObject("title"),
579
+ 16f,
580
+ 24,
581
+ "medium",
582
+ color(theme, "primaryText", "#202020"),
583
+ "start",
584
+ )
585
+ val badges = item.json.optJSONArray("badges")
586
+ val titleBadgeGap = style?.optDouble("titleBadgeGap", 4.0)?.roundToInt() ?: 4
587
+ if (badges != null) {
588
+ for (index in 0 until minOf(marketBadgeViews.size, badges.length())) {
589
+ val badge = badges.getJSONObject(index)
590
+ val badgeView = marketBadgeViews[index]
591
+ val badgeLabel = marketBadgeLabels[index]
592
+ val badgeStyle = badge.optJSONObject("style")
593
+ val hasGlyph = badge.optString("iconName") == "verified"
594
+ val remoteIcon = badge.optJSONObject("icon")
595
+ val hasIcon = hasGlyph || remoteIcon != null
596
+ val text = badge.optString("text")
597
+ val toneColor =
598
+ when (badge.optString("tone")) {
599
+ "success" -> color(theme, "positive", "#218358")
600
+ "danger" -> color(theme, "negative", "#CE2C31")
601
+ "info" -> color(theme, "info", "#0D74CE")
602
+ "warning" -> color(theme, "primaryText", "#202020")
603
+ else -> color(theme, "secondaryText", "#646464")
604
+ }
605
+ val foreground = safeColor(badge.optString("textColor"), toneColor)
606
+ badgeView.visibility = VISIBLE
607
+ val iconOnly = hasIcon && text.isEmpty()
608
+ // OneKey patch: Market callers can request the original badge padding.
609
+ val padding = badgeStyle?.optDouble("horizontalPadding", 5.0)?.roundToInt() ?: 5
610
+ val leftPadding =
611
+ if (badgeStyle?.has("horizontalPadding") == true) padding else if (hasIcon) 2 else 5
612
+ // badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly)
613
+ // 0 else 5), 0)
614
+ badgeView.setPadding(
615
+ dp(if (iconOnly) 0 else leftPadding),
616
+ 0,
617
+ dp(if (iconOnly) 0 else padding),
618
+ 0,
619
+ )
620
+ badgeView.background =
621
+ roundedFill(
622
+ safeColor(
623
+ badge.optString("backgroundColor"),
624
+ if (hasIcon && text.isEmpty()) Color.TRANSPARENT
625
+ else color(theme, "strongBackground", "#0000000F"),
626
+ ),
627
+ 4f,
628
+ )
629
+ .apply {
630
+ if (badgeStyle != null) this.cornerRadius = 4f * resources.displayMetrics.density
631
+ }
632
+ badgeLabel.text = text
633
+ // OneKey patch: match SizableText's tabular numerals only for explicit Market metrics.
634
+ badgeLabel.fontFeatureSettings = if (badgeStyle != null) "tnum" else null
635
+ badgeLabel.textSize = sp(badgeStyle?.optDouble("fontSize", 11.0)?.toFloat() ?: 11f)
636
+ badgeLabel.typeface =
637
+ marketTypeface(badgeStyle?.optString("fontWeight").orEmpty(), "medium")
638
+ if (badgeStyle?.has("lineHeight") == true) {
639
+ TextViewCompat.setLineHeight(
640
+ badgeLabel,
641
+ dp(badgeStyle.optDouble("lineHeight").roundToInt()),
642
+ )
643
+ }
644
+ badgeLabel.setTextColor(foreground)
645
+ badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
646
+ if (hasGlyph) {
647
+ marketBadgeGlyphs[index].apply {
648
+ iconName = "BadgeVerifiedSolid"
649
+ tintColor = foreground
650
+ visibility = VISIBLE
651
+ }
652
+ }
653
+ remoteIcon?.let { icon ->
654
+ marketBadgeImages[index].visibility = VISIBLE
655
+ marketBadgeImages[index].outlineProvider = circleOutlineProvider
656
+ marketBadgeImages[index].clipToOutline = true
657
+ badgeSlots[index].bind(icon, item.key + ":badge:" + index, "generic")
658
+ }
659
+ val actionKey = badge.optString("actionKey")
660
+ badgeView.isClickable = actionKey.isNotEmpty()
661
+ if (actionKey.isNotEmpty()) {
662
+ badgeView.setOnClickListener {
663
+ emitAction(item, actionKey, null, badgeView, "marketBadge", index)
664
+ }
665
+ }
666
+ badgeView.contentDescription = badge.optString("accessibilityLabel", text)
667
+ titleLine.addView(
668
+ badgeView,
669
+ // OneKey patch: preserve 18dp unless the Market row opts into source metrics.
670
+ // LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap)
671
+ // },
672
+ LayoutParams(
673
+ LayoutParams.WRAP_CONTENT,
674
+ dp(badgeStyle?.optDouble("height", 18.0)?.roundToInt() ?: 18),
675
+ )
676
+ .apply { marginStart = dp(titleBadgeGap) },
677
+ )
678
+ }
679
+ }
680
+ subtitle.layoutParams =
681
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
682
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
683
+ }
684
+ val subtitleText = item.json.optString("subtitle")
685
+ val subtitleSegments = item.json.optJSONArray("subtitleSegments")
686
+ if (subtitleText.isNotEmpty() || (subtitleSegments?.length() ?: 0) > 0) {
687
+ subtitle.visibility = VISIBLE
688
+ applyMarketTextStyle(
689
+ subtitle,
690
+ style?.optJSONObject("subtitle"),
691
+ 14f,
692
+ 20,
693
+ "regular",
694
+ color(theme, "secondaryText", "#646464"),
695
+ "start",
696
+ )
697
+ subtitle.text =
698
+ marketText(
699
+ subtitleText,
700
+ subtitleSegments,
701
+ style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
702
+ )
703
+ }
704
+ // OneKey patch: preserve independent name metrics, truncation, and volume.
705
+ val subtitlePrefix = item.json.optJSONObject("subtitlePrefix")
706
+ val subtitlePadding = style?.optDouble("subtitleTrailingPadding", 0.0)?.roundToInt() ?: 0
707
+ if (subtitlePrefix != null || subtitlePadding > 0) {
708
+ mainColumn.removeView(subtitle)
709
+ mainColumn.removeView(tertiary)
710
+ marketSubtitleLine.orientation = HORIZONTAL
711
+ marketSubtitleLine.gravity = Gravity.CENTER_VERTICAL
712
+ marketSubtitleLine.packsChildrenAtStart = true
713
+ marketSubtitleLine.leadingTextMaxWidth =
714
+ subtitlePrefix
715
+ ?.takeIf { it.has("maxWidth") }
716
+ ?.optDouble("maxWidth")
717
+ ?.roundToInt()
718
+ ?.let(::dp) ?: Int.MAX_VALUE
719
+ marketSubtitleLine.setPadding(0, 0, dp(subtitlePadding), 0)
720
+ showText(tertiary, subtitlePrefix?.optString("text") ?: "", 1)
721
+ applyMarketTextStyle(
722
+ tertiary,
723
+ subtitlePrefix?.optJSONObject("style"),
724
+ 12f,
725
+ 16,
726
+ "regular",
727
+ color(theme, "secondaryText", "#646464"),
728
+ "start",
729
+ )
730
+ marketSubtitleLine.addView(tertiary, wrap())
731
+ marketSubtitleLine.addView(
732
+ subtitle,
733
+ wrap().apply {
734
+ if (tertiary.visibility == VISIBLE && subtitle.visibility == VISIBLE) {
735
+ marginStart = dp(subtitlePrefix?.optDouble("gap", 4.0)?.roundToInt() ?: 4)
736
+ }
737
+ },
738
+ )
739
+ mainColumn.addView(
740
+ marketSubtitleLine,
741
+ 1,
742
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
743
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
744
+ },
745
+ )
746
+ marketSubtitleLine.visibility =
747
+ if (tertiary.visibility == VISIBLE || subtitle.visibility == VISIBLE) VISIBLE else GONE
748
+ }
749
+ trailingColumn.orientation = HORIZONTAL
750
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
751
+ addView(trailingColumn, wrap())
752
+ bindQuote(item, theme)
753
+ val nextImageSignature =
754
+ nativeListCanonical(
755
+ JSONObject()
756
+ .put("key", item.key)
757
+ .put("leading", leading)
758
+ .put("fit", resolved.opt("contentFit"))
759
+ )
760
+ item.json
761
+ .optJSONObject("diagnostics")
762
+ ?.optString("imageBindActionKey")
763
+ ?.takeIf(String::isNotEmpty)
764
+ ?.takeIf { nextImageSignature != imageSignature }
765
+ ?.takeIf {
766
+ leading.optJSONObject("image") != null || leading.optJSONObject("networkImage") != null
767
+ }
768
+ ?.let { actionKey -> onAction?.invoke(item, actionKey, null, null) }
769
+ imageSignature = nextImageSignature
770
+ }
771
+
772
+ private fun bindQuote(item: NativeListItem, theme: JSONObject?) {
773
+ val style = item.json.optJSONObject("style")
774
+ val priceStyle = style?.optJSONObject("price")
775
+ val price = trailingViews[0]
776
+ price.isClickable = false
777
+ price.isLongClickable = false
778
+ price.visibility = VISIBLE
779
+ price.maxWidth = dp(112)
780
+ applyMarketTextStyle(
781
+ price,
782
+ priceStyle,
783
+ 16f,
784
+ 24,
785
+ "medium",
786
+ color(theme, "primaryText", "#202020"),
787
+ "end",
788
+ )
789
+ price.text =
790
+ marketText(
791
+ item.json.optString("price"),
792
+ item.json.optJSONArray("priceSegments"),
793
+ priceStyle?.optDouble("fontSize", 16.0)?.toFloat() ?: 16f,
794
+ )
795
+ val trailingGap = style?.optDouble("trailingGap", 8.0)?.roundToInt() ?: 8
796
+ price.layoutParams =
797
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
798
+ marginEnd = dp(trailingGap)
799
+ }
800
+
801
+ val changeData = item.json.getJSONObject("change")
802
+ val changeStyle = style?.optJSONObject("change")
803
+ val change = trailingViews[1]
804
+ change.isClickable = false
805
+ change.isLongClickable = false
806
+ val toneColor =
807
+ when (changeData.optString("tone")) {
808
+ "positive" -> color(theme, "positive", "#218358")
809
+ "negative" -> color(theme, "negative", "#CE2C31")
810
+ else -> color(theme, "secondaryText", "#8D8D8D")
811
+ }
812
+ val textColor =
813
+ safeColor(changeData.optString("textColor"), color(theme, "inverseText", "#FFFFFF"))
814
+ change.visibility = VISIBLE
815
+ applyMarketTextStyle(change, changeStyle, 14f, 20, "medium", textColor, "center")
816
+ change.text =
817
+ marketText(
818
+ changeData.optString("text"),
819
+ changeData.optJSONArray("textSegments"),
820
+ changeStyle?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
821
+ )
822
+ change.background =
823
+ roundedFill(
824
+ safeColor(changeData.optString("backgroundColor"), toneColor),
825
+ style?.optDouble("changeCornerRadius", 8.0)?.toFloat() ?: 8f,
826
+ )
827
+ change.layoutParams =
828
+ LayoutParams(
829
+ dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
830
+ dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
831
+ )
832
+ applyMarketTextMetrics(title, style?.optJSONObject("title"))
833
+ applyMarketTextMetrics(subtitle, style?.optJSONObject("subtitle"))
834
+ applyMarketTextMetrics(
835
+ tertiary,
836
+ item.json.optJSONObject("subtitlePrefix")?.optJSONObject("style"),
837
+ )
838
+ applyMarketTextMetrics(price, priceStyle)
839
+ applyMarketTextMetrics(change, changeStyle)
840
+ val badges = item.json.optJSONArray("badges")
841
+ marketBadgeLabels.forEachIndexed { index, label ->
842
+ val badge = badges?.optJSONObject(index)
843
+ val badgeStyle = badge?.optJSONObject("style")
844
+ applyMarketTextMetrics(label, badgeStyle)
845
+ if (
846
+ badge != null &&
847
+ label.visibility == VISIBLE &&
848
+ badgeStyle?.has("horizontalPadding") == true &&
849
+ badge.optJSONObject("icon") == null &&
850
+ badge.optString("iconName").isEmpty()
851
+ ) {
852
+ // OneKey patch: match RN's intrinsic text width and one rounded padding pair.
853
+ label.layoutParams =
854
+ wrap().apply { width = label.paint.measureText(label.text.toString()).roundToInt() }
855
+ val padding = badgeStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
856
+ marketBadgeViews[index].setPadding(
857
+ kotlin.math.floor(padding).toInt(),
858
+ 0,
859
+ (padding * 2).roundToInt() - kotlin.math.floor(padding).toInt(),
860
+ 0,
861
+ )
862
+ }
863
+ }
864
+ contentDescription =
865
+ item.json.optString(
866
+ "accessibilityLabel",
867
+ listOf(
868
+ item.json.optString("title"),
869
+ item.json.optString("subtitle"),
870
+ item.json.optString("price"),
871
+ changeData.optString("text"),
872
+ )
873
+ .filter(String::isNotEmpty)
874
+ .joinToString(", "),
875
+ )
876
+ }
877
+
878
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
879
+ val marketItem = (tag as? NativeListItem)?.takeIf { it.type == "market" }
880
+ val marketStyle = marketItem?.json?.optJSONObject("style")
881
+ if (marketStyle?.has("horizontalPadding") == true) {
882
+ // OneKey patch: Yoga rounds the two absolute edges, not both padding values.
883
+ val sourcePadding =
884
+ marketStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
885
+ val width = MeasureSpec.getSize(widthMeasureSpec)
886
+ val rowHeight = marketItem.styledHeight?.let(::stylePx) ?: modelHeight(marketItem, "linear")
887
+ val sourceVerticalPadding =
888
+ marketStyle
889
+ .takeIf { it.has("verticalPadding") && rowHeight != null }
890
+ ?.optDouble("verticalPadding")
891
+ ?.times(resources.displayMetrics.density)
892
+ setPadding(
893
+ sourcePadding.roundToInt(),
894
+ sourceVerticalPadding?.roundToInt() ?: paddingTop,
895
+ width - (width - sourcePadding).roundToInt(),
896
+ if (sourceVerticalPadding != null && rowHeight != null)
897
+ rowHeight - (rowHeight - sourceVerticalPadding).roundToInt()
898
+ else paddingBottom,
899
+ )
900
+ }
901
+
902
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
903
+ }
904
+
905
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
906
+ super.onLayout(changed, left, top, right, bottom)
907
+ val item = tag as? NativeListItem ?: return
908
+ val hasContainerAlignment =
909
+ item.json
910
+ .optJSONObject("style")
911
+ ?.optJSONObject("container")
912
+ ?.has("contentVerticalAlignment") == true
913
+ if (item.type == "market" && item.json.optJSONObject("style") != null) {
914
+ // OneKey patch: preserve Yoga's half-pixel centering for Market columns and badges.
915
+ for (column in listOf(leadingFrame, mainColumn, trailingColumn)) {
916
+ if (!hasContainerAlignment && column.parent === this && column.visibility != GONE) {
917
+ column.offsetTopAndBottom((height - column.height + 1) / 2 - column.top)
918
+ }
919
+ }
920
+ // OneKey patch: token avatars stay centered in the row when text rounding adds a pixel.
921
+ if (
922
+ !hasContainerAlignment &&
923
+ item.json.optString("variant") != "token" &&
924
+ leadingFrame.parent === this &&
925
+ mainColumn.parent === this
926
+ ) {
927
+ leadingFrame.offsetTopAndBottom(
928
+ mainColumn.top + (mainColumn.height - leadingFrame.height + 1) / 2 - leadingFrame.top
929
+ )
930
+ }
931
+ // OneKey patch: preserve Yoga's absolute-edge rounding for the Market network badge.
932
+ if (
933
+ item.json.optJSONObject("leading")?.optJSONObject("networkImage") != null &&
934
+ item.json.optString("variant") == "token"
935
+ ) {
936
+ val recycler = parent as? RecyclerView
937
+ val adapter = recycler?.adapter as? NativeListAdapter
938
+ val position = recycler?.getChildAdapterPosition(this) ?: RecyclerView.NO_POSITION
939
+ if (
940
+ recycler != null &&
941
+ adapter != null &&
942
+ position != RecyclerView.NO_POSITION &&
943
+ adapter.itemAt(position)?.key == item.key
944
+ ) {
945
+ val density = resources.displayMetrics.density.toDouble()
946
+ val rowTop = adapter.marketSourceEdgePx(position)
947
+ val rowHeight = adapter.marketSourceEdgePx(position + 1) - rowTop
948
+ val imageHeight =
949
+ item.json.optJSONObject("style")?.optJSONObject("image")?.optDouble("height", 32.0)
950
+ ?: 32.0
951
+ val contentTop = (recycler.paddingTop / density).roundToInt() * density
952
+ val badgeTop =
953
+ contentTop +
954
+ rowTop +
955
+ (rowHeight - imageHeight * density) / 2 +
956
+ (imageHeight - 16) * density
957
+ val badgeHeight = (badgeTop + 20 * density).roundToInt() - badgeTop.roundToInt()
958
+ val top = ((imageHeight - 16) * density).roundToInt()
959
+ leadingFrame.layoutNetworkBackdrop(top, badgeHeight)
960
+ }
961
+ }
962
+ for (badge in marketBadgeViews) {
963
+ if (badge.parent === titleLine && badge.visibility != GONE) {
964
+ badge.offsetTopAndBottom((titleLine.height - badge.height + 1) / 2 - badge.top)
965
+ }
966
+ }
967
+ // OneKey patch: preserve fractional text advances and gaps until the final edge.
968
+ val style = item.json.optJSONObject("style")
969
+ for ((line, gap) in
970
+ listOf(
971
+ titleLine to style.optDouble("titleBadgeGap", 4.0),
972
+ marketSubtitleLine to
973
+ (item.json.optJSONObject("subtitlePrefix")?.optDouble("gap", 4.0) ?: 0.0),
974
+ )) {
975
+ var cursor = 0f
976
+ var hasPrevious = false
977
+ for (index in 0 until line.childCount) {
978
+ val child = line.getChildAt(index)
979
+ if (child.visibility == GONE) continue
980
+ if (hasPrevious) {
981
+ cursor += (gap * resources.displayMetrics.density).toFloat()
982
+ child.offsetLeftAndRight(cursor.roundToInt() - child.left)
983
+ }
984
+ val advance =
985
+ if (index == 0 && child is TextView) {
986
+ val textWidth =
987
+ if (line === titleLine) child.paint.measureText(child.text.toString())
988
+ else child.layout?.getLineWidth(0)
989
+ textWidth?.coerceAtMost(child.width.toFloat()) ?: child.width.toFloat()
990
+ } else child.width.toFloat()
991
+ cursor = if (hasPrevious) cursor + advance else child.left + advance
992
+ hasPrevious = true
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }