@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
@@ -1,4538 +0,0 @@
1
- package com.margelo.nitro.nativelist
2
-
3
- import android.animation.Animator
4
- import android.animation.AnimatorListenerAdapter
5
- import android.animation.TimeInterpolator
6
- import android.animation.ValueAnimator
7
- import android.graphics.Color
8
- import android.graphics.Canvas
9
- import android.graphics.LinearGradient
10
- import android.graphics.Matrix
11
- import android.graphics.Outline
12
- import android.graphics.Paint
13
- import android.graphics.Path
14
- import android.graphics.RectF
15
- import android.graphics.Shader
16
- import android.graphics.Typeface
17
- import android.graphics.drawable.Drawable
18
- import android.graphics.drawable.GradientDrawable
19
- import android.graphics.drawable.StateListDrawable
20
- import android.os.Handler
21
- import android.os.Looper
22
- import android.text.Spannable
23
- import android.text.SpannableStringBuilder
24
- import android.text.TextUtils
25
- import android.text.style.AbsoluteSizeSpan
26
- import android.text.style.BackgroundColorSpan
27
- import android.text.style.ForegroundColorSpan
28
- import android.util.TypedValue
29
- import android.view.Gravity
30
- import android.view.MotionEvent
31
- import android.view.View
32
- import android.view.ViewGroup
33
- import android.view.ViewOutlineProvider
34
- import android.view.animation.LinearInterpolator
35
- import android.widget.FrameLayout
36
- import android.widget.LinearLayout
37
- import android.widget.ProgressBar
38
- import android.widget.TextView
39
- import com.facebook.react.uimanager.ThemedReactContext
40
- // OneKey patch: selector checkboxes share the source React Native border/background renderer.
41
- import com.facebook.react.uimanager.BackgroundStyleApplicator
42
- import com.facebook.react.uimanager.LengthPercentage
43
- import com.facebook.react.uimanager.LengthPercentageType
44
- import com.facebook.react.uimanager.style.BorderRadiusProp
45
- import com.facebook.react.uimanager.style.LogicalEdge
46
- import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
47
- import androidx.core.graphics.PathParser
48
- import androidx.core.widget.TextViewCompat
49
- import androidx.recyclerview.widget.RecyclerView
50
- import org.json.JSONArray
51
- import org.json.JSONObject
52
- import kotlin.math.roundToInt
53
-
54
- // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
55
- private class NativeListMarketSkeleton(context: android.content.Context, backgroundColor: Int) : View(context) {
56
- private val marks = Array(5) { RectF() }
57
- private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
58
- private val matrix = Matrix()
59
- private val shaders = arrayOfNulls<LinearGradient>(5)
60
- private val dark = Color.red(backgroundColor) * 0.299 + Color.green(backgroundColor) * 0.587 + Color.blue(backgroundColor) * 0.114 < 128
61
- private val colors = intArrayOf(
62
- Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
63
- Color.parseColor(if (dark) "#333333" else "#CDCDCD"),
64
- Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
65
- )
66
- private var phase = 0f
67
- private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
68
- duration = 3000
69
- repeatCount = ValueAnimator.INFINITE
70
- interpolator = LinearInterpolator()
71
- addUpdateListener { phase = it.animatedValue as Float; invalidate() }
72
- }
73
-
74
- private fun dp(value: Float) = NativeListScale.dp(resources, value)
75
-
76
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
77
- super.onSizeChanged(w, h, oldw, oldh)
78
- marks[0].set(0f, 0f, dp(32f), dp(32f))
79
- marks[1].set(dp(44f), 0f, dp(124f), dp(16f))
80
- marks[2].set(dp(44f), dp(20f), dp(104f), dp(32f))
81
- marks[3].set(w - dp(168f), dp(7f), w - dp(88f), dp(25f))
82
- marks[4].set(w - dp(80f), dp(7f), w.toFloat(), dp(25f))
83
- marks.forEachIndexed { index, mark ->
84
- shaders[index] = LinearGradient(0f, 0f, mark.width(), 0f, colors, floatArrayOf(0f, 0.5f, 1f), Shader.TileMode.CLAMP)
85
- }
86
- }
87
-
88
- override fun onDraw(canvas: Canvas) {
89
- super.onDraw(canvas)
90
- marks.forEachIndexed { index, mark ->
91
- matrix.setTranslate(mark.left - mark.width() + phase * mark.width() * 3f, 0f)
92
- shaders[index]?.setLocalMatrix(matrix)
93
- paint.shader = shaders[index]
94
- val radius = dp(if (index == 0) 16f else 8f)
95
- canvas.drawRoundRect(mark, radius, radius, paint)
96
- }
97
- }
98
-
99
- override fun onAttachedToWindow() {
100
- super.onAttachedToWindow()
101
- if (windowVisibility == VISIBLE) animator.start()
102
- }
103
-
104
- override fun onDetachedFromWindow() {
105
- animator.cancel()
106
- super.onDetachedFromWindow()
107
- }
108
-
109
- override fun onWindowVisibilityChanged(visibility: Int) {
110
- super.onWindowVisibilityChanged(visibility)
111
- if (visibility == VISIBLE && isAttachedToWindow) {
112
- if (!animator.isStarted) animator.start()
113
- } else animator.cancel()
114
- }
115
- }
116
-
117
- internal data class NativeListActionOrigin(
118
- val sourceView: View,
119
- val ownerRowView: NativeListRowView,
120
- val bindingEpoch: Long,
121
- val source: String,
122
- val slot: Int? = null,
123
- val anchorInsetPixels: Int = 0,
124
- val windowPointPixels: android.graphics.PointF? = null,
125
- )
126
-
127
- /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
128
- internal fun parseNativeListColor(value: String): Int {
129
- val normalized = value.trim()
130
- val androidValue = if (
131
- normalized.length == 9 && normalized[0] == '#' &&
132
- normalized.drop(1).all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
133
- ) {
134
- "#${normalized.takeLast(2)}${normalized.substring(1, 7)}"
135
- } else {
136
- normalized
137
- }
138
- return Color.parseColor(androidValue)
139
- }
140
-
141
- internal object NativeListScale {
142
- private const val STANDARD_DP_WIDTH_THRESHOLD = 400
143
-
144
- fun factor(resources: android.content.res.Resources): Float {
145
- val widthDp = resources.configuration.screenWidthDp
146
- return if (widthDp in 1 until STANDARD_DP_WIDTH_THRESHOLD) 0.9f else 1f
147
- }
148
-
149
- fun dp(resources: android.content.res.Resources, value: Int): Int =
150
- (value * factor(resources) * resources.displayMetrics.density).roundToInt()
151
-
152
- fun dp(resources: android.content.res.Resources, value: Float): Float =
153
- (value * factor(resources) * resources.displayMetrics.density).roundToInt().toFloat()
154
-
155
- fun font(resources: android.content.res.Resources, value: Float): Float =
156
- if (factor(resources) == 1f) value else (value * factor(resources)).roundToInt().toFloat()
157
- }
158
-
159
- internal object NativeListFonts {
160
- private val cache = mutableMapOf<String, Typeface>()
161
-
162
- private fun load(context: android.content.Context, weight: String, fallback: String): Typeface =
163
- cache.getOrPut(weight) {
164
- try {
165
- Typeface.createFromAsset(context.assets, "fonts/Roobert-$weight.ttf")
166
- } catch (_: RuntimeException) {
167
- Typeface.create(fallback, Typeface.NORMAL)
168
- }
169
- }
170
-
171
- fun regular(context: android.content.Context) = load(context, "Regular", "sans-serif")
172
- fun medium(context: android.content.Context) = load(context, "Medium", "sans-serif-medium")
173
- fun semibold(context: android.content.Context) = load(context, "SemiBold", "sans-serif-medium")
174
- fun bold(context: android.content.Context) = load(context, "Bold", "sans-serif")
175
- }
176
-
177
- internal data class NativeSelectionTarget(val scope: String, val key: String?)
178
-
179
- // OneKey patch: match React Native's CustomLineHeightSpan for first/last line bounds.
180
- private class SelectorLineHeightSpan(private val lineHeight: Int) : android.text.style.LineHeightSpan {
181
- override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, fm: Paint.FontMetricsInt) {
182
- val leading = lineHeight - (fm.descent - fm.ascent)
183
- fm.ascent -= kotlin.math.ceil(leading / 2.0).toInt()
184
- fm.descent += kotlin.math.floor(leading / 2.0).toInt()
185
- if (start == 0) fm.top = fm.ascent
186
- if (end == text.length) fm.bottom = fm.descent
187
- }
188
- }
189
-
190
- private class DottedUnderlineTextView(context: android.content.Context) : TextView(context) {
191
- var useSourceScale = false
192
- private fun scaledDp(value: Float) = if (useSourceScale) value * resources.displayMetrics.density else NativeListScale.dp(resources, value)
193
- var showsDottedUnderline = false
194
- var dottedUnderlineColor = Color.TRANSPARENT
195
- private val dottedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
196
-
197
- override fun onDraw(canvas: Canvas) {
198
- super.onDraw(canvas)
199
- if (!showsDottedUnderline || text.isEmpty()) return
200
- dottedPaint.color = dottedUnderlineColor
201
- val radius = scaledDp(0.75f)
202
- val spacing = scaledDp(4f)
203
- val lineWidth = paint.measureText(text.toString()).coerceAtMost(width.toFloat())
204
- val y = height - radius
205
- var x = scaledDp(1f)
206
- while (x <= lineWidth - radius) {
207
- canvas.drawCircle(x, y, radius, dottedPaint)
208
- x += spacing
209
- }
210
- }
211
- }
212
-
213
- private class PackedTitleLineLayout(context: android.content.Context) : LinearLayout(context) {
214
- var packsChildrenAtStart = false
215
- // OneKey patch: optional cap for the Market subtitle's localized name.
216
- var leadingTextMaxWidth = Int.MAX_VALUE
217
-
218
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
219
- if (!packsChildrenAtStart || childCount < 2) {
220
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
221
- return
222
- }
223
-
224
- val title = getChildAt(0)
225
- val widthMode = MeasureSpec.getMode(widthMeasureSpec)
226
- val widthSize = MeasureSpec.getSize(widthMeasureSpec)
227
- var accessoryWidth = 0
228
- for (index in 1 until childCount) {
229
- val child = getChildAt(index)
230
- if (child.visibility == GONE) continue
231
- measureChildWithMargins(
232
- child,
233
- widthMeasureSpec,
234
- paddingLeft + paddingRight + accessoryWidth,
235
- heightMeasureSpec,
236
- paddingTop + paddingBottom,
237
- )
238
- val margins = child.layoutParams as MarginLayoutParams
239
- accessoryWidth += child.measuredWidth + margins.leftMargin + margins.rightMargin
240
- }
241
- val titleMargins = title.layoutParams as MarginLayoutParams
242
- if (widthMode != MeasureSpec.UNSPECIFIED) {
243
- (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - accessoryWidth -
244
- titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0).coerceAtMost(leadingTextMaxWidth)
245
- }
246
- (title.layoutParams as LayoutParams).apply {
247
- width = LayoutParams.WRAP_CONTENT
248
- weight = 0f
249
- }
250
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
251
- if (widthMode != MeasureSpec.UNSPECIFIED) {
252
- setMeasuredDimension(widthSize, measuredHeight)
253
- }
254
- }
255
- }
256
-
257
- // OneKey patch: fit subtitle segments at intrinsic width, shrinking text only when necessary.
258
- private class SelectorSubtitleLayout(context: android.content.Context) : LinearLayout(context) {
259
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
260
- val available = MeasureSpec.getSize(widthMeasureSpec)
261
- val labels = mutableListOf<Pair<TextView, Int>>()
262
- var fixedWidth = 0
263
- for (index in 0 until childCount) {
264
- val child = getChildAt(index)
265
- val params = child.layoutParams as LayoutParams
266
- if (child is TextView) {
267
- child.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec)
268
- labels.add(child to child.measuredWidth)
269
- } else fixedWidth += params.width + params.leftMargin + params.rightMargin
270
- }
271
- val desired = labels.sumOf { it.second }
272
- val textWidth = (available - fixedWidth).coerceAtLeast(0)
273
- labels.forEach { (label, width) ->
274
- (label.layoutParams as LayoutParams).width = if (desired <= textWidth) width else (width.toLong() * textWidth / desired.coerceAtLeast(1)).toInt()
275
- }
276
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
277
- }
278
- }
279
-
280
- private class NativeListTableColumnView(context: android.content.Context) : LinearLayout(context) {
281
- private val primaryLine = LinearLayout(context)
282
- private val primary = TextView(context)
283
- private val badges = LinearLayout(context)
284
- private val secondaryLine = LinearLayout(context)
285
- private val secondaryLeading = TextView(context)
286
- private val secondary = TextView(context)
287
-
288
- init {
289
- orientation = VERTICAL
290
- primaryLine.orientation = HORIZONTAL
291
- primaryLine.gravity = Gravity.CENTER_VERTICAL
292
- badges.orientation = HORIZONTAL
293
- badges.gravity = Gravity.CENTER_VERTICAL
294
- primary.includeFontPadding = false
295
- primary.fontFeatureSettings = "tnum"
296
- primary.maxLines = 1
297
- primary.ellipsize = TextUtils.TruncateAt.END
298
- primary.textSize = sp(14f)
299
- primary.typeface = NativeListFonts.medium(context)
300
- TextViewCompat.setLineHeight(primary, dp(20))
301
- secondaryLine.orientation = HORIZONTAL
302
- secondaryLine.gravity = Gravity.CENTER_VERTICAL
303
- listOf(secondaryLeading, secondary).forEach { label ->
304
- label.includeFontPadding = false
305
- label.fontFeatureSettings = "tnum"
306
- label.maxLines = 1
307
- label.ellipsize = TextUtils.TruncateAt.END
308
- label.textSize = sp(12f)
309
- label.typeface = NativeListFonts.regular(context)
310
- TextViewCompat.setLineHeight(label, dp(16))
311
- }
312
- secondaryLeading.maxWidth = dp(120)
313
- secondaryLine.addView(secondaryLeading, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)))
314
- secondaryLine.addView(secondary, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
315
- marginStart = dp(4)
316
- })
317
- primaryLine.addView(primary, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
318
- primaryLine.addView(badges, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
319
- marginStart = dp(6)
320
- })
321
- addView(primaryLine, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
322
- addView(secondaryLine, LayoutParams(LayoutParams.MATCH_PARENT, dp(16)).apply {
323
- topMargin = dp(4)
324
- })
325
- }
326
-
327
- fun reset() {
328
- primary.text = ""
329
- secondaryLeading.text = ""
330
- secondary.text = ""
331
- secondaryLeading.visibility = GONE
332
- secondary.visibility = GONE
333
- secondaryLine.visibility = GONE
334
- badges.removeAllViews()
335
- badges.visibility = GONE
336
- }
337
-
338
- fun bind(
339
- column: JSONObject,
340
- rowBadges: JSONArray?,
341
- primaryColor: Int,
342
- secondaryColor: Int,
343
- infoColor: Int,
344
- ) {
345
- reset()
346
- gravity = when (column.optString("alignment", "start")) {
347
- "center" -> Gravity.CENTER_HORIZONTAL
348
- "end" -> Gravity.END
349
- else -> Gravity.START
350
- }
351
- primary.gravity = gravity
352
- secondaryLeading.gravity = gravity
353
- secondary.gravity = gravity
354
- primary.text = column.optString("text")
355
- primary.setTextColor(primaryColor)
356
- if (rowBadges != null && rowBadges.length() > 0) {
357
- badges.visibility = VISIBLE
358
- for (index in 0 until minOf(2, rowBadges.length())) {
359
- val badge = TextView(context).apply {
360
- includeFontPadding = false
361
- fontFeatureSettings = "tnum"
362
- gravity = Gravity.CENTER
363
- text = rowBadges.getJSONObject(index).optString("text")
364
- textSize = sp(10f)
365
- typeface = NativeListFonts.regular(context)
366
- setTextColor(infoColor)
367
- setPadding(dp(6), 0, dp(6), 0)
368
- background = GradientDrawable().apply {
369
- setColor(parseNativeListColor("#008FF519"))
370
- cornerRadius = dp(4).toFloat()
371
- }
372
- }
373
- badges.addView(badge, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
374
- if (index > 0) marginStart = dp(4)
375
- })
376
- }
377
- }
378
- val secondaryLeadingText = column.optString("secondaryLeadingText")
379
- if (secondaryLeadingText.isNotEmpty()) {
380
- secondaryLeading.text = secondaryLeadingText
381
- secondaryLeading.setTextColor(secondaryColor)
382
- secondaryLeading.visibility = VISIBLE
383
- secondaryLine.visibility = VISIBLE
384
- }
385
- val secondaryText = column.optString("secondaryText")
386
- if (secondaryText.isNotEmpty()) {
387
- secondary.text = secondaryText
388
- secondary.setTextColor(secondaryColor)
389
- secondary.visibility = VISIBLE
390
- secondaryLine.visibility = VISIBLE
391
- }
392
- }
393
-
394
- private fun dp(value: Int): Int = NativeListScale.dp(resources, value)
395
- private fun sp(value: Float): Float = NativeListScale.font(resources, value)
396
- }
397
-
398
- private object NativeListSourceFallbackState {
399
- private const val CACHE_LIMIT = 128
400
- private val sources = LinkedHashMap<String, Unit>(CACHE_LIMIT, 0.75f, true)
401
-
402
- fun has(key: String): Boolean = synchronized(sources) {
403
- sources[key] != null
404
- }
405
-
406
- fun remember(key: String) {
407
- synchronized(sources) {
408
- sources[key] = Unit
409
- while (sources.size > CACHE_LIMIT) {
410
- sources.remove(sources.entries.first().key)
411
- }
412
- }
413
- }
414
-
415
- fun forget(key: String) {
416
- synchronized(sources) {
417
- sources.remove(key)
418
- }
419
- }
420
- }
421
-
422
- internal class NativeListRowView(
423
- private val reactContext: ThemedReactContext,
424
- ) : LinearLayout(reactContext) {
425
- private var marketLeadingUsesSourceClip = false
426
- private val leadingFrame = object : FrameLayout(context) {
427
- override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
428
- if (!marketLeadingUsesSourceClip || child !== leadingImages[0]) return super.drawChild(canvas, child, drawingTime)
429
- // OneKey patch: clip only the Market avatar to the source border's padding box.
430
- val saved = canvas.save()
431
- BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
432
- return try { super.drawChild(canvas, child, drawingTime) } finally { canvas.restoreToCount(saved) }
433
- }
434
- }
435
- // OneKey patch: reset selector fragments and corner decorations on every bind.
436
- private val selectorViews = mutableListOf<View>()
437
- private var selectorUsesSourceScale = false
438
- private val selectorAccessibilityDelegate = object : View.AccessibilityDelegate() {
439
- override fun onInitializeAccessibilityNodeInfo(host: View, info: android.view.accessibility.AccessibilityNodeInfo) {
440
- super.onInitializeAccessibilityNodeInfo(host, info)
441
- info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
442
- }
443
- }
444
- private val selectorOriginalFontFeatures = mutableMapOf<TextView, String?>()
445
- private val selectorOriginalPaintFlags = mutableMapOf<TextView, Int>()
446
- private val marketOriginalPaintFlags = mutableMapOf<TextView, Int>()
447
- private val selectorLineHeights = mutableMapOf<TextView, Int>()
448
- private val selectorFontSizes = mutableMapOf<TextView, Float>()
449
- private val selectorImages = mutableListOf<OneKeyImageReusableView>()
450
- private var selectorHeight: Int? = null
451
- private val leadingImages = List(3) { OneKeyImageReusableView(reactContext) }
452
- private val leadingOverlayBackground = View(context)
453
- private val leadingCornerIconFrame = FrameLayout(context)
454
- private val leadingCornerIcon = OneKeyIconView(context)
455
- private val leadingFallback = TextView(context)
456
- private val leadingIcon = OneKeyIconView(context)
457
- private val favoriteIcon = OneKeyIconView(context)
458
- private val headerTitleIcon = OneKeyIconView(context)
459
- private val headerValueIcon = OneKeyIconView(context)
460
- private val leadingActionIcon = OneKeyIconView(context)
461
- private val secondaryImage = OneKeyImageReusableView(reactContext)
462
- private val mediaNetworkImage = OneKeyImageReusableView(reactContext)
463
- private val metricVisualImages = List(5) { OneKeyImageReusableView(reactContext) }
464
- private val mainColumn = LinearLayout(context)
465
- private val mediaMetadataRow = LinearLayout(context)
466
- private val titleLine = PackedTitleLineLayout(context)
467
- private val title = DottedUnderlineTextView(context)
468
- private val subtitle = TextView(context)
469
- // OneKey patch: the first text shrinks while the volume retains its width.
470
- private val marketSubtitleLine = PackedTitleLineLayout(context)
471
- private val tertiary = TextView(context)
472
- private val status = TextView(context)
473
- private val metricSubtitle = TextView(context)
474
- private val badgeLine = TextView(context)
475
- private val marketBadgeViews = List(3) { LinearLayout(context) }
476
- private val marketBadgeLabels = List(3) { TextView(context) }
477
- private val marketBadgeImages = List(3) { OneKeyImageReusableView(reactContext) }
478
- private val marketBadgeGlyphs = List(3) { OneKeyIconView(context) }
479
- private val activityContentRow = LinearLayout(context)
480
- private val actionLine = LinearLayout(context)
481
- private val actionViews = List(3) { TextView(context) }
482
- private val trailingColumn = LinearLayout(context)
483
- private val trailingViews = List(2) { TextView(context) }
484
- private val trailingIcons = List(2) { OneKeyIconView(context) }
485
- private val checkbox = OneKeyCheckboxView(context)
486
- private val spinner = ProgressBar(context)
487
- private val dataContainer = LinearLayout(context)
488
- private val dataColumns = List(4) { TextView(context) }
489
- private val tableDataContainer = LinearLayout(context)
490
- private val tableDataColumns = List(4) { NativeListTableColumnView(context) }
491
- private val unreadDot = View(context)
492
- private val mediaBadge = TextView(context)
493
- private val skeletonPrimary = View(context)
494
- private val skeletonSecondary = View(context)
495
- private val walletGroupRows = mutableListOf<NativeListRowView>()
496
- private val walletGroupDragBadgeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
497
- private val walletGroupDragBadgeBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
498
- style = Paint.Style.STROKE
499
- }
500
- private val walletGroupDragBadgeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
501
- textAlign = Paint.Align.CENTER
502
- typeface = NativeListFonts.semibold(context)
503
- }
504
- private var walletGroupDragChildCount = 0
505
- private var walletGroupExpandedHeightPx = 0
506
- private var walletGroupExpandAnimator: ValueAnimator? = null
507
- private var isMediaTile = false
508
- private var boundKey: String? = null
509
- // OneKey patch: delayed retries cannot survive cell rebinding or recycling.
510
- private val selectorImageRetries = mutableMapOf<OneKeyImageReusableView, Runnable>()
511
- var bindingEpoch: Long = 0
512
- private set
513
- private var boundCheckboxData: JSONObject? = null
514
- private var currentLayout = "linear"
515
- private var restingRowBackground: Drawable? = null
516
- private var pressedRowBackground: Drawable? = null
517
- // OneKey patch: preserve a held row independently from RecyclerView snapshot rebinding.
518
- private var touchPressed = false
519
- private val marketLongPressHandler = Handler(Looper.getMainLooper())
520
- private var marketLongPressRunnable: Runnable? = null
521
- private var marketTouchStartX = 0f
522
- private var marketTouchStartY = 0f
523
- private var marketTouchX = 0f
524
- private var marketTouchY = 0f
525
- private var marketLongPressFired = false
526
- private var reorderActive = false
527
- private var checkboxCheckedColor = Color.rgb(32, 32, 32)
528
- private var checkboxUncheckedColor = Color.rgb(252, 252, 252)
529
- private var checkboxBorderColor = Color.rgb(206, 206, 206)
530
- private var checkboxIconColor = Color.rgb(252, 252, 252)
531
- private var checkboxUsesSelectorStyle = false
532
- private var iconSubduedColor = Color.rgb(141, 141, 141)
533
- private var visualBackdropColor = Color.WHITE
534
- private var imagePlaceholderColor = "#0000000F"
535
- private val circleOutlineProvider = object : ViewOutlineProvider() {
536
- override fun getOutline(view: View, outline: Outline) {
537
- outline.setOval(0, 0, view.width, view.height)
538
- }
539
- }
540
- private val separatorPaint = Paint(Paint.ANTI_ALIAS_FLAG)
541
- private var showsSeparator = false
542
-
543
- var onRowPress: ((NativeListItem, NativeListActionOrigin) -> Unit)? = null
544
- var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Unit)? = null
545
- var onBindingInvalidated: ((NativeListRowView, Long) -> Unit)? = null
546
-
547
- init {
548
- gravity = Gravity.CENTER_VERTICAL
549
- isClickable = true
550
- isFocusable = true
551
-
552
- leadingFallback.gravity = Gravity.CENTER
553
- leadingFallback.typeface = NativeListFonts.bold(context)
554
- leadingFrame.addView(leadingFallback)
555
- leadingFrame.addView(leadingIcon)
556
- leadingFrame.addView(leadingImages[0])
557
- leadingFrame.addView(leadingOverlayBackground)
558
- leadingFrame.addView(leadingImages[1])
559
- leadingFrame.addView(leadingImages[2])
560
- leadingCornerIconFrame.addView(leadingCornerIcon)
561
- leadingFrame.addView(leadingCornerIconFrame)
562
- leadingFrame.addView(unreadDot)
563
- leadingFrame.addView(mediaBadge)
564
- leadingFrame.clipChildren = false
565
- leadingFrame.clipToPadding = false
566
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
567
- favoriteIcon.layoutParams = LayoutParams(dp(24), dp(24))
568
- leadingActionIcon.layoutParams = LayoutParams(dp(24), dp(24))
569
- leadingCornerIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
570
- unreadDot.layoutParams = FrameLayout.LayoutParams(dp(8), dp(8), Gravity.TOP or Gravity.END)
571
- mainColumn.orientation = VERTICAL
572
- mainColumn.gravity = Gravity.CENTER_VERTICAL
573
- title.typeface = NativeListFonts.regular(context)
574
- subtitle.typeface = NativeListFonts.regular(context)
575
- tertiary.typeface = NativeListFonts.regular(context)
576
- status.typeface = NativeListFonts.regular(context)
577
- metricSubtitle.typeface = NativeListFonts.regular(context)
578
- badgeLine.typeface = NativeListFonts.medium(context)
579
- listOf(title, subtitle, tertiary, status, metricSubtitle, badgeLine).forEach {
580
- it.includeFontPadding = false
581
- it.fontFeatureSettings = "tnum"
582
- }
583
- subtitle.maxLines = 2
584
- status.maxLines = 1
585
- metricSubtitle.maxLines = 1
586
- badgeLine.maxLines = 1
587
- titleLine.orientation = HORIZONTAL
588
- titleLine.gravity = Gravity.CENTER_VERTICAL
589
- titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
590
- titleLine.addView(badgeLine, wrap())
591
- marketBadgeViews.forEachIndexed { index, badge ->
592
- badge.orientation = HORIZONTAL
593
- badge.gravity = Gravity.CENTER_VERTICAL
594
- badge.isClickable = true
595
- marketBadgeGlyphs[index].visibility = GONE
596
- marketBadgeImages[index].visibility = GONE
597
- marketBadgeLabels[index].apply {
598
- includeFontPadding = false
599
- maxLines = 1
600
- ellipsize = TextUtils.TruncateAt.END
601
- textSize = sp(11f)
602
- typeface = NativeListFonts.medium(context)
603
- }
604
- badge.addView(marketBadgeGlyphs[index], LayoutParams(dp(16), dp(16)))
605
- badge.addView(marketBadgeImages[index], LayoutParams(dp(14), dp(14)))
606
- badge.addView(marketBadgeLabels[index], wrap())
607
- }
608
- mainColumn.addView(titleLine)
609
- mainColumn.addView(subtitle)
610
- mainColumn.addView(tertiary)
611
- mainColumn.addView(status)
612
- mainColumn.addView(metricSubtitle)
613
-
614
- actionLine.orientation = HORIZONTAL
615
- actionViews.forEach { action ->
616
- action.typeface = NativeListFonts.semibold(context)
617
- action.includeFontPadding = false
618
- action.textSize = sp(14f)
619
- TextViewCompat.setLineHeight(action, dp(20))
620
- action.setPadding(dp(8), dp(4), dp(8), dp(4))
621
- actionLine.addView(action)
622
- }
623
- mainColumn.addView(actionLine)
624
-
625
- trailingColumn.orientation = VERTICAL
626
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
627
- trailingViews.forEach { trailingColumn.addView(it) }
628
- trailingIcons.forEach { trailingColumn.addView(it) }
629
- trailingColumn.addView(checkbox)
630
- trailingColumn.addView(spinner)
631
-
632
- dataContainer.orientation = HORIZONTAL
633
- dataContainer.gravity = Gravity.CENTER_VERTICAL
634
- dataColumns.forEach { dataContainer.addView(it) }
635
- tableDataContainer.orientation = HORIZONTAL
636
- tableDataContainer.gravity = Gravity.CENTER_VERTICAL
637
- tableDataColumns.forEach { tableDataContainer.addView(it) }
638
-
639
- mediaBadge.gravity = Gravity.CENTER
640
- mediaBadge.typeface = NativeListFonts.regular(context)
641
- secondaryImage.outlineProvider = object : ViewOutlineProvider() {
642
- override fun getOutline(view: View, outline: Outline) {
643
- outline.setRoundRect(0, 0, view.width, view.height, dp(6).toFloat())
644
- }
645
- }
646
- secondaryImage.clipToOutline = true
647
-
648
- setOnTouchListener { _, event ->
649
- when (event.actionMasked) {
650
- MotionEvent.ACTION_DOWN -> if ((tag as? NativeListItem)?.isRowPressEnabled == true) {
651
- touchPressed = true
652
- marketLongPressFired = false
653
- val item = tag as? NativeListItem
654
- if (item?.type == "market") {
655
- marketTouchStartX = event.x
656
- marketTouchStartY = event.y
657
- marketTouchX = event.x
658
- marketTouchY = event.y
659
- item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
660
- emitAction(item, actionKey, null, this, "row")
661
- }
662
- item.json.optString("longPressActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
663
- val expectedKey = item.key
664
- val runnable = Runnable {
665
- val current = tag as? NativeListItem
666
- if (touchPressed && current?.key == expectedKey && current.type == "market") {
667
- marketLongPressRunnable = null
668
- marketLongPressFired = true
669
- val location = IntArray(2)
670
- getLocationInWindow(location)
671
- onAction?.invoke(current, actionKey, null, actionOrigin(this, "row").copy(
672
- windowPointPixels = android.graphics.PointF(location[0] + marketTouchX, location[1] + marketTouchY),
673
- ))
674
- }
675
- }
676
- marketLongPressRunnable = runnable
677
- marketLongPressHandler.postDelayed(runnable, 800L)
678
- }
679
- }
680
- if ((tag as? NativeListItem)?.type == "mediaTile") {
681
- leadingFrame.alpha = 0.8f
682
- } else {
683
- background = pressedRowBackground
684
- }
685
- }
686
- MotionEvent.ACTION_MOVE -> {
687
- marketTouchX = event.x
688
- marketTouchY = event.y
689
- val outside = event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
690
- val movedMarket = (tag as? NativeListItem)?.type == "market" &&
691
- (kotlin.math.abs(event.x - marketTouchStartX) > dp(10) ||
692
- kotlin.math.abs(event.y - marketTouchStartY) > dp(10))
693
- if (outside || movedMarket) {
694
- cancelMarketLongPress()
695
- touchPressed = false
696
- restoreRestingBackground()
697
- }
698
- }
699
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
700
- cancelMarketLongPress()
701
- touchPressed = false
702
- restoreRestingBackground()
703
- }
704
- }
705
- false
706
- }
707
- setOnClickListener { view ->
708
- (view.tag as? NativeListItem)?.let { item ->
709
- if (item.type == "market" && marketLongPressFired) {
710
- marketLongPressFired = false
711
- return@let
712
- }
713
- // OneKey patch: allow create-address accessories when whole-row press is gated.
714
- if (item.isRowPressEnabled) onRowPress?.invoke(item, actionOrigin(view, "row"))
715
- }
716
- }
717
- setWillNotDraw(false)
718
- }
719
-
720
- override fun dispatchDraw(canvas: Canvas) {
721
- super.dispatchDraw(canvas)
722
- if (showsSeparator) {
723
- val start = if ((tag as? NativeListItem)?.type == "identity") dp(60).toFloat() else dp(12).toFloat()
724
- canvas.drawLine(start, height - 1f, width.toFloat(), height - 1f, separatorPaint)
725
- }
726
- if (reorderActive && (tag as? NativeListItem)?.type == "walletGroup" && walletGroupDragChildCount > 0) {
727
- val label = "+$walletGroupDragChildCount"
728
- val badgeHeight = dp(24).toFloat()
729
- val badgeWidth = maxOf(
730
- dp(24).toFloat(),
731
- walletGroupDragBadgeTextPaint.measureText(label) + dp(12),
732
- )
733
- val right = width - dp(4).toFloat()
734
- val bottom = height - dp(4).toFloat()
735
- val left = right - badgeWidth
736
- val top = bottom - badgeHeight
737
- canvas.drawRoundRect(
738
- left,
739
- top,
740
- right,
741
- bottom,
742
- badgeHeight / 2f,
743
- badgeHeight / 2f,
744
- walletGroupDragBadgeBackgroundPaint,
745
- )
746
- canvas.drawRoundRect(
747
- left,
748
- top,
749
- right,
750
- bottom,
751
- dp(12).toFloat(),
752
- dp(12).toFloat(),
753
- walletGroupDragBadgeBorderPaint,
754
- )
755
- val metrics = walletGroupDragBadgeTextPaint.fontMetrics
756
- val baseline = (top + bottom - metrics.ascent - metrics.descent) / 2f
757
- canvas.drawText(label, (left + right) / 2f, baseline, walletGroupDragBadgeTextPaint)
758
- }
759
- }
760
-
761
- // OneKey patch: RecyclerView may replace item delegates during a selection update.
762
- override fun onInitializeAccessibilityNodeInfo(info: android.view.accessibility.AccessibilityNodeInfo) {
763
- super.onInitializeAccessibilityNodeInfo(info)
764
- info.viewIdResourceName = getTag(com.facebook.react.R.id.react_test_id) as? String
765
- }
766
-
767
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
768
- val marketItem = (tag as? NativeListItem)?.takeIf { it.type == "market" }
769
- val marketStyle = marketItem?.json?.optJSONObject("style")
770
- if (marketStyle?.has("horizontalPadding") == true) {
771
- // OneKey patch: Yoga rounds the two absolute edges, not both padding values.
772
- val sourcePadding = marketStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
773
- val width = MeasureSpec.getSize(widthMeasureSpec)
774
- val rowHeight = selectorHeight
775
- val sourceVerticalPadding = marketStyle.takeIf { it.has("verticalPadding") && rowHeight != null }
776
- ?.optDouble("verticalPadding")?.times(resources.displayMetrics.density)
777
- setPadding(
778
- sourcePadding.roundToInt(),
779
- sourceVerticalPadding?.roundToInt() ?: paddingTop,
780
- width - (width - sourcePadding).roundToInt(),
781
- if (sourceVerticalPadding != null && rowHeight != null) rowHeight - (rowHeight - sourceVerticalPadding).roundToInt() else paddingBottom,
782
- )
783
- }
784
- if (isMediaTile) {
785
- val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
786
- .coerceAtLeast(0)
787
- leadingFrame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, availableWidth)
788
- }
789
- // OneKey patch: RecyclerView must use the adapter's measured selector height.
790
- super.onMeasure(widthMeasureSpec, selectorHeight?.let { MeasureSpec.makeMeasureSpec(it, MeasureSpec.EXACTLY) } ?: heightMeasureSpec)
791
- val item = tag as? NativeListItem ?: return
792
- if (item.json.has("height") && item.json.optString("presentation") == "networkSelector" && item.type in setOf("identity", "sectionHeader") && checkbox.visibility == VISIBLE && trailingColumn.parent === this) {
793
- // OneKey patch: Yoga snaps the compound accessory before its children; their rounded edges can overflow it.
794
- val density = resources.displayMetrics.density
795
- val visibleValues = trailingViews.filter { it.visibility == VISIBLE }
796
- val sourceTrailingWidth = visibleValues.sumOf { it.measuredWidth } + (20 + 12 * visibleValues.size) * density
797
- val sourceTrailingLeft = (measuredWidth - 12 * density - sourceTrailingWidth).roundToInt()
798
- val measuredTrailingLeft = measuredWidth - paddingRight - trailingColumn.measuredWidth
799
- val mainWidth = (mainColumn.measuredWidth + sourceTrailingLeft - measuredTrailingLeft).coerceAtLeast(0)
800
- mainColumn.measure(MeasureSpec.makeMeasureSpec(mainWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mainColumn.measuredHeight, MeasureSpec.EXACTLY))
801
- }
802
- }
803
-
804
- // OneKey patch: Yoga rounds a half-pixel text center upward; LinearLayout truncates it.
805
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
806
- super.onLayout(changed, left, top, right, bottom)
807
- val item = tag as? NativeListItem ?: return
808
- if (item.type == "market" && item.json.optJSONObject("style") != null) {
809
- // OneKey patch: preserve Yoga's half-pixel centering for Market columns and badges.
810
- for (column in listOf(leadingFrame, mainColumn, trailingColumn)) {
811
- if (column.parent === this && column.visibility != GONE) {
812
- column.offsetTopAndBottom((height - column.height + 1) / 2 - column.top)
813
- }
814
- }
815
- // OneKey patch: token avatars stay centered in the row when text rounding adds a pixel.
816
- if (item.json.optString("variant") != "token" && leadingFrame.parent === this && mainColumn.parent === this) {
817
- leadingFrame.offsetTopAndBottom(mainColumn.top + (mainColumn.height - leadingFrame.height + 1) / 2 - leadingFrame.top)
818
- }
819
- // OneKey patch: preserve Yoga's absolute-edge rounding for the Market network badge.
820
- if (leadingOverlayBackground.visibility == VISIBLE && item.json.optString("variant") == "token") {
821
- val recycler = parent as? RecyclerView
822
- val adapter = recycler?.adapter as? NativeListAdapter
823
- val position = recycler?.getChildAdapterPosition(this) ?: RecyclerView.NO_POSITION
824
- if (recycler != null && adapter != null && position != RecyclerView.NO_POSITION && adapter.itemAt(position)?.key == item.key) {
825
- val density = resources.displayMetrics.density.toDouble()
826
- val rowTop = adapter.marketSourceEdgePx(position)
827
- val rowHeight = adapter.marketSourceEdgePx(position + 1) - rowTop
828
- val imageHeight = item.json.optJSONObject("style")?.optJSONObject("image")?.optDouble("height", 32.0) ?: 32.0
829
- val contentTop = (recycler.paddingTop / density).roundToInt() * density
830
- val badgeTop = contentTop + rowTop + (rowHeight - imageHeight * density) / 2 + (imageHeight - 16) * density
831
- val badgeHeight = (badgeTop + 20 * density).roundToInt() - badgeTop.roundToInt()
832
- val top = ((imageHeight - 16) * density).roundToInt()
833
- leadingOverlayBackground.layout(leadingOverlayBackground.left, top, leadingOverlayBackground.right, top + badgeHeight)
834
- }
835
- }
836
- for (badge in marketBadgeViews) {
837
- if (badge.parent === titleLine && badge.visibility != GONE) {
838
- badge.offsetTopAndBottom((titleLine.height - badge.height + 1) / 2 - badge.top)
839
- }
840
- }
841
- // OneKey patch: preserve fractional text advances and gaps until the final edge.
842
- val style = item.json.optJSONObject("style")
843
- for ((line, gap) in listOf(
844
- titleLine to style.optDouble("titleBadgeGap", 4.0),
845
- marketSubtitleLine to (item.json.optJSONObject("subtitlePrefix")?.optDouble("gap", 4.0) ?: 0.0),
846
- )) {
847
- var cursor = 0f
848
- var hasPrevious = false
849
- for (index in 0 until line.childCount) {
850
- val child = line.getChildAt(index)
851
- if (child.visibility == GONE) continue
852
- if (hasPrevious) {
853
- cursor += (gap * resources.displayMetrics.density).toFloat()
854
- child.offsetLeftAndRight(cursor.roundToInt() - child.left)
855
- }
856
- val advance = if (index == 0 && child is TextView) {
857
- val textWidth = if (line === titleLine) child.paint.measureText(child.text.toString()) else child.layout?.getLineWidth(0)
858
- textWidth?.coerceAtMost(child.width.toFloat()) ?: child.width.toFloat()
859
- } else child.width.toFloat()
860
- cursor = if (hasPrevious) cursor + advance else child.left + advance
861
- hasPrevious = true
862
- }
863
- }
864
- }
865
- val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
866
- if (item.type == "identity" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && accessory?.optString("kind") == "icon" && accessory.optString("name") == "PlusSmallOutline") {
867
- // OneKey patch: the borderless Plus retains the source's fixed top18/negative7 slot.
868
- val icon = trailingIcons[0]
869
- trailingColumn.offsetTopAndBottom(dp(18) - dp(7) - trailingColumn.top - icon.top)
870
- }
871
- if (item.type == "action" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && leadingIcon.visibility == VISIBLE) {
872
- // OneKey patch: Yoga rounds the 4dp padding inside the 32dp Add account icon upward.
873
- leadingIcon.offsetLeftAndRight((leadingFrame.width - leadingIcon.width + 1) / 2 - leadingIcon.left)
874
- leadingIcon.offsetTopAndBottom((leadingFrame.height - leadingIcon.height + 1) / 2 - leadingIcon.top)
875
- }
876
- if (!item.json.has("height")) return
877
- val isNetworkIdentity = item.type == "identity" && item.json.optString("presentation") == "networkSelector"
878
- val isAccountAction = item.type == "action" && item.json.optString("presentation") == "accountSelector"
879
- val isNetworkSummary = item.type == "sectionHeader" && item.json.optString("presentation") == "networkSelector" && item.json.optString("variant") == "summary"
880
- val centeredColumns = when {
881
- isNetworkIdentity || isAccountAction -> listOf(mainColumn, trailingColumn)
882
- isNetworkSummary -> listOf(trailingColumn)
883
- else -> return
884
- }
885
- for (column in centeredColumns) {
886
- if (column.parent !== this || column.visibility == GONE) continue
887
- val margins = column.layoutParams as MarginLayoutParams
888
- val available = height - paddingTop - paddingBottom - margins.topMargin - margins.bottomMargin
889
- val desiredTop = paddingTop + margins.topMargin + (available - column.height + 1) / 2
890
- column.offsetTopAndBottom(desiredTop - column.top)
891
- }
892
- }
893
-
894
- fun bind(
895
- item: NativeListItem,
896
- theme: JSONObject?,
897
- layout: String,
898
- listOrientation: String,
899
- itemIndex: Int?,
900
- selected: Boolean,
901
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
902
- useSourceScale: Boolean = false,
903
- ) {
904
- val reusesImageIdentity = boundKey == item.key && (tag as? NativeListItem)?.type == item.type
905
- val shouldRestorePressed = touchPressed && boundKey == item.key
906
- cancelMarketLongPress()
907
- marketLongPressFired = false
908
- invalidateCurrentBinding()
909
- bindingEpoch += 1
910
- boundKey = item.key
911
- touchPressed = shouldRestorePressed
912
- currentLayout = layout
913
- tag = item
914
- selectorUsesSourceScale = item.type == "market" || item.type == "system" && item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry" || item.usesSelectorSourceScale || useSourceScale
915
- reorderActive = false
916
- if (!reusesImageIdentity) {
917
- selectorImages.forEach(OneKeyImageReusableView::prepareForReuse)
918
- leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
919
- secondaryImage.prepareForReuse()
920
- mediaNetworkImage.prepareForReuse()
921
- metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
922
- }
923
- resetViews()
924
-
925
- val primary = color(theme, "primaryText", "#000000DF")
926
- val secondary = color(theme, "secondaryText", "#0000009B")
927
- val accent = color(theme, "accent", "#0D8200FC")
928
- checkboxCheckedColor = primary
929
- checkboxUncheckedColor = color(theme, "inverseText", "#FCFCFC")
930
- checkboxIconColor = checkboxUncheckedColor
931
- checkboxUsesSelectorStyle = item.json.optString("presentation") == "networkSelector"
932
- checkboxBorderColor = Color.argb(
933
- 0x31,
934
- 0,
935
- 0,
936
- 0,
937
- )
938
- if (item.json.optString("presentation") == "networkSelector") {
939
- checkboxCheckedColor = color(theme, "checkboxBackground", "#202020")
940
- checkboxBorderColor = color(theme, "checkboxBorder", "#00000031")
941
- checkboxIconColor = color(theme, "checkboxIcon", "#FFFFFF")
942
- // OneKey patch: the V1 checkbox fills even its unchecked body with iconInverse.
943
- checkboxUncheckedColor = checkboxIconColor
944
- }
945
- iconSubduedColor = color(theme, "iconSubdued", "#00000072")
946
- visualBackdropColor = color(theme, "rowBackground", "#FFFFFF")
947
- imagePlaceholderColor = theme?.optString("strongBackground", "#0000000F")
948
- ?.takeIf(String::isNotEmpty) ?: "#0000000F"
949
- unreadDot.background = roundedFill(
950
- parseNativeListColor("#E5484D"),
951
- 4f,
952
- )
953
- applySelectionState(item, theme, layout, itemIndex, selected)
954
- pressedRowBackground = groupedBackground(
955
- when (item.type) {
956
- "rail" -> "rail"
957
- "mediaTile" -> "mediaTile"
958
- else -> "single"
959
- },
960
- color(
961
- theme,
962
- if (item.type == "rail") "strongBackground" else "rowPressedBackground",
963
- if (item.type == "rail") "#0000000F" else "#00000017",
964
- ),
965
- )
966
- background = if (touchPressed || reorderActive) pressedRowBackground else restingRowBackground
967
- if (layout == "table") {
968
- if (item.type == "dataRow") {
969
- setPadding(dp(20), dp(10), dp(20), dp(10))
970
- } else {
971
- setPadding(dp(16), dp(8), dp(16), dp(8))
972
- }
973
- }
974
- title.setTextColor(primary)
975
- subtitle.setTextColor(secondary)
976
- tertiary.setTextColor(secondary)
977
- status.setTextColor(secondary)
978
- metricSubtitle.setTextColor(secondary)
979
- badgeLine.setTextColor(accent)
980
- separatorPaint.color = color(theme, "separator", "#0000001F")
981
- separatorPaint.strokeWidth = 1f
982
- showsSeparator = item.json.optBoolean("separator", false) &&
983
- !item.key.startsWith("token-") &&
984
- !item.key.startsWith("balance-token-") &&
985
- item.key != "linear-custom-token"
986
- invalidate()
987
- trailingViews.forEach { it.setTextColor(primary) }
988
- isEnabled = !item.json.optBoolean("disabled", false)
989
- if (!isEnabled) touchPressed = false
990
- background = if (touchPressed || reorderActive) pressedRowBackground else restingRowBackground
991
- // OneKey patch: deprecation dims the row without disabling menu controls.
992
- // alpha = if (isEnabled) 1f else 0.5f
993
- alpha = item.json.optDouble("opacity", 1.0).toFloat() * (if (isEnabled) 1f else 0.5f)
994
- contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
995
- // OneKey patch: retain stable original selector test identifiers.
996
- setTag(com.facebook.react.R.id.react_test_id, item.json.optString("testID").takeIf { it.isNotEmpty() })
997
-
998
- when (item.type) {
999
- "walletGroup" -> bindWalletGroup(item, theme, layout, listOrientation, checkboxState)
1000
- "identity" -> bindIdentity(item, theme, selected, checkboxState)
1001
- "rail" -> bindRail(item, theme)
1002
- "activity" -> bindActivity(item, theme)
1003
- "message" -> bindMessage(item, theme)
1004
- "dataRow" -> bindDataRow(item, theme, checkboxState)
1005
- "market" -> bindMarket(item, theme)
1006
- "mediaTile" -> bindMediaTile(item, theme)
1007
- "metricCard" -> bindMetricCard(item, theme)
1008
- "sectionHeader" -> bindSectionHeader(item, theme, checkboxState)
1009
- "action" -> bindAction(item, theme, checkboxState)
1010
- "system" -> bindSystem(item, theme)
1011
- }
1012
- // Keep passive rows out of accessibility and keyboard focus while allowing
1013
- // their independently bound accessory controls to remain interactive.
1014
- val wholeRowPressEnabled = item.isWholeRowInteractive
1015
- isClickable = wholeRowPressEnabled
1016
- isFocusable = wholeRowPressEnabled
1017
- applySize(item)
1018
- if (item.type == "system" && item.json.optString("variant") == "warning") {
1019
- title.typeface = NativeListFonts.medium(context)
1020
- title.textSize = sp(14f)
1021
- subtitle.textSize = sp(14f)
1022
- TextViewCompat.setLineHeight(title, dp(20))
1023
- TextViewCompat.setLineHeight(subtitle, dp(20))
1024
- minimumHeight = 0
1025
- }
1026
- applyListOrientation(item, listOrientation)
1027
- applySelectorTypography(item)
1028
- }
1029
-
1030
- // OneKey patch: keep a small idle member pool after reuse or a direct rebind.
1031
- // The compact proxy/expansion keeps every member until it leaves that state.
1032
- private fun trimWalletGroupRows(required: Int) {
1033
- val retained = maxOf(8, required)
1034
- while (walletGroupRows.size > retained) {
1035
- val row = walletGroupRows.removeAt(walletGroupRows.lastIndex)
1036
- (row.parent as? ViewGroup)?.removeView(row)
1037
- row.dispose()
1038
- row.onRowPress = null
1039
- row.onAction = null
1040
- row.onBindingInvalidated = null
1041
- }
1042
- }
1043
-
1044
- fun recycle() {
1045
- cancelMarketLongPress()
1046
- marketLongPressFired = false
1047
- touchPressed = false
1048
- restoreRestingBackground()
1049
- invalidateCurrentBinding()
1050
- boundKey = null
1051
- selectorImages.forEach(OneKeyImageReusableView::prepareForReuse)
1052
- leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
1053
- secondaryImage.prepareForReuse()
1054
- mediaNetworkImage.prepareForReuse()
1055
- metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
1056
- walletGroupRows.forEach { row ->
1057
- row.visibility = VISIBLE
1058
- row.alpha = 1f
1059
- row.recycle()
1060
- }
1061
- if (!reorderActive && walletGroupExpandAnimator?.isRunning != true) trimWalletGroupRows(0)
1062
- }
1063
-
1064
- fun bindSelection(
1065
- item: NativeListItem,
1066
- theme: JSONObject?,
1067
- layout: String,
1068
- itemIndex: Int?,
1069
- selected: Boolean,
1070
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1071
- ) {
1072
- if (boundKey != item.key) return
1073
- // OneKey patch: keep row callbacks and checkbox fallback state current without resetting images.
1074
- tag = item
1075
- if (item.type == "walletGroup") {
1076
- val members = buildList {
1077
- add(item.json.getJSONObject("parent"))
1078
- item.json.getJSONArray("children").let { children ->
1079
- for (index in 0 until children.length()) add(children.getJSONObject(index))
1080
- }
1081
- }
1082
- members.forEachIndexed { index, memberJson ->
1083
- walletGroupRows.getOrNull(index)?.bindSelection(
1084
- NativeListItem.parse(memberJson),
1085
- theme,
1086
- layout,
1087
- null,
1088
- memberJson.optBoolean("selected", false),
1089
- checkboxState,
1090
- )
1091
- }
1092
- // The group owns the subdued backdrop. Selection belongs to a member;
1093
- // applying the translucent selected color to both levels produces dark
1094
- // bands in the 12dp gaps and double-composites the parent highlight.
1095
- background = restingRowBackground
1096
- return
1097
- }
1098
- applySelectionState(item, theme, layout, itemIndex, selected)
1099
- if (item.type == "identity" && item.json.optString("presentation") == "walletSidebar") {
1100
- title.setTextColor(
1101
- color(
1102
- theme,
1103
- if (selected) "primaryText" else "secondaryText",
1104
- if (selected) "#FFFFFFED" else "#FFFFFFAF",
1105
- ),
1106
- )
1107
- }
1108
- // OneKey patch: the comparator permits state changes only on these existing checkbox slots.
1109
- // boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
1110
- if (boundCheckboxData != null) {
1111
- val latestCheckbox = if (item.type == "identity") {
1112
- item.json.optJSONArray("trailing")?.let { trailing ->
1113
- (0 until trailing.length()).mapNotNull { trailing.optJSONObject(it) }
1114
- .lastOrNull { it.optString("kind") == "checkbox" }
1115
- }
1116
- } else item.json.optJSONObject("checkbox")
1117
- boundCheckboxData = latestCheckbox ?: boundCheckboxData
1118
- boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
1119
- }
1120
- }
1121
-
1122
- fun bindStableSummary(item: NativeListItem) {
1123
- if (
1124
- boundKey != item.key ||
1125
- item.type != "sectionHeader" ||
1126
- item.json.optString("variant") != "summary"
1127
- ) {
1128
- return
1129
- }
1130
- tag = item
1131
- contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
1132
- title.text = item.json.optString("title")
1133
- trailingViews[0].text = item.json.optString("value")
1134
- applySelectorTypography(item)
1135
- }
1136
-
1137
- fun dispose() {
1138
- cancelMarketLongPress()
1139
- invalidateCurrentBinding()
1140
- restoreRestingBackground()
1141
- selectorImages.forEach(OneKeyImageReusableView::dispose)
1142
- selectorImages.clear()
1143
- leadingImages.forEach(OneKeyImageReusableView::dispose)
1144
- secondaryImage.dispose()
1145
- mediaNetworkImage.dispose()
1146
- metricVisualImages.forEach(OneKeyImageReusableView::dispose)
1147
- marketBadgeImages.forEach(OneKeyImageReusableView::dispose)
1148
- walletGroupRows.forEach(NativeListRowView::dispose)
1149
- }
1150
-
1151
- private fun actionOrigin(
1152
- sourceView: View,
1153
- source: String,
1154
- slot: Int? = null,
1155
- ) = NativeListActionOrigin(sourceView, this, bindingEpoch, source, slot,
1156
- if ((tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" && sourceView in trailingIcons && (sourceView.layoutParams as MarginLayoutParams).marginStart < 0) dp(7) else 0)
1157
-
1158
- private fun emitAction(
1159
- item: NativeListItem,
1160
- actionKey: String,
1161
- target: NativeSelectionTarget?,
1162
- sourceView: View,
1163
- source: String,
1164
- slot: Int? = null,
1165
- ) {
1166
- onAction?.invoke(item, actionKey, target, actionOrigin(sourceView, source, slot))
1167
- }
1168
-
1169
- // OneKey patch: match SizableText TABULAR_NUMS on dynamic labels without replacing their typeface.
1170
- private fun applySelectorTypography(item: NativeListItem) {
1171
- val usesSelectorTypography = item.json.optString("presentation") in setOf("accountSelector", "networkSelector", "walletSidebar") || item.type == "system" && item.json.optString("variant") == "warning"
1172
- fun visit(view: View) {
1173
- if (view is OneKeyIconView) view.useSourceScale = selectorUsesSourceScale
1174
- if (view is DottedUnderlineTextView) view.useSourceScale = selectorUsesSourceScale
1175
- if (view is TextView && usesSelectorTypography) {
1176
- val selectorLineHeight = selectorLineHeights.getOrPut(view) { view.lineHeight }
1177
- val original = view.fontFeatureSettings
1178
- if (!selectorOriginalFontFeatures.containsKey(view)) selectorOriginalFontFeatures[view] = original
1179
- view.fontFeatureSettings = if (original.isNullOrEmpty()) "tnum" else if (original.contains("tnum")) original else "$original, 'tnum' 1"
1180
- // OneKey patch: SizableText disables font scaling and rounds font sizes to whole pixels.
1181
- val sourceTypography = selectorUsesSourceScale || item.type == "system" && item.json.optString("variant") == "warning"
1182
- if (sourceTypography) {
1183
- // OneKey patch: React Native CustomStyleSpan disables hinting and preserves fractional advances.
1184
- selectorOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
1185
- view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
1186
- val originalSize = selectorFontSizes.getOrPut(view) { view.textSize }
1187
- val sourceSize = originalSize * resources.displayMetrics.density / resources.displayMetrics.scaledDensity
1188
- view.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
1189
- view.letterSpacing = 0f
1190
- }
1191
- if (sourceTypography && view.text.isNotEmpty()) {
1192
- val text = SpannableStringBuilder(view.text)
1193
- text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
1194
- text.setSpan(SelectorLineHeightSpan(selectorLineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
1195
- view.setLineSpacing(0f, 1f)
1196
- view.text = text
1197
- }
1198
- }
1199
- if (view is ViewGroup) for (index in 0 until view.childCount) visit(view.getChildAt(index))
1200
- }
1201
- visit(this)
1202
- }
1203
-
1204
- private fun invalidateCurrentBinding() {
1205
- selectorImageRetries.forEach { (image, retry) -> image.removeCallbacks(retry) }
1206
- selectorImageRetries.clear()
1207
- if (boundKey == null) return
1208
- onBindingInvalidated?.invoke(this, bindingEpoch)
1209
- bindingEpoch += 1
1210
- }
1211
-
1212
- private fun resetViews() {
1213
- clipChildren = true
1214
- clipToPadding = true
1215
- selectorOriginalFontFeatures.forEach { (view, original) -> view.fontFeatureSettings = original }
1216
- selectorOriginalFontFeatures.clear()
1217
- selectorOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1218
- selectorOriginalPaintFlags.clear()
1219
- marketOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1220
- marketOriginalPaintFlags.clear()
1221
- selectorLineHeights.clear()
1222
- selectorFontSizes.clear()
1223
- selectorImages.forEach { (it.parent as? ViewGroup)?.removeView(it) }
1224
- // OneKey patch: selector-only views cannot survive a recycled binding.
1225
- selectorViews.forEach { (it.parent as? ViewGroup)?.removeView(it) }
1226
- selectorViews.clear()
1227
- selectorHeight = null
1228
- marketBadgeViews.forEachIndexed { index, badge ->
1229
- (badge.parent as? ViewGroup)?.removeView(badge)
1230
- badge.visibility = GONE
1231
- badge.background = null
1232
- badge.setPadding(0, 0, 0, 0)
1233
- badge.setOnClickListener(null)
1234
- badge.contentDescription = null
1235
- marketBadgeLabels[index].apply {
1236
- // OneKey patch: restore every optional Market badge typography property on reuse.
1237
- text = ""
1238
- layoutParams = wrap()
1239
- fontFeatureSettings = null
1240
- textSize = sp(11f)
1241
- typeface = NativeListFonts.medium(context)
1242
- setLineSpacing(0f, 1f)
1243
- includeFontPadding = false
1244
- maxLines = 1
1245
- ellipsize = TextUtils.TruncateAt.END
1246
- gravity = Gravity.START or Gravity.CENTER_VERTICAL
1247
- setTextColor(color(null, "secondaryText", "#0000009B"))
1248
- }
1249
- marketBadgeImages[index].prepareForReuse()
1250
- marketBadgeImages[index].visibility = GONE
1251
- marketBadgeGlyphs[index].apply {
1252
- visibility = GONE
1253
- iconName = ""
1254
- tintColor = color(null, "secondaryText", "#0000009B")
1255
- }
1256
- }
1257
- title.setOnClickListener(null)
1258
- title.isClickable = false
1259
- walletGroupRows.forEach { it.invalidateCurrentBinding() }
1260
- walletGroupExpandAnimator?.removeAllListeners()
1261
- walletGroupExpandAnimator?.cancel()
1262
- walletGroupExpandAnimator = null
1263
- layoutParams?.takeIf { it.height != ViewGroup.LayoutParams.WRAP_CONTENT }?.let { params ->
1264
- params.height = ViewGroup.LayoutParams.WRAP_CONTENT
1265
- layoutParams = params
1266
- }
1267
- restoreRestingBackground()
1268
- walletGroupRows.forEach { row ->
1269
- row.visibility = VISIBLE
1270
- row.alpha = 1f
1271
- row.recycle()
1272
- }
1273
- activityContentRow.removeAllViews()
1274
- (actionLine.parent as? ViewGroup)?.removeView(actionLine)
1275
- removeAllViews()
1276
- // OneKey patch: the old animator is cancelled and old children are detached.
1277
- // Do not trim currently needed members when re-binding an expanded group.
1278
- val nextItem = tag as? NativeListItem
1279
- val required = if (nextItem?.type == "walletGroup") (nextItem.json.optJSONArray("children")?.length() ?: 0) + 1 else 0
1280
- trimWalletGroupRows(required)
1281
- orientation = HORIZONTAL
1282
- gravity = Gravity.CENTER_VERTICAL
1283
- minimumHeight = 0
1284
- setPadding(dp(12), dp(8), dp(12), dp(8))
1285
- isMediaTile = false
1286
- walletGroupDragChildCount = 0
1287
- walletGroupExpandedHeightPx = 0
1288
- boundCheckboxData = null
1289
- mainColumn.orientation = VERTICAL
1290
- mainColumn.gravity = Gravity.CENTER_VERTICAL
1291
- // OneKey patch: remove the Market group before restoring shared labels.
1292
- marketSubtitleLine.removeAllViews()
1293
- mainColumn.removeView(marketSubtitleLine)
1294
- mediaMetadataRow.removeView(subtitle)
1295
- mediaMetadataRow.removeView(mediaNetworkImage)
1296
- mainColumn.removeView(mediaMetadataRow)
1297
- mainColumn.removeView(titleLine)
1298
- mainColumn.removeView(subtitle)
1299
- mainColumn.removeView(tertiary)
1300
- mainColumn.addView(titleLine, 0)
1301
- mainColumn.addView(subtitle, 1)
1302
- mainColumn.addView(tertiary, 2)
1303
- mainColumn.addView(actionLine)
1304
- titleLine.packsChildrenAtStart = false
1305
- titleLine.removeView(headerTitleIcon)
1306
- trailingColumn.removeView(headerValueIcon)
1307
- title.gravity = Gravity.START
1308
- title.ellipsize = null
1309
- title.maxWidth = Int.MAX_VALUE
1310
- title.setHorizontallyScrolling(false)
1311
- title.text = ""
1312
- subtitle.text = ""
1313
- tertiary.text = ""
1314
- status.text = ""
1315
- metricSubtitle.text = ""
1316
- badgeLine.text = ""
1317
- title.setLineSpacing(0f, 1f)
1318
- subtitle.setLineSpacing(0f, 1f)
1319
- tertiary.setLineSpacing(0f, 1f)
1320
- tertiary.maxWidth = Int.MAX_VALUE
1321
- tertiary.ellipsize = TextUtils.TruncateAt.END
1322
- tertiary.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1323
- title.letterSpacing = 0f
1324
- title.showsDottedUnderline = false
1325
- title.setPadding(0, 0, 0, 0)
1326
- subtitle.letterSpacing = 0f
1327
- status.letterSpacing = 0f
1328
- badgeLine.letterSpacing = 0f
1329
- status.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1330
- subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1331
- title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
1332
- badgeLine.layoutParams = wrap()
1333
- titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1334
- title.visibility = GONE
1335
- subtitle.visibility = GONE
1336
- tertiary.visibility = GONE
1337
- status.visibility = GONE
1338
- metricSubtitle.visibility = GONE
1339
- badgeLine.visibility = GONE
1340
- badgeLine.background = null
1341
- badgeLine.setPadding(0, 0, 0, 0)
1342
- actionLine.visibility = GONE
1343
- actionViews.forEach {
1344
- it.visibility = GONE
1345
- it.background = null
1346
- it.setOnClickListener(null)
1347
- }
1348
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1349
- trailingColumn.orientation = VERTICAL
1350
- trailingColumn.layoutParams = wrap()
1351
- trailingColumn.clipChildren = true
1352
- trailingViews.forEach {
1353
- it.visibility = GONE
1354
- it.setTag(com.facebook.react.R.id.react_test_id, null)
1355
- it.gravity = Gravity.END
1356
- it.maxLines = 1
1357
- it.layoutParams = wrap()
1358
- it.background = null
1359
- it.setPadding(0, 0, 0, 0)
1360
- it.setOnClickListener(null)
1361
- }
1362
- trailingIcons.forEach {
1363
- it.visibility = GONE
1364
- it.layoutParams = LayoutParams(dp(24), dp(24)).apply { gravity = Gravity.END }
1365
- it.glyphSizeDp = null
1366
- it.alpha = 1f
1367
- it.isEnabled = true
1368
- it.background = null
1369
- it.setPadding(0, 0, 0, 0)
1370
- it.setOnClickListener(null)
1371
- }
1372
- checkbox.visibility = GONE
1373
- checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
1374
- checkbox.alpha = 1f
1375
- checkbox.setOnClickListener(null)
1376
- spinner.visibility = GONE
1377
- spinner.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
1378
- spinner.alpha = 1f
1379
- marketLeadingUsesSourceClip = false
1380
- leadingFrame.visibility = GONE
1381
- leadingFrame.background = null
1382
- leadingFrame.clipChildren = false
1383
- leadingFrame.clipToPadding = false
1384
- secondaryImage.visibility = GONE
1385
- secondaryImage.foreground = null
1386
- mediaNetworkImage.visibility = GONE
1387
- metricVisualImages.forEach {
1388
- (it.parent as? ViewGroup)?.removeView(it)
1389
- it.visibility = GONE
1390
- it.background = null
1391
- it.clipToOutline = false
1392
- it.outlineProvider = ViewOutlineProvider.BACKGROUND
1393
- }
1394
- unreadDot.visibility = GONE
1395
- mediaBadge.visibility = GONE
1396
- mediaBadge.text = ""
1397
- leadingIcon.visibility = GONE
1398
- leadingIcon.iconName = ""
1399
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
1400
- leadingIcon.glyphSizeDp = null
1401
- favoriteIcon.visibility = GONE
1402
- favoriteIcon.iconName = ""
1403
- headerTitleIcon.visibility = GONE
1404
- headerTitleIcon.iconName = ""
1405
- headerValueIcon.visibility = GONE
1406
- headerValueIcon.iconName = ""
1407
- leadingActionIcon.visibility = GONE
1408
- leadingActionIcon.iconName = ""
1409
- leadingActionIcon.glyphSizeDp = 24
1410
- leadingActionIcon.setOnClickListener(null)
1411
- leadingActionIcon.setTag(com.facebook.react.R.id.react_test_id, null)
1412
- leadingActionIcon.contentDescription = null
1413
- mainColumn.visibility = VISIBLE
1414
- dataContainer.visibility = GONE
1415
- dataColumns.forEach { it.visibility = GONE }
1416
- tableDataContainer.visibility = GONE
1417
- tableDataColumns.forEach {
1418
- it.reset()
1419
- it.visibility = GONE
1420
- }
1421
- leadingOverlayBackground.visibility = GONE
1422
- leadingCornerIconFrame.visibility = GONE
1423
- leadingCornerIcon.iconName = ""
1424
- leadingImages.forEach {
1425
- it.visibility = GONE
1426
- it.clipToOutline = false
1427
- it.outlineProvider = ViewOutlineProvider.BACKGROUND
1428
- }
1429
- leadingFallback.text = ""
1430
- leadingFallback.textSize = sp(13f)
1431
- leadingFallback.typeface = NativeListFonts.bold(context)
1432
- leadingFallback.visibility = GONE
1433
- leadingFallback.background = null
1434
- showsSeparator = false
1435
- mainColumn.removeView(skeletonPrimary)
1436
- mainColumn.removeView(skeletonSecondary)
1437
- setOnClickListener { view ->
1438
- (view.tag as? NativeListItem)?.let { item ->
1439
- if (item.type == "market" && marketLongPressFired) {
1440
- marketLongPressFired = false
1441
- return@let
1442
- }
1443
- // OneKey patch: allow create-address accessories when whole-row press is gated.
1444
- if (item.isRowPressEnabled) onRowPress?.invoke(item, actionOrigin(view, "row"))
1445
- }
1446
- }
1447
- }
1448
-
1449
- private fun cancelMarketLongPress() {
1450
- marketLongPressRunnable?.let(marketLongPressHandler::removeCallbacks)
1451
- marketLongPressRunnable = null
1452
- }
1453
-
1454
- private fun restoreRestingBackground() {
1455
- background = if (reorderActive) pressedRowBackground else restingRowBackground
1456
- leadingFrame.alpha = 1f
1457
- }
1458
-
1459
- fun setReorderActive(active: Boolean) {
1460
- if ((tag as? NativeListItem)?.type == "walletGroup" && reorderActive == active) return
1461
- reorderActive = active
1462
- if ((tag as? NativeListItem)?.type == "walletGroup") {
1463
- walletGroupExpandAnimator?.removeAllListeners()
1464
- walletGroupExpandAnimator?.cancel()
1465
- walletGroupExpandAnimator = null
1466
- walletGroupRows.forEachIndexed { index, row ->
1467
- row.alpha = 1f
1468
- row.visibility = if (active && index > 0) GONE else VISIBLE
1469
- if (index == 0) row.setReorderActive(active)
1470
- }
1471
- minimumHeight = if (active) dp(68) else walletGroupExpandedHeightPx
1472
- layoutParams = layoutParams?.apply {
1473
- height = ViewGroup.LayoutParams.WRAP_CONTENT
1474
- }
1475
- background = restingRowBackground
1476
- requestLayout()
1477
- invalidate()
1478
- return
1479
- }
1480
- restoreRestingBackground()
1481
- }
1482
-
1483
- fun canStartWalletGroupReorder(localY: Float): Boolean {
1484
- if ((tag as? NativeListItem)?.type != "walletGroup") return true
1485
- walletGroupRows.forEach { row ->
1486
- if (localY >= row.top && localY < row.bottom) {
1487
- return (row.tag as? NativeListItem)?.json?.optBoolean("draggable", true) != false
1488
- }
1489
- }
1490
- return true
1491
- }
1492
-
1493
- fun finishWalletGroupReorder(
1494
- durationMs: Long,
1495
- interpolator: TimeInterpolator,
1496
- completion: (() -> Unit)? = null,
1497
- ) {
1498
- if ((tag as? NativeListItem)?.type != "walletGroup") {
1499
- setReorderActive(false)
1500
- completion?.invoke()
1501
- return
1502
- }
1503
- reorderActive = false
1504
- walletGroupRows.firstOrNull()?.setReorderActive(false)
1505
- val startHeight = height.coerceAtLeast(dp(68))
1506
- val targetHeight = walletGroupExpandedHeightPx
1507
- minimumHeight = startHeight
1508
- layoutParams = layoutParams?.apply { height = startHeight }
1509
- walletGroupRows.drop(1).forEach { row ->
1510
- row.visibility = VISIBLE
1511
- row.alpha = 0f
1512
- }
1513
- background = restingRowBackground
1514
- if (targetHeight <= startHeight) {
1515
- walletGroupRows.forEach { row ->
1516
- row.visibility = VISIBLE
1517
- row.alpha = 1f
1518
- }
1519
- minimumHeight = targetHeight
1520
- layoutParams = layoutParams?.apply { height = ViewGroup.LayoutParams.WRAP_CONTENT }
1521
- requestLayout()
1522
- invalidate()
1523
- completion?.invoke()
1524
- return
1525
- }
1526
- walletGroupExpandAnimator = ValueAnimator.ofInt(startHeight, targetHeight).apply {
1527
- duration = durationMs
1528
- this.interpolator = interpolator
1529
- addUpdateListener { animator ->
1530
- val progress = animator.animatedFraction
1531
- layoutParams = layoutParams?.apply { height = animator.animatedValue as Int }
1532
- walletGroupRows.drop(1).forEach { it.alpha = progress }
1533
- requestLayout()
1534
- invalidate()
1535
- }
1536
- addListener(object : AnimatorListenerAdapter() {
1537
- override fun onAnimationEnd(animation: Animator) {
1538
- walletGroupExpandAnimator = null
1539
- walletGroupRows.forEach { row ->
1540
- row.visibility = VISIBLE
1541
- row.alpha = 1f
1542
- }
1543
- minimumHeight = targetHeight
1544
- layoutParams = layoutParams?.apply { height = ViewGroup.LayoutParams.WRAP_CONTENT }
1545
- requestLayout()
1546
- invalidate()
1547
- postOnAnimation {
1548
- if (!reorderActive) {
1549
- walletGroupRows.forEach { row ->
1550
- row.visibility = VISIBLE
1551
- row.alpha = 1f
1552
- }
1553
- requestLayout()
1554
- invalidate()
1555
- }
1556
- }
1557
- completion?.invoke()
1558
- }
1559
- })
1560
- start()
1561
- }
1562
- }
1563
-
1564
- private fun bindWalletGroup(
1565
- item: NativeListItem,
1566
- theme: JSONObject?,
1567
- layout: String,
1568
- listOrientation: String,
1569
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1570
- ) {
1571
- orientation = VERTICAL
1572
- gravity = Gravity.CENTER_HORIZONTAL
1573
- setPadding(0, 0, 0, 0)
1574
- isClickable = false
1575
- setOnClickListener(null)
1576
- val members = mutableListOf<JSONObject>()
1577
- members.add(item.json.getJSONObject("parent"))
1578
- item.json.getJSONArray("children").let { children ->
1579
- for (index in 0 until children.length()) {
1580
- members.add(children.getJSONObject(index))
1581
- }
1582
- }
1583
- if (members.first().has("height")) setPadding(dp(1), dp(1), dp(1), dp(1))
1584
- val childCount = members.size - 1
1585
- walletGroupDragChildCount = members.drop(1).count {
1586
- it.optBoolean("draggable", true)
1587
- }
1588
- walletGroupExpandedHeightPx =
1589
- // OneKey patch: expanded groups include individual wallet badge heights.
1590
- // dp(members.size * 68 + walletGroupDragChildCount * 12)
1591
- dp(members.sumOf { it.optInt("height", if ((it.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68) } + childCount * 12 + if (members.first().has("height")) 2 else 0)
1592
- walletGroupDragBadgeBackgroundPaint.color = color(
1593
- theme,
1594
- "inverseBackground",
1595
- "#000000DF",
1596
- )
1597
- walletGroupDragBadgeBorderPaint.color = color(
1598
- theme,
1599
- "rowBackground",
1600
- "#FFFFFF",
1601
- )
1602
- walletGroupDragBadgeBorderPaint.strokeWidth = dp(1).toFloat()
1603
- walletGroupDragBadgeTextPaint.color = color(
1604
- theme,
1605
- "inverseText",
1606
- "#FCFCFC",
1607
- )
1608
- walletGroupDragBadgeTextPaint.textSize = TypedValue.applyDimension(
1609
- TypedValue.COMPLEX_UNIT_SP,
1610
- sp(12f),
1611
- resources.displayMetrics,
1612
- )
1613
- while (walletGroupRows.size < members.size) {
1614
- walletGroupRows.add(NativeListRowView(reactContext))
1615
- }
1616
- members.forEachIndexed { index, memberJson ->
1617
- val member = NativeListItem.parse(memberJson)
1618
- val memberRow = walletGroupRows[index]
1619
- memberRow.onRowPress = { item, origin -> onRowPress?.invoke(item, origin) }
1620
- memberRow.onAction = { source, actionKey, target, origin ->
1621
- onAction?.invoke(source, actionKey, target, origin)
1622
- }
1623
- memberRow.onBindingInvalidated = { row, epoch ->
1624
- onBindingInvalidated?.invoke(row, epoch)
1625
- }
1626
- memberRow.bind(
1627
- member,
1628
- theme,
1629
- layout,
1630
- listOrientation,
1631
- null,
1632
- memberJson.optBoolean("selected", false),
1633
- checkboxState,
1634
- useSourceScale = selectorUsesSourceScale,
1635
- )
1636
- // OneKey patch: member geometry matches the outer group height calculation.
1637
- // memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(68)).apply {
1638
- memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(memberJson.optInt("height", if ((memberJson.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68))).apply {
1639
- if (index > 0) topMargin = dp(12)
1640
- }
1641
- addView(memberRow)
1642
- }
1643
- val fill = color(theme, "subduedBackground", "#F9F9F9")
1644
- val stroke = color(theme, "separator", "#0000001F")
1645
- restingRowBackground = GradientDrawable().apply {
1646
- setColor(fill)
1647
- setStroke(dp(1), stroke)
1648
- cornerRadius = dp(12).toFloat()
1649
- }
1650
- pressedRowBackground = restingRowBackground
1651
- background = restingRowBackground
1652
- }
1653
-
1654
- private fun bindIdentity(
1655
- item: NativeListItem,
1656
- theme: JSONObject?,
1657
- selected: Boolean,
1658
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1659
- ) {
1660
- if (item.json.optString("presentation") == "walletSidebar") {
1661
- orientation = VERTICAL
1662
- gravity = Gravity.CENTER
1663
- setPadding(dp(4), dp(4), dp(4), dp(4))
1664
- addLeading(item.json.optJSONObject("leading"), 40, spacingDp = 0)
1665
- leadingFallback.textSize = sp(28f)
1666
- leadingFallback.typeface = NativeListFonts.regular(context)
1667
- mainColumn.gravity = Gravity.CENTER
1668
- titleLine.gravity = Gravity.CENTER
1669
- titleLine.packsChildrenAtStart = false
1670
- titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1671
- // OneKey patch: the original wallet title ellipsizes within the full inner row width.
1672
- title.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1673
- title.gravity = Gravity.CENTER
1674
- // OneKey patch: this branch returns before the common identity ellipsis setup.
1675
- if (item.json.has("height")) title.ellipsize = TextUtils.TruncateAt.END
1676
- showText(title, item.json.optString("title"), 1)
1677
- title.setTextColor(
1678
- color(
1679
- theme,
1680
- if (selected) "primaryText" else "secondaryText",
1681
- if (selected) "#FFFFFFED" else "#FFFFFFAF",
1682
- ),
1683
- )
1684
- addView(
1685
- mainColumn,
1686
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1687
- // OneKey patch: snap the source 4 + 40 + 4 sequence once instead of rounding each gap.
1688
- topMargin = if (item.json.has("height")) dp(48) - dp(44) else dp(4)
1689
- },
1690
- )
1691
- // OneKey patch: wallet tags are a centered line below the wallet name.
1692
- item.json.optJSONArray("badges")?.takeIf { it.length() > 0 }?.let { badges ->
1693
- val isSelector = item.json.has("height")
1694
- val badgeLineHeight = if (isSelector) 14 else 16
1695
- val badgeHeight = badgeLineHeight + 4
1696
- val line = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER }
1697
- for (index in 0 until badges.length()) {
1698
- val badge = TextView(context).apply {
1699
- text = badges.getJSONObject(index).optString("text")
1700
- textSize = sp(if (isSelector) 11f else 12f)
1701
- typeface = NativeListFonts.regular(context)
1702
- includeFontPadding = false
1703
- maxLines = 1
1704
- ellipsize = TextUtils.TruncateAt.END
1705
- val warning = isSelector && badges.getJSONObject(index).optString("tone") == "warning"
1706
- setTextColor(color(theme, if (warning) "caution" else "secondaryText", if (warning) "#AB6400" else "#0000009B"))
1707
- background = roundedFill(color(theme, if (warning) "cautionBackground" else if (isSelector) "subduedBackground" else "strongBackground", if (warning) "#FFF4D5" else "#00000006"), 4f)
1708
- setPadding(dp(if (isSelector) 6 else 4), dp(2), dp(if (isSelector) 6 else 4), dp(2))
1709
- }
1710
- TextViewCompat.setLineHeight(badge, dp(badgeLineHeight))
1711
- line.addView(badge, LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply { if (index > 0) marginStart = dp(4) })
1712
- }
1713
- mainColumn.addView(line, LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply { topMargin = dp(4) })
1714
- selectorViews.add(line)
1715
- }
1716
- return
1717
- }
1718
- if (item.json.optString("presentation") == "networkSelector") {
1719
- // OneKey patch: the explicit 48-point row centers its 32-point network icon.
1720
- // setPadding(dp(12), dp(7), dp(12), dp(8))
1721
- setPadding(dp(12), dp(if (item.json.has("height")) 8 else 7), dp(12), dp(8))
1722
- }
1723
- val leading = item.json.optJSONObject("leading")
1724
- item.json.optJSONObject("leadingAction")?.let { action ->
1725
- leadingActionIcon.visibility = VISIBLE
1726
- leadingActionIcon.iconName = action.optString("name")
1727
- leadingActionIcon.glyphSizeDp = 24
1728
- leadingActionIcon.tintColor = safeColor(
1729
- action.optString("tintColor"),
1730
- color(theme, "icon", "#0000009B"),
1731
- )
1732
- leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
1733
- leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
1734
- leadingActionIcon.setOnClickListener {
1735
- emitAction(
1736
- item,
1737
- action.optString("actionKey"),
1738
- null,
1739
- leadingActionIcon,
1740
- "leadingAction",
1741
- )
1742
- }
1743
- // ListItem.IconButton is 36dp with 6dp inner padding and m=-7. Keep the
1744
- // full button frame but absorb its leading negative margin into the row
1745
- // padding and its trailing margin into the following gap.
1746
- setPadding(dp(5), paddingTop, paddingRight, paddingBottom)
1747
- addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
1748
- }
1749
- addLeading(
1750
- leading,
1751
- if (
1752
- leading?.optString("kind") == "network" ||
1753
- item.json.optString("presentation") == "accountSelector"
1754
- ) 32 else 40,
1755
- )
1756
- // OneKey patch: custom network initials retain LetterAvatar typography.
1757
- if (item.json.optString("presentation") == "networkSelector" && leading?.optJSONObject("image") == null && leading?.optJSONObject("fallbackIcon") == null && !leading?.optString("fallbackText").isNullOrEmpty()) {
1758
- leadingFallback.textSize = sp(19f)
1759
- leadingFallback.typeface = NativeListFonts.semibold(context)
1760
- leadingFallback.setTextColor(color(theme, "inverseText", "#FCFCFC"))
1761
- TextViewCompat.setLineHeight(leadingFallback, dp(27))
1762
- }
1763
- addView(mainColumn, weighted())
1764
- titleLine.packsChildrenAtStart = true
1765
- title.ellipsize = TextUtils.TruncateAt.END
1766
- showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
1767
- if (item.json.optString("presentation") == "accountSelector") {
1768
- title.typeface = NativeListFonts.regular(context)
1769
- }
1770
- showText(subtitle, item.json.optString("subtitle"), item.json.optInt("subtitleLines", 2))
1771
- // OneKey patch: use separate labels for independently truncated balance/address.
1772
- item.json.optJSONArray("subtitleSegments")?.takeIf { it.length() > 0 }?.let { segments ->
1773
- subtitle.visibility = GONE
1774
- val line = SelectorSubtitleLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
1775
- for (index in 0 until segments.length()) {
1776
- val segment = segments.getJSONObject(index)
1777
- if (segment.optBoolean("separatorBefore", false)) {
1778
- val dot = View(context).apply { background = roundedFill(color(theme, "disabledText", "#00000072"), 2f) }
1779
- line.addView(dot, LayoutParams(dp(4), dp(4)).apply { marginStart = dp(6); marginEnd = dp(6) })
1780
- }
1781
- val label = TextView(context).apply {
1782
- text = segment.optString("text")
1783
- typeface = NativeListFonts.regular(context)
1784
- textSize = sp(14f)
1785
- includeFontPadding = false
1786
- maxLines = 1
1787
- ellipsize = TextUtils.TruncateAt.END
1788
- val toneKey = when (segment.optString("tone")) { "primary" -> "primaryText"; "disabled" -> "disabledText"; "caution" -> "caution"; "positive" -> "positive"; "negative" -> "negative"; else -> "secondaryText" }
1789
- setTextColor(color(theme, toneKey, if (toneKey == "caution") "#AB6400" else "#0000009B"))
1790
- }
1791
- TextViewCompat.setLineHeight(label, dp(20))
1792
- applyValueSegments(label, segment.optJSONArray("textSegments"), 14, 20, false)
1793
- line.addView(label, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
1794
- }
1795
- mainColumn.addView(line, 2, LayoutParams(LayoutParams.MATCH_PARENT, dp(20)))
1796
- selectorViews.add(line)
1797
- }
1798
- item.json.optJSONArray("titleMatch")?.takeIf { it.length() > 0 }?.let { matches ->
1799
- val highlighted = SpannableStringBuilder(title.text)
1800
- for (index in 0 until matches.length()) {
1801
- val match = matches.getJSONObject(index)
1802
- val start = match.optInt("start")
1803
- val end = match.optInt("end")
1804
- if (start >= 0 && end > start && end <= highlighted.length) highlighted.setSpan(ForegroundColorSpan(color(theme, "info", "#0D74CE")), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
1805
- }
1806
- title.text = highlighted
1807
- }
1808
- showText(tertiary, item.json.optString("tertiary"), 1)
1809
- tertiary.setTextColor(
1810
- color(
1811
- theme,
1812
- if (item.json.optString("tertiaryTone") == "info") "info" else "secondaryText",
1813
- if (item.json.optString("tertiaryTone") == "info") "#006DCBF2" else "#0000009B",
1814
- ),
1815
- )
1816
- val badges = item.json.optJSONArray("badges")
1817
- if (badges != null && badges.length() > 0) {
1818
- val texts = (0 until minOf(2, badges.length())).map { badges.getJSONObject(it).optString("text") }
1819
- showText(badgeLine, texts.joinToString(" "), 1)
1820
- badgeLine.layoutParams = wrap().apply { marginStart = dp(8) }
1821
- badgeLine.setTextColor(color(theme, "secondaryText", "#0000009B"))
1822
- badgeLine.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 4f)
1823
- badgeLine.setPadding(dp(8), dp(2), dp(8), dp(2))
1824
- TextViewCompat.setLineHeight(badgeLine, dp(16))
1825
- }
1826
- addView(trailingColumn, wrap())
1827
- val accessories = item.json.optJSONArray("trailing")
1828
- if (item.json.has("height") && item.json.optString("presentation") == "networkSelector" && accessories.hasAccessory("checkbox")) {
1829
- // OneKey patch: retain ListItem's title-to-accessory gap when measuring truncation.
1830
- (mainColumn.layoutParams as LayoutParams).marginEnd = dp(12)
1831
- }
1832
- if (accessories.hasAccessory("checkbox") && accessories.hasAccessory("value")) {
1833
- trailingColumn.orientation = HORIZONTAL
1834
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1835
- checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginStart = dp(12) }
1836
- } else if (accessories.isValuePairMenu()) {
1837
- trailingColumn.orientation = HORIZONTAL
1838
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1839
- } else if (accessories.isBookmarkEditActions()) {
1840
- trailingColumn.orientation = HORIZONTAL
1841
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1842
- }
1843
- bindAccessories(item, accessories, theme, checkboxState)
1844
- }
1845
-
1846
- private fun bindRail(item: NativeListItem, theme: JSONObject?) {
1847
- setPadding(dp(4), dp(4), dp(4), dp(4))
1848
- addLeading(item.json.optJSONObject("visual"), 20, spacingDp = 6)
1849
- showText(title, item.json.optString("title"), 1)
1850
- TextViewCompat.setLineHeight(title, dp(16))
1851
- title.fontFeatureSettings = "tnum"
1852
- title.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
1853
- titleLine.layoutParams = wrap()
1854
- mainColumn.orientation = HORIZONTAL
1855
- mainColumn.gravity = Gravity.CENTER_VERTICAL
1856
- mainColumn.layoutParams = wrap()
1857
- title.setHorizontallyScrolling(true)
1858
- item.json.optJSONObject("badge")?.let { badge ->
1859
- showText(badgeLine, badge.optString("text"), 1)
1860
- badgeLine.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
1861
- marginStart = dp(6)
1862
- }
1863
- badgeLine.setTextColor(
1864
- when (badge.optString("tone")) {
1865
- "success" -> color(theme, "positive", "#00713FDE")
1866
- "danger" -> color(theme, "negative", "#C40006D3")
1867
- else -> color(theme, "secondaryText", "#0000009B")
1868
- },
1869
- )
1870
- badgeLine.typeface = NativeListFonts.medium(context)
1871
- badgeLine.fontFeatureSettings = "tnum"
1872
- TextViewCompat.setLineHeight(badgeLine, dp(16))
1873
- }
1874
- item.json.optString("status").takeUnless { it.isEmpty() || it == "none" }
1875
- ?.let { showText(status, it, 1) }
1876
- addView(mainColumn, wrap())
1877
- }
1878
-
1879
- private fun bindActivity(item: NativeListItem, theme: JSONObject?) {
1880
- addLeading(
1881
- item.json.optJSONObject("leading"),
1882
- secondaryVisual = item.json.optJSONObject("secondaryLeading"),
1883
- )
1884
- addView(mainColumn, weighted())
1885
- showText(title, item.json.optString("title"), 1)
1886
- showText(subtitle, item.json.optString("description"), 2)
1887
- TextViewCompat.setLineHeight(title, dp(24))
1888
- TextViewCompat.setLineHeight(subtitle, dp(20))
1889
- if (item.json.optString("status") == "Failed") {
1890
- showText(badgeLine, "Failed", 1)
1891
- badgeLine.setTextColor(color(theme, "negative", "#C40006D3"))
1892
- badgeLine.background = roundedFill(
1893
- color(theme, "criticalBackground", "#F3000D14"),
1894
- 4f,
1895
- )
1896
- badgeLine.setPadding(dp(8), dp(2), dp(8), dp(2))
1897
- } else {
1898
- showText(status, item.json.optString("status"), 1)
1899
- }
1900
- addView(trailingColumn, wrap())
1901
- showTrailing(0, item.json.optString("primaryAmount"), true)
1902
- showTrailing(1, item.json.optString("secondaryAmount"), false)
1903
- if (item.json.optString("primaryAmount").startsWith("+")) {
1904
- trailingViews[0].setTextColor(color(theme, "positive", "#00713FDE"))
1905
- }
1906
- val actions = item.json.optJSONArray("footerActions")
1907
- if (actions != null && actions.length() > 0) {
1908
- actionLine.visibility = VISIBLE
1909
- for (index in 0 until minOf(3, actions.length())) {
1910
- val action = actions.getJSONObject(index)
1911
- val actionView = actionViews[index]
1912
- actionView.text = action.optString("label")
1913
- actionView.visibility = VISIBLE
1914
- actionView.isEnabled = isEnabled && !action.optBoolean("disabled", false)
1915
- actionView.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 8f)
1916
- actionView.setTextColor(
1917
- if (action.optString("tone") == "danger") {
1918
- color(theme, "negative", "#C40006D3")
1919
- } else {
1920
- color(theme, "primaryText", "#000000DF")
1921
- },
1922
- )
1923
- actionView.layoutParams = LayoutParams(
1924
- LayoutParams.WRAP_CONTENT,
1925
- LayoutParams.WRAP_CONTENT,
1926
- ).apply { marginEnd = dp(8) }
1927
- actionView.setOnClickListener {
1928
- emitAction(item, action.optString("key"), null, actionView, "footerAction", index)
1929
- }
1930
- }
1931
- // TxActionCommonListView is one column ListItem with an 8dp gap between
1932
- // its content XStack and the pending action footer.
1933
- removeView(leadingFrame)
1934
- removeView(mainColumn)
1935
- removeView(trailingColumn)
1936
- mainColumn.removeView(actionLine)
1937
- activityContentRow.orientation = HORIZONTAL
1938
- activityContentRow.gravity = Gravity.CENTER_VERTICAL
1939
- activityContentRow.addView(leadingFrame)
1940
- activityContentRow.addView(mainColumn)
1941
- activityContentRow.addView(trailingColumn)
1942
- orientation = VERTICAL
1943
- gravity = Gravity.START
1944
- addView(activityContentRow, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
1945
- addView(actionLine, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
1946
- marginStart = if (leadingFrame.visibility == VISIBLE) dp(52) else 0
1947
- topMargin = dp(8)
1948
- bottomMargin = dp(4)
1949
- })
1950
- }
1951
- }
1952
-
1953
- private fun bindMessage(item: NativeListItem, theme: JSONObject?) {
1954
- gravity = Gravity.TOP
1955
- setPadding(dp(12), dp(16), dp(12), dp(16))
1956
- item.json.optJSONObject("leading")?.let { addLeading(it, 28) }
1957
- unreadDot.visibility = if (item.json.optBoolean("unread", false)) VISIBLE else GONE
1958
- addView(mainColumn, weighted())
1959
- showText(title, item.json.optString("title"), 2)
1960
- showText(subtitle, item.json.optString("body"), item.json.optInt("bodyLines", 3).coerceIn(1, 3))
1961
- showText(status, item.json.optString("time"), 1)
1962
- TextViewCompat.setLineHeight(title, dp(20))
1963
- TextViewCompat.setLineHeight(subtitle, dp(20))
1964
- TextViewCompat.setLineHeight(status, dp(16))
1965
- subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1966
- topMargin = dp(2)
1967
- }
1968
- status.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1969
- topMargin = dp(4)
1970
- }
1971
- status.setTextColor(color(theme, "disabledText", "#00000072"))
1972
- item.json.optJSONObject("thumbnail")?.let { source ->
1973
- secondaryImage.visibility = VISIBLE
1974
- secondaryImage.foreground = roundedHairlineStroke(
1975
- color(theme, "strongBackground", "#0000000F"),
1976
- 6f,
1977
- )
1978
- addView(secondaryImage, LayoutParams(dp(64), dp(64)).apply { marginStart = dp(12) })
1979
- bindImage(source, secondaryImage, item.key, 0, "generic")
1980
- }
1981
- }
1982
-
1983
- private fun bindDataRow(
1984
- item: NativeListItem,
1985
- theme: JSONObject?,
1986
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1987
- ) {
1988
- if (
1989
- item.json.optBoolean("favorite", false) ||
1990
- item.json.optBoolean("favoriteActive", false)
1991
- ) {
1992
- val favoriteActive = item.json.optBoolean("favoriteActive", false)
1993
- favoriteIcon.visibility = VISIBLE
1994
- favoriteIcon.iconName = if (favoriteActive) "StarSolid" else "StarOutline"
1995
- favoriteIcon.tintColor = color(
1996
- theme,
1997
- if (favoriteActive) "icon" else "iconSubdued",
1998
- if (favoriteActive) "#0000009B" else "#00000072",
1999
- )
2000
- addView(favoriteIcon, LayoutParams(dp(20), dp(20)).apply {
2001
- marginEnd = dp(if (currentLayout == "table") 8 else 12)
2002
- })
2003
- }
2004
- item.json.optJSONObject("leading")?.let {
2005
- addLeading(it, 40, spacingDp = if (currentLayout == "table") 10 else 12)
2006
- }
2007
- var hasLeadingAccessory = false
2008
- item.json.optJSONObject("checkbox")?.let {
2009
- bindCheckbox(item, it, checkboxState)
2010
- hasLeadingAccessory = true
2011
- }
2012
- if (item.json.has("index")) {
2013
- val indexView = trailingViews[0]
2014
- indexView.text = item.json.optInt("index").toString()
2015
- indexView.visibility = VISIBLE
2016
- hasLeadingAccessory = true
2017
- }
2018
- if (hasLeadingAccessory) {
2019
- addView(trailingColumn, LayoutParams(dp(32), LayoutParams.WRAP_CONTENT).apply { marginEnd = dp(10) })
2020
- }
2021
- val columns = item.json.getJSONArray("columns")
2022
- val rowBadges = item.json.optJSONArray("badges")
2023
- if (currentLayout == "table") {
2024
- tableDataContainer.visibility = VISIBLE
2025
- for (index in 0 until minOf(4, columns.length())) {
2026
- val column = columns.getJSONObject(index)
2027
- tableDataColumns[index].visibility = VISIBLE
2028
- tableDataColumns[index].bind(
2029
- column = column,
2030
- rowBadges = if (index == 0) rowBadges else null,
2031
- primaryColor = dataTextColor(column.optString("tone"), theme),
2032
- secondaryColor = dataTextColor(
2033
- column.optString("secondaryTone", "secondary"),
2034
- theme,
2035
- ),
2036
- infoColor = color(theme, "info", "#006DCBF2"),
2037
- )
2038
- tableDataColumns[index].layoutParams = LayoutParams(
2039
- 0,
2040
- dp(40),
2041
- column.optInt("weight", 1).toFloat(),
2042
- )
2043
- }
2044
- addView(tableDataContainer, weighted())
2045
- return
2046
- }
2047
-
2048
- dataContainer.visibility = VISIBLE
2049
- for (index in 0 until minOf(4, columns.length())) {
2050
- val column = columns.getJSONObject(index)
2051
- val view = dataColumns[index]
2052
- view.text = styledDataText(column, if (index == 0) rowBadges else null, theme)
2053
- view.maxLines = if (column.optString("secondaryText").isEmpty()) 1 else 2
2054
- view.gravity = when (column.optString("alignment", "start")) {
2055
- "center" -> Gravity.CENTER
2056
- "end" -> Gravity.END
2057
- else -> Gravity.START
2058
- }
2059
- view.visibility = VISIBLE
2060
- view.setTextColor(
2061
- when (column.optString("tone", "primary")) {
2062
- "secondary" -> color(theme, "secondaryText", "#0000009B")
2063
- "positive" -> color(theme, "positive", "#00713FDE")
2064
- "negative" -> color(theme, "negative", "#C40006D3")
2065
- else -> color(theme, "primaryText", "#000000DF")
2066
- },
2067
- )
2068
- view.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, column.optInt("weight", 1).toFloat())
2069
- }
2070
- addView(dataContainer, weighted())
2071
- }
2072
-
2073
- private fun marketTypeface(weight: String, fallback: String): Typeface = when (
2074
- weight.ifEmpty { fallback }
2075
- ) {
2076
- "regular" -> NativeListFonts.regular(context)
2077
- "semibold" -> NativeListFonts.semibold(context)
2078
- "bold" -> NativeListFonts.bold(context)
2079
- else -> NativeListFonts.medium(context)
2080
- }
2081
-
2082
- private fun applyMarketTextStyle(
2083
- view: TextView,
2084
- style: JSONObject?,
2085
- defaultSize: Float,
2086
- defaultLineHeight: Int,
2087
- defaultWeight: String,
2088
- defaultColor: Int,
2089
- defaultAlignment: String,
2090
- ) {
2091
- view.includeFontPadding = false
2092
- view.fontFeatureSettings = "tnum"
2093
- view.textSize = sp(style?.optDouble("fontSize", defaultSize.toDouble())?.toFloat() ?: defaultSize)
2094
- view.typeface = marketTypeface(style?.optString("fontWeight").orEmpty(), defaultWeight)
2095
- view.setTextColor(safeColor(style?.optString("color"), defaultColor))
2096
- val alignment = style?.optString("alignment", defaultAlignment) ?: defaultAlignment
2097
- view.gravity = Gravity.CENTER_VERTICAL or when (alignment) {
2098
- "center" -> Gravity.CENTER_HORIZONTAL
2099
- "end" -> Gravity.END
2100
- else -> Gravity.START
2101
- }
2102
- view.maxLines = style?.optInt("lines", 1)?.coerceIn(1, 2) ?: 1
2103
- view.ellipsize = TextUtils.TruncateAt.END
2104
- TextViewCompat.setLineHeight(
2105
- view,
2106
- dp(style?.optDouble("lineHeight", defaultLineHeight.toDouble())?.roundToInt() ?: defaultLineHeight),
2107
- )
2108
- }
2109
-
2110
- // OneKey patch: opt-in Market text uses the same pixel rounding and line box as RN.
2111
- private fun applyMarketTextMetrics(view: TextView, style: JSONObject?) {
2112
- if (style == null || view.visibility != VISIBLE || view.text.isEmpty()) return
2113
- marketOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
2114
- view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
2115
- if (style.has("fontSize")) {
2116
- val sourceSize = sp(style.optDouble("fontSize").toFloat()) * resources.displayMetrics.density
2117
- view.setTextSize(TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
2118
- }
2119
- val text = SpannableStringBuilder(view.text)
2120
- text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
2121
- if (style.has("lineHeight")) {
2122
- val lineHeight = kotlin.math.ceil(style.optDouble("lineHeight") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toInt()
2123
- text.setSpan(SelectorLineHeightSpan(lineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2124
- view.setLineSpacing(0f, 1f)
2125
- }
2126
- view.text = text
2127
- }
2128
-
2129
- private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
2130
- if (segments == null || segments.length() == 0) return value
2131
- val result = SpannableStringBuilder()
2132
- for (index in 0 until segments.length()) {
2133
- val segment = segments.getJSONObject(index)
2134
- val start = result.length
2135
- result.append(segment.optString("text"))
2136
- if (segment.optString("style") == "subscript") {
2137
- result.setSpan(
2138
- AbsoluteSizeSpan(sp(kotlin.math.ceil(fontSize * 0.6f)).roundToInt(), true),
2139
- start,
2140
- result.length,
2141
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2142
- )
2143
- }
2144
- }
2145
- return result
2146
- }
2147
-
2148
- private fun bindMarket(item: NativeListItem, theme: JSONObject?) {
2149
- // Network badges extend beyond the leading image's frame.
2150
- clipChildren = false
2151
- val variant = item.json.optString("variant")
2152
- val style = item.json.optJSONObject("style")
2153
- val imageStyle = style?.optJSONObject("image")
2154
- val imageWidth = imageStyle?.optDouble("width", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
2155
- ?: if (variant == "stock") 40 else 32
2156
- val imageHeight = imageStyle?.optDouble("height", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
2157
- ?: if (variant == "stock") 40 else 32
2158
- val horizontalPadding = style?.optDouble("horizontalPadding", if (variant == "perp") 16.0 else 20.0)?.roundToInt()
2159
- ?: if (variant == "perp") 16 else 20
2160
- val verticalPadding = style?.optDouble("verticalPadding", 12.0)?.roundToInt() ?: 12
2161
- val leadingGap = style?.optDouble("leadingGap", if (variant == "perp") 8.0 else 14.0)?.roundToInt()
2162
- ?: if (variant == "perp") 8 else 14
2163
- setPadding(dp(horizontalPadding), dp(verticalPadding), dp(horizontalPadding), dp(verticalPadding))
2164
-
2165
- item.json.optJSONObject("leadingAction")?.let { action ->
2166
- leadingActionIcon.visibility = VISIBLE
2167
- leadingActionIcon.iconName = action.optString("name")
2168
- leadingActionIcon.glyphSizeDp = 24
2169
- leadingActionIcon.tintColor = safeColor(
2170
- action.optString("tintColor"),
2171
- color(theme, "icon", "#0000009B"),
2172
- )
2173
- leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
2174
- leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
2175
- leadingActionIcon.setTag(
2176
- com.facebook.react.R.id.react_test_id,
2177
- action.optString("testID").takeIf(String::isNotEmpty),
2178
- )
2179
- leadingActionIcon.contentDescription = action.optString("accessibilityLabel")
2180
- leadingActionIcon.setOnClickListener {
2181
- emitAction(
2182
- item,
2183
- action.optString("actionKey"),
2184
- null,
2185
- leadingActionIcon,
2186
- "leadingAction",
2187
- )
2188
- }
2189
- addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
2190
- }
2191
-
2192
- val leading = JSONObject(item.json.getJSONObject("leading").toString())
2193
- imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
2194
- imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
2195
- leading.optJSONObject("image")?.put("contentFit", contentFit)
2196
- }
2197
- val shape = imageStyle?.optString("shape", leading.optString("shape", "circle"))
2198
- ?: leading.optString("shape", "circle")
2199
- val cornerRadius = imageStyle?.takeIf { it.has("cornerRadius") }?.optDouble("cornerRadius")?.toFloat()
2200
- ?: when (shape) {
2201
- "square" -> 0f
2202
- "rounded" -> 8f
2203
- else -> minOf(imageWidth, imageHeight) / 2f
2204
- }
2205
- addLeading(
2206
- leading,
2207
- sizeDp = imageWidth,
2208
- spacingDp = leadingGap,
2209
- heightDp = imageHeight,
2210
- cornerRadiusDp = cornerRadius,
2211
- )
2212
-
2213
- // OneKey patch: retain the source gap without changing other templates.
2214
- // addView(mainColumn, weighted())
2215
- addView(mainColumn, weighted().apply {
2216
- marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
2217
- })
2218
- titleLine.packsChildrenAtStart = true
2219
- showText(title, item.json.optString("title"), style?.optJSONObject("title")?.optInt("lines", 1) ?: 1)
2220
- applyMarketTextStyle(
2221
- title,
2222
- style?.optJSONObject("title"),
2223
- 16f,
2224
- 24,
2225
- "medium",
2226
- color(theme, "primaryText", "#202020"),
2227
- "start",
2228
- )
2229
- val badges = item.json.optJSONArray("badges")
2230
- val titleBadgeGap = style?.optDouble("titleBadgeGap", 4.0)?.roundToInt() ?: 4
2231
- if (badges != null) {
2232
- for (index in 0 until minOf(marketBadgeViews.size, badges.length())) {
2233
- val badge = badges.getJSONObject(index)
2234
- val badgeView = marketBadgeViews[index]
2235
- val badgeLabel = marketBadgeLabels[index]
2236
- val badgeStyle = badge.optJSONObject("style")
2237
- val hasGlyph = badge.optString("iconName") == "verified"
2238
- val remoteIcon = badge.optJSONObject("icon")
2239
- val hasIcon = hasGlyph || remoteIcon != null
2240
- val text = badge.optString("text")
2241
- val toneColor = when (badge.optString("tone")) {
2242
- "success" -> color(theme, "positive", "#218358")
2243
- "danger" -> color(theme, "negative", "#CE2C31")
2244
- "info" -> color(theme, "info", "#0D74CE")
2245
- "warning" -> color(theme, "primaryText", "#202020")
2246
- else -> color(theme, "secondaryText", "#646464")
2247
- }
2248
- val foreground = safeColor(badge.optString("textColor"), toneColor)
2249
- badgeView.visibility = VISIBLE
2250
- val iconOnly = hasIcon && text.isEmpty()
2251
- // OneKey patch: Market callers can request the original badge padding.
2252
- val padding = badgeStyle?.optDouble("horizontalPadding", 5.0)?.roundToInt() ?: 5
2253
- val leftPadding = if (badgeStyle?.has("horizontalPadding") == true) padding else if (hasIcon) 2 else 5
2254
- // badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2255
- badgeView.setPadding(dp(if (iconOnly) 0 else leftPadding), 0, dp(if (iconOnly) 0 else padding), 0)
2256
- badgeView.background = roundedFill(
2257
- safeColor(
2258
- badge.optString("backgroundColor"),
2259
- if (hasIcon && text.isEmpty()) Color.TRANSPARENT else color(theme, "strongBackground", "#0000000F"),
2260
- ),
2261
- 4f,
2262
- ).apply {
2263
- if (badgeStyle != null) this.cornerRadius = 4f * resources.displayMetrics.density
2264
- }
2265
- badgeLabel.text = text
2266
- // OneKey patch: match SizableText's tabular numerals only for explicit Market metrics.
2267
- badgeLabel.fontFeatureSettings = if (badgeStyle != null) "tnum" else null
2268
- badgeLabel.textSize = sp(badgeStyle?.optDouble("fontSize", 11.0)?.toFloat() ?: 11f)
2269
- badgeLabel.typeface = marketTypeface(badgeStyle?.optString("fontWeight").orEmpty(), "medium")
2270
- if (badgeStyle?.has("lineHeight") == true) {
2271
- TextViewCompat.setLineHeight(badgeLabel, dp(badgeStyle.optDouble("lineHeight").roundToInt()))
2272
- }
2273
- badgeLabel.setTextColor(foreground)
2274
- badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
2275
- if (hasGlyph) {
2276
- marketBadgeGlyphs[index].apply {
2277
- iconName = "BadgeVerifiedSolid"
2278
- tintColor = foreground
2279
- visibility = VISIBLE
2280
- }
2281
- }
2282
- remoteIcon?.let { icon ->
2283
- marketBadgeImages[index].visibility = VISIBLE
2284
- marketBadgeImages[index].outlineProvider = circleOutlineProvider
2285
- marketBadgeImages[index].clipToOutline = true
2286
- bindImage(icon, marketBadgeImages[index], item.key, 20 + index, "generic")
2287
- }
2288
- val actionKey = badge.optString("actionKey")
2289
- badgeView.isClickable = actionKey.isNotEmpty()
2290
- if (actionKey.isNotEmpty()) {
2291
- badgeView.setOnClickListener {
2292
- emitAction(item, actionKey, null, badgeView, "marketBadge", index)
2293
- }
2294
- }
2295
- badgeView.contentDescription = badge.optString("accessibilityLabel", text)
2296
- titleLine.addView(
2297
- badgeView,
2298
- // OneKey patch: preserve 18dp unless the Market row opts into source metrics.
2299
- // LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2300
- LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeStyle?.optDouble("height", 18.0)?.roundToInt() ?: 18)).apply { marginStart = dp(titleBadgeGap) },
2301
- )
2302
- }
2303
- }
2304
- subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2305
- topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2306
- }
2307
- val subtitleText = item.json.optString("subtitle")
2308
- val subtitleSegments = item.json.optJSONArray("subtitleSegments")
2309
- if (subtitleText.isNotEmpty() || (subtitleSegments?.length() ?: 0) > 0) {
2310
- subtitle.visibility = VISIBLE
2311
- applyMarketTextStyle(
2312
- subtitle,
2313
- style?.optJSONObject("subtitle"),
2314
- 14f,
2315
- 20,
2316
- "regular",
2317
- color(theme, "secondaryText", "#646464"),
2318
- "start",
2319
- )
2320
- subtitle.text = marketText(
2321
- subtitleText,
2322
- subtitleSegments,
2323
- style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2324
- )
2325
- }
2326
- // OneKey patch: preserve independent name metrics, truncation, and volume.
2327
- val subtitlePrefix = item.json.optJSONObject("subtitlePrefix")
2328
- val subtitlePadding = style?.optDouble("subtitleTrailingPadding", 0.0)?.roundToInt() ?: 0
2329
- if (subtitlePrefix != null || subtitlePadding > 0) {
2330
- mainColumn.removeView(subtitle)
2331
- mainColumn.removeView(tertiary)
2332
- marketSubtitleLine.orientation = HORIZONTAL
2333
- marketSubtitleLine.gravity = Gravity.CENTER_VERTICAL
2334
- marketSubtitleLine.packsChildrenAtStart = true
2335
- marketSubtitleLine.leadingTextMaxWidth = subtitlePrefix?.takeIf { it.has("maxWidth") }
2336
- ?.optDouble("maxWidth")?.roundToInt()?.let(::dp) ?: Int.MAX_VALUE
2337
- marketSubtitleLine.setPadding(0, 0, dp(subtitlePadding), 0)
2338
- showText(tertiary, subtitlePrefix?.optString("text") ?: "", 1)
2339
- applyMarketTextStyle(tertiary, subtitlePrefix?.optJSONObject("style"), 12f, 16, "regular", color(theme, "secondaryText", "#646464"), "start")
2340
- marketSubtitleLine.addView(tertiary, wrap())
2341
- marketSubtitleLine.addView(subtitle, wrap().apply {
2342
- if (tertiary.visibility == VISIBLE && subtitle.visibility == VISIBLE) {
2343
- marginStart = dp(subtitlePrefix?.optDouble("gap", 4.0)?.roundToInt() ?: 4)
2344
- }
2345
- })
2346
- mainColumn.addView(marketSubtitleLine, 1, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2347
- topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2348
- })
2349
- marketSubtitleLine.visibility = if (tertiary.visibility == VISIBLE || subtitle.visibility == VISIBLE) VISIBLE else GONE
2350
- }
2351
- trailingColumn.orientation = HORIZONTAL
2352
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2353
- addView(trailingColumn, wrap())
2354
- bindMarketQuote(item, theme)
2355
- item.json.optJSONObject("diagnostics")?.optString("imageBindActionKey")
2356
- ?.takeIf(String::isNotEmpty)
2357
- ?.takeIf { leading.optJSONObject("image") != null || leading.optJSONObject("networkImage") != null }
2358
- ?.let { actionKey -> onAction?.invoke(item, actionKey, null, null) }
2359
- }
2360
-
2361
- fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {
2362
- if (boundKey != item.key || item.type != "market") return
2363
- tag = item
2364
- val style = item.json.optJSONObject("style")
2365
- val priceStyle = style?.optJSONObject("price")
2366
- val price = trailingViews[0]
2367
- price.isClickable = false
2368
- price.isLongClickable = false
2369
- price.visibility = VISIBLE
2370
- price.maxWidth = dp(112)
2371
- applyMarketTextStyle(
2372
- price,
2373
- priceStyle,
2374
- 16f,
2375
- 24,
2376
- "medium",
2377
- color(theme, "primaryText", "#202020"),
2378
- "end",
2379
- )
2380
- price.text = marketText(
2381
- item.json.optString("price"),
2382
- item.json.optJSONArray("priceSegments"),
2383
- priceStyle?.optDouble("fontSize", 16.0)?.toFloat() ?: 16f,
2384
- )
2385
- val trailingGap = style?.optDouble("trailingGap", 8.0)?.roundToInt() ?: 8
2386
- price.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
2387
- marginEnd = dp(trailingGap)
2388
- }
2389
-
2390
- val changeData = item.json.getJSONObject("change")
2391
- val changeStyle = style?.optJSONObject("change")
2392
- val change = trailingViews[1]
2393
- change.isClickable = false
2394
- change.isLongClickable = false
2395
- val toneColor = when (changeData.optString("tone")) {
2396
- "positive" -> color(theme, "positive", "#218358")
2397
- "negative" -> color(theme, "negative", "#CE2C31")
2398
- else -> color(theme, "secondaryText", "#8D8D8D")
2399
- }
2400
- val textColor = safeColor(
2401
- changeData.optString("textColor"),
2402
- color(theme, "inverseText", "#FFFFFF"),
2403
- )
2404
- change.visibility = VISIBLE
2405
- applyMarketTextStyle(change, changeStyle, 14f, 20, "medium", textColor, "center")
2406
- change.text = marketText(
2407
- changeData.optString("text"),
2408
- changeData.optJSONArray("textSegments"),
2409
- changeStyle?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2410
- )
2411
- change.background = roundedFill(
2412
- safeColor(changeData.optString("backgroundColor"), toneColor),
2413
- style?.optDouble("changeCornerRadius", 8.0)?.toFloat() ?: 8f,
2414
- )
2415
- change.layoutParams = LayoutParams(
2416
- dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
2417
- dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
2418
- )
2419
- applyMarketTextMetrics(title, style?.optJSONObject("title"))
2420
- applyMarketTextMetrics(subtitle, style?.optJSONObject("subtitle"))
2421
- applyMarketTextMetrics(tertiary, item.json.optJSONObject("subtitlePrefix")?.optJSONObject("style"))
2422
- applyMarketTextMetrics(price, priceStyle)
2423
- applyMarketTextMetrics(change, changeStyle)
2424
- val badges = item.json.optJSONArray("badges")
2425
- marketBadgeLabels.forEachIndexed { index, label ->
2426
- val badge = badges?.optJSONObject(index)
2427
- val badgeStyle = badge?.optJSONObject("style")
2428
- applyMarketTextMetrics(label, badgeStyle)
2429
- if (badge != null && label.visibility == VISIBLE && badgeStyle?.has("horizontalPadding") == true &&
2430
- badge.optJSONObject("icon") == null && badge.optString("iconName").isEmpty()) {
2431
- // OneKey patch: match RN's intrinsic text width and one rounded padding pair.
2432
- label.layoutParams = wrap().apply { width = label.paint.measureText(label.text.toString()).roundToInt() }
2433
- val padding = badgeStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
2434
- marketBadgeViews[index].setPadding(kotlin.math.floor(padding).toInt(), 0, (padding * 2).roundToInt() - kotlin.math.floor(padding).toInt(), 0)
2435
- }
2436
- }
2437
- contentDescription = item.json.optString(
2438
- "accessibilityLabel",
2439
- listOf(
2440
- item.json.optString("title"),
2441
- item.json.optString("subtitle"),
2442
- item.json.optString("price"),
2443
- changeData.optString("text"),
2444
- ).filter(String::isNotEmpty).joinToString(", "),
2445
- )
2446
- }
2447
-
2448
- private fun styledDataText(
2449
- column: JSONObject,
2450
- badges: JSONArray?,
2451
- theme: JSONObject?,
2452
- ): CharSequence {
2453
- val result = SpannableStringBuilder(column.optString("text"))
2454
- result.setSpan(
2455
- ForegroundColorSpan(dataTextColor(column.optString("tone"), theme)),
2456
- 0,
2457
- result.length,
2458
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2459
- )
2460
- if (badges != null) {
2461
- for (index in 0 until minOf(2, badges.length())) {
2462
- val start = result.length
2463
- result.append(" ${badges.getJSONObject(index).optString("text")} ")
2464
- result.setSpan(
2465
- ForegroundColorSpan(color(theme, "info", "#006DCBF2")),
2466
- start,
2467
- result.length,
2468
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2469
- )
2470
- result.setSpan(
2471
- BackgroundColorSpan(parseNativeListColor("#008FF519")),
2472
- start,
2473
- result.length,
2474
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2475
- )
2476
- }
2477
- }
2478
- column.optString("secondaryText").takeIf(String::isNotEmpty)?.let { secondary ->
2479
- val start = result.length
2480
- result.append("\n$secondary")
2481
- result.setSpan(
2482
- ForegroundColorSpan(dataTextColor(column.optString("secondaryTone", "secondary"), theme)),
2483
- start,
2484
- result.length,
2485
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2486
- )
2487
- }
2488
- return result
2489
- }
2490
-
2491
- private fun dataTextColor(tone: String, theme: JSONObject?): Int = when (tone.ifEmpty { "primary" }) {
2492
- "secondary" -> color(theme, "secondaryText", "#0000009B")
2493
- "positive" -> color(theme, "positive", "#00713FDE")
2494
- "negative" -> color(theme, "negative", "#C40006D3")
2495
- else -> color(theme, "primaryText", "#000000DF")
2496
- }
2497
-
2498
- private fun bindMediaTile(item: NativeListItem, theme: JSONObject?) {
2499
- isMediaTile = true
2500
- orientation = VERTICAL
2501
- gravity = Gravity.START
2502
- setPadding(dp(10), dp(10), dp(10), dp(10))
2503
- val imageState = item.json.optString("imageState", "loaded")
2504
- when (imageState) {
2505
- "empty" -> {
2506
- addLeading(null, 160)
2507
- leadingFrame.background = roundedFill(Color.WHITE, 10f)
2508
- }
2509
- "error" -> {
2510
- addLeading(null, 160)
2511
- leadingFallback.visibility = VISIBLE
2512
- leadingFallback.background = roundedFill(
2513
- color(theme, "strongBackground", "#0000000F"),
2514
- 10f,
2515
- )
2516
- leadingIcon.visibility = VISIBLE
2517
- leadingIcon.iconName = "ImageSquareWavesOutline"
2518
- leadingIcon.tintColor = parseNativeListColor("#00000044")
2519
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
2520
- }
2521
- else -> {
2522
- addLeading(
2523
- JSONObject().put("kind", "image").put("image", item.json.getJSONObject("image")),
2524
- 160,
2525
- )
2526
- }
2527
- }
2528
- leadingFrame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(160))
2529
- leadingFallback.layoutParams = FrameLayout.LayoutParams(
2530
- LayoutParams.MATCH_PARENT,
2531
- LayoutParams.MATCH_PARENT,
2532
- )
2533
- leadingImages[0].layoutParams = FrameLayout.LayoutParams(
2534
- LayoutParams.MATCH_PARENT,
2535
- LayoutParams.MATCH_PARENT,
2536
- )
2537
- leadingImages[0].outlineProvider = object : ViewOutlineProvider() {
2538
- override fun getOutline(view: View, outline: Outline) {
2539
- outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
2540
- }
2541
- }
2542
- leadingImages[0].clipToOutline = true
2543
- mainColumn.removeView(titleLine)
2544
- mainColumn.removeView(subtitle)
2545
- mediaMetadataRow.orientation = HORIZONTAL
2546
- mediaMetadataRow.gravity = Gravity.CENTER_VERTICAL
2547
- mediaMetadataRow.addView(subtitle, weighted().apply { marginEnd = dp(8) })
2548
- item.json.optJSONObject("networkImage")?.let { networkImage ->
2549
- // OneKey patch: Preserve badge layout without exposing a loading/error tile.
2550
- // mediaNetworkImage.visibility = VISIBLE
2551
- mediaNetworkImage.outlineProvider = circleOutlineProvider
2552
- mediaNetworkImage.clipToOutline = true
2553
- mediaMetadataRow.addView(mediaNetworkImage, LayoutParams(dp(14), dp(14)))
2554
- bindImage(networkImage, mediaNetworkImage, item.key, 2, "network", hideUntilLoaded = true)
2555
- }
2556
- mainColumn.addView(mediaMetadataRow, 0)
2557
- mainColumn.addView(titleLine, 1)
2558
- showText(subtitle, item.json.optString("subtitle"), 1)
2559
- showText(title, item.json.optString("title"), 1)
2560
- item.json.optJSONObject("badge")?.optString("text")?.let { value ->
2561
- mediaBadge.text = value
2562
- mediaBadge.setTextColor(parseNativeListColor("#FCFCFC"))
2563
- mediaBadge.textSize = sp(14f)
2564
- mediaBadge.background = roundedStroke(
2565
- Color.WHITE,
2566
- parseNativeListColor("#000000DF"),
2567
- 10f,
2568
- )
2569
- mediaBadge.setPadding(dp(8), 0, dp(8), 0)
2570
- mediaBadge.layoutParams = FrameLayout.LayoutParams(
2571
- LayoutParams.WRAP_CONTENT,
2572
- dp(24),
2573
- Gravity.END or Gravity.BOTTOM,
2574
- )
2575
- mediaBadge.visibility = VISIBLE
2576
- }
2577
- addView(mainColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2578
- topMargin = dp(8)
2579
- })
2580
- item.json.optString("closeActionKey").takeIf { it.isNotEmpty() }?.let { actionKey ->
2581
- val close = trailingViews[0]
2582
- close.text = "×"
2583
- close.textSize = sp(22f)
2584
- close.gravity = Gravity.END
2585
- close.visibility = VISIBLE
2586
- close.setOnClickListener {
2587
- emitAction(item, actionKey, null, close, "mediaClose")
2588
- }
2589
- addView(trailingColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
2590
- }
2591
- }
2592
-
2593
- private fun bindMetricCard(item: NativeListItem, theme: JSONObject?) {
2594
- orientation = VERTICAL
2595
- gravity = Gravity.START
2596
- setPadding(dp(14), dp(14), dp(14), dp(14))
2597
- val variant = item.json.optString("variant", "standard")
2598
- if (variant == "activity" || variant == "performance") {
2599
- bindCompositeMetricCard(item, theme, variant)
2600
- return
2601
- }
2602
- item.json.optJSONObject("visual")?.let {
2603
- addLeading(it, 32)
2604
- leadingFrame.clipChildren = true
2605
- leadingFrame.clipToPadding = true
2606
- }
2607
- showText(subtitle, item.json.optString("title"), 1)
2608
- subtitle.setTextColor(color(theme, "disabledText", "#00000072"))
2609
- showText(title, item.json.optString("value"), 1)
2610
- title.textSize = sp(if (item.json.optString("size") == "large") 24f else 18f)
2611
- title.typeface = NativeListFonts.semibold(context)
2612
- showText(status, item.json.optString("trend"), 1)
2613
- status.setTextColor(
2614
- when (item.json.optString("trendTone", "neutral")) {
2615
- "positive" -> color(theme, "positive", "#00713FDE")
2616
- "negative" -> color(theme, "negative", "#C40006D3")
2617
- else -> color(theme, "secondaryText", "#0000009B")
2618
- },
2619
- )
2620
- showText(metricSubtitle, item.json.optString("subtitle"), 1)
2621
- item.json.optJSONObject("badge")?.optString("text")?.let { showText(badgeLine, it, 1) }
2622
- addView(mainColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
2623
- }
2624
-
2625
- private fun bindCompositeMetricCard(item: NativeListItem, theme: JSONObject?, variant: String) {
2626
- val metrics = item.json.optJSONArray("metrics") ?: JSONArray()
2627
- addView(
2628
- metricText(
2629
- value = item.json.optString("title").uppercase(),
2630
- size = 11f,
2631
- lineHeight = 14,
2632
- typeface = NativeListFonts.regular(context),
2633
- textColor = color(theme, "disabledText", "#00000072"),
2634
- letterSpacingDp = 1.2f,
2635
- ),
2636
- weightedWidth(),
2637
- )
2638
- if (variant == "activity") {
2639
- addView(activityHeroMetrics(metrics, theme), weightedWidth().apply {
2640
- topMargin = dp(14)
2641
- })
2642
- addView(View(context).apply {
2643
- setBackgroundColor(color(theme, "separator", "#0000001F"))
2644
- }, LayoutParams(LayoutParams.MATCH_PARENT, 1).apply {
2645
- topMargin = dp(14)
2646
- bottomMargin = dp(14)
2647
- })
2648
- addView(activityCompactMetrics(metrics, theme), weightedWidth())
2649
- } else {
2650
- val winRateRow = LinearLayout(context).apply {
2651
- orientation = HORIZONTAL
2652
- gravity = Gravity.BOTTOM
2653
- if (metrics.length() > 0) {
2654
- addView(
2655
- makeMetricColumn(
2656
- metric = metrics.getJSONObject(0),
2657
- visualSlot = 0,
2658
- theme = theme,
2659
- alignment = Gravity.START,
2660
- valueSize = 18f,
2661
- valueLineHeight = 24,
2662
- valueTypeface = NativeListFonts.semibold(context),
2663
- ),
2664
- LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
2665
- )
2666
- }
2667
- if (metrics.length() > 1) {
2668
- addView(
2669
- makeMetricColumn(
2670
- metric = metrics.getJSONObject(1),
2671
- visualSlot = 1,
2672
- theme = theme,
2673
- alignment = Gravity.END,
2674
- valueSize = 14f,
2675
- valueLineHeight = 20,
2676
- valueTypeface = NativeListFonts.semibold(context),
2677
- ),
2678
- LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
2679
- )
2680
- }
2681
- }
2682
- addView(winRateRow, weightedWidth().apply { topMargin = dp(14) })
2683
- val progressValue = item.json.optDouble("progress", 0.0).coerceIn(0.0, 1.0)
2684
- val progress = LinearLayout(context).apply {
2685
- orientation = HORIZONTAL
2686
- clipToOutline = true
2687
- outlineProvider = object : ViewOutlineProvider() {
2688
- override fun getOutline(view: View, outline: Outline) {
2689
- outline.setRoundRect(0, 0, view.width, view.height, dp(2).toFloat())
2690
- }
2691
- }
2692
- if (progressValue > 0.0) {
2693
- addView(View(context).apply {
2694
- setBackgroundColor(parseNativeListColor("#22AB15"))
2695
- }, LayoutParams(0, LayoutParams.MATCH_PARENT, progressValue.toFloat()))
2696
- }
2697
- if (progressValue < 1.0) {
2698
- addView(View(context).apply {
2699
- setBackgroundColor(parseNativeListColor("#E5484D"))
2700
- }, LayoutParams(0, LayoutParams.MATCH_PARENT, (1.0 - progressValue).toFloat()))
2701
- }
2702
- }
2703
- addView(progress, LayoutParams(LayoutParams.MATCH_PARENT, dp(4)).apply {
2704
- topMargin = dp(8)
2705
- })
2706
- addView(performanceAverageMetrics(metrics, theme), weightedWidth().apply {
2707
- topMargin = dp(14)
2708
- })
2709
- }
2710
- }
2711
-
2712
- private fun activityHeroMetrics(metrics: JSONArray, theme: JSONObject?) =
2713
- LinearLayout(context).apply {
2714
- orientation = HORIZONTAL
2715
- gravity = Gravity.BOTTOM
2716
- if (metrics.length() > 0) {
2717
- addView(
2718
- makeMetricColumn(
2719
- metric = metrics.getJSONObject(0),
2720
- visualSlot = 0,
2721
- theme = theme,
2722
- alignment = Gravity.START,
2723
- valueSize = 16f,
2724
- valueLineHeight = 24,
2725
- valueTypeface = NativeListFonts.semibold(context),
2726
- ),
2727
- LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
2728
- )
2729
- }
2730
- if (metrics.length() > 1) {
2731
- addView(
2732
- makeMetricColumn(
2733
- metric = metrics.getJSONObject(1),
2734
- visualSlot = 1,
2735
- theme = theme,
2736
- alignment = Gravity.END,
2737
- valueSize = 14f,
2738
- valueLineHeight = 20,
2739
- valueTypeface = NativeListFonts.semibold(context),
2740
- ),
2741
- LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
2742
- )
2743
- }
2744
- }
2745
-
2746
- private fun activityCompactMetrics(metrics: JSONArray, theme: JSONObject?) =
2747
- LinearLayout(context).apply {
2748
- orientation = HORIZONTAL
2749
- gravity = Gravity.CENTER_VERTICAL
2750
- for (index in 2 until metrics.length()) {
2751
- val alignment = when (index) {
2752
- 2 -> Gravity.START
2753
- metrics.length() - 1 -> Gravity.END
2754
- else -> Gravity.CENTER_HORIZONTAL
2755
- }
2756
- addView(
2757
- makeMetricColumn(
2758
- metric = metrics.getJSONObject(index),
2759
- visualSlot = index,
2760
- theme = theme,
2761
- alignment = alignment,
2762
- valueSize = 14f,
2763
- valueLineHeight = 20,
2764
- valueTypeface = NativeListFonts.medium(context),
2765
- ),
2766
- LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
2767
- )
2768
- }
2769
- }
2770
-
2771
- private fun performanceAverageMetrics(metrics: JSONArray, theme: JSONObject?) =
2772
- LinearLayout(context).apply {
2773
- orientation = HORIZONTAL
2774
- val indices = listOf(2, metrics.length() - 1).distinct().filter { it in 0 until metrics.length() }
2775
- indices.forEachIndexed { position, index ->
2776
- val metric = metrics.getJSONObject(index)
2777
- val card = makeMetricColumn(
2778
- metric = metric,
2779
- visualSlot = index,
2780
- theme = theme,
2781
- alignment = Gravity.START,
2782
- valueSize = 14f,
2783
- valueLineHeight = 20,
2784
- valueTypeface = NativeListFonts.medium(context),
2785
- ).apply {
2786
- setPadding(dp(10), dp(10), dp(10), dp(10))
2787
- background = roundedFill(color(theme, "strongBackground", "#0000000F"), 8f)
2788
- }
2789
- addView(card, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f).apply {
2790
- if (position > 0) marginStart = dp(8)
2791
- })
2792
- }
2793
- }
2794
-
2795
- private fun makeMetricColumn(
2796
- metric: JSONObject,
2797
- visualSlot: Int,
2798
- theme: JSONObject?,
2799
- alignment: Int,
2800
- valueSize: Float,
2801
- valueLineHeight: Int,
2802
- valueTypeface: Typeface,
2803
- ) = LinearLayout(context).apply {
2804
- orientation = VERTICAL
2805
- gravity = alignment
2806
- addView(
2807
- metricText(
2808
- value = metric.optString("label"),
2809
- size = 11f,
2810
- lineHeight = 14,
2811
- typeface = NativeListFonts.regular(context),
2812
- textColor = color(theme, "disabledText", "#00000072"),
2813
- gravity = alignment,
2814
- ),
2815
- )
2816
- val valueText = metricText(
2817
- value = metric.optString("value"),
2818
- size = valueSize,
2819
- lineHeight = valueLineHeight,
2820
- typeface = valueTypeface,
2821
- textColor = dataTextColor(metric.optString("tone"), theme),
2822
- gravity = alignment,
2823
- )
2824
- val valueVisual = metric.optJSONObject("visual")
2825
- val valueView = if (valueVisual != null && visualSlot in metricVisualImages.indices) {
2826
- LinearLayout(context).apply {
2827
- orientation = HORIZONTAL
2828
- gravity = Gravity.CENTER_VERTICAL
2829
- val image = metricVisualImages[visualSlot]
2830
- image.visibility = VISIBLE
2831
- image.outlineProvider = circleOutlineProvider
2832
- image.clipToOutline = true
2833
- valueVisual.optString("backgroundColor").takeIf(String::isNotEmpty)?.let { backgroundColor ->
2834
- image.background = roundedFill(
2835
- safeColor(backgroundColor, Color.WHITE),
2836
- 8f,
2837
- )
2838
- }
2839
- visualSources(valueVisual).firstOrNull()?.let { (source, variant) ->
2840
- bindImage(source, image, boundKey ?: "metric", visualSlot, variant)
2841
- }
2842
- addView(image, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
2843
- addView(valueText, wrap())
2844
- }
2845
- } else {
2846
- valueText
2847
- }
2848
- addView(
2849
- valueView,
2850
- LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
2851
- topMargin = dp(2)
2852
- },
2853
- )
2854
- }
2855
-
2856
- private fun metricText(
2857
- value: String,
2858
- size: Float,
2859
- lineHeight: Int,
2860
- typeface: Typeface,
2861
- textColor: Int,
2862
- letterSpacingDp: Float = 0f,
2863
- gravity: Int = Gravity.START,
2864
- ) = TextView(context).apply {
2865
- includeFontPadding = false
2866
- text = value
2867
- textSize = sp(size)
2868
- this.typeface = typeface
2869
- setTextColor(textColor)
2870
- this.gravity = gravity
2871
- maxLines = 1
2872
- ellipsize = TextUtils.TruncateAt.END
2873
- fontFeatureSettings = "tnum"
2874
- TextViewCompat.setLineHeight(this, dp(lineHeight))
2875
- if (letterSpacingDp > 0f) {
2876
- letterSpacing = letterSpacingDp / sp(size)
2877
- }
2878
- }
2879
-
2880
- private fun weightedWidth() = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
2881
-
2882
- private fun bindSectionHeader(
2883
- item: NativeListItem,
2884
- theme: JSONObject?,
2885
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
2886
- ) {
2887
- val variant = item.json.optString("variant")
2888
- val isSummary = variant == "summary"
2889
- val isGallery = variant == "gallery"
2890
- val isTable = currentLayout == "table"
2891
- // OneKey patch: title help emits its own frame instead of toggling the section.
2892
- item.json.optString("titleActionKey").takeIf { it.isNotEmpty() }?.let { key ->
2893
- title.setOnClickListener { emitAction(item, key, null, title, "leadingAction") }
2894
- }
2895
- val isNetworkSelector = item.json.optString("presentation") == "networkSelector"
2896
- val isHistory = variant == "history" || item.sectionKey?.startsWith("history-") == true
2897
- val isTokenManager = item.sectionKey in setOf("linear-tokens", "action-tokens")
2898
- val isExplicitNetworkHeader = isNetworkSelector && item.json.has("height")
2899
- val hasDottedTitle = isSummary || (isNetworkSelector && (!isExplicitNetworkHeader || item.json.optString("titleActionKey").isNotEmpty())) ||
2900
- (item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null)
2901
- // Linear/sectioned snapshots reserve the ListItem mx=8 at RecyclerView
2902
- // level, so header-local insets below are source px minus that outer inset.
2903
- val headerHorizontalInset = 12
2904
- addView(mainColumn, weighted())
2905
- showText(title, item.json.optString("title"), 1)
2906
- if (hasDottedTitle) {
2907
- title.showsDottedUnderline = true
2908
- title.dottedUnderlineColor = color(theme, "secondaryText", "#0000009B")
2909
- title.setPadding(0, 0, 0, dp(3))
2910
- }
2911
- title.setTextColor(
2912
- color(
2913
- theme,
2914
- if (isSummary || isGallery) "primaryText" else "secondaryText",
2915
- if (isSummary || isGallery) "#000000DF" else "#0000009B",
2916
- ),
2917
- )
2918
- if (isNetworkSelector) {
2919
- val verticalInset = if (isExplicitNetworkHeader && item.json.optString("titleActionKey").isEmpty()) 8 else 12
2920
- setPadding(dp(headerHorizontalInset), dp(verticalInset), dp(headerHorizontalInset), dp(verticalInset))
2921
- title.textSize = sp(14f)
2922
- title.typeface = NativeListFonts.medium(context)
2923
- TextViewCompat.setLineHeight(title, dp(20))
2924
- } else if (isHistory) {
2925
- setPadding(0, 0, 0, 0)
2926
- title.text = item.json.optString("title").uppercase()
2927
- title.textSize = sp(12f)
2928
- title.typeface = NativeListFonts.semibold(context)
2929
- title.letterSpacing = 0.8f / sp(12f)
2930
- TextViewCompat.setLineHeight(title, dp(16))
2931
- } else if (isTokenManager) {
2932
- setPadding(dp(headerHorizontalInset), dp(10), dp(headerHorizontalInset), 0)
2933
- title.textSize = sp(14f)
2934
- title.typeface = NativeListFonts.regular(context)
2935
- TextViewCompat.setLineHeight(title, dp(20))
2936
- } else if (!isTable && !isGallery && !isSummary) {
2937
- // SectionList.SectionHeader: h=36, px=20, headingSm 14/20 semibold.
2938
- setPadding(dp(headerHorizontalInset), dp(8), dp(headerHorizontalInset), dp(8))
2939
- TextViewCompat.setLineHeight(title, dp(20))
2940
- } else if (isTable) {
2941
- setPadding(dp(16), dp(12), dp(16), dp(2))
2942
- title.includeFontPadding = false
2943
- title.layoutParams = wrap()
2944
- titleLine.layoutParams = wrap()
2945
- item.json.optJSONObject("titleIcon")?.let { icon ->
2946
- headerTitleIcon.visibility = VISIBLE
2947
- headerTitleIcon.iconName = icon.optString("name")
2948
- headerTitleIcon.tintColor = safeColor(
2949
- icon.optString("tintColor"),
2950
- color(theme, "iconSubdued", "#00000072"),
2951
- )
2952
- titleLine.addView(headerTitleIcon, LayoutParams(dp(12), dp(12)).apply {
2953
- marginStart = dp(4)
2954
- })
2955
- if (icon.optString("name") != "ChevronGrabberVerOutline") {
2956
- title.setTextColor(color(theme, "primaryText", "#000000DF"))
2957
- }
2958
- }
2959
- }
2960
- showText(subtitle, item.json.optString("subtitle"), 1)
2961
- if (isGallery) {
2962
- setPadding(dp(12), 0, dp(12), dp(8))
2963
- } else if (isSummary) {
2964
- addView(trailingColumn, wrap())
2965
- // NetworkListHeader: outer mt=16/pb=12, inner XStack px=20/py=8.
2966
- setPadding(dp(headerHorizontalInset), dp(24), dp(headerHorizontalInset), dp(20))
2967
- showTrailing(
2968
- 0,
2969
- item.json.optString("value"),
2970
- true,
2971
- item.json.optString("valueActionKey"),
2972
- color(theme, "secondaryText", "#0000009B"),
2973
- )
2974
- trailingViews[0].setTag(com.facebook.react.R.id.react_test_id, item.json.optString("valueActionTestID").takeIf { it.isNotEmpty() })
2975
- trailingViews[0].accessibilityDelegate = selectorAccessibilityDelegate
2976
- trailingViews[0].textSize = sp(16f)
2977
- trailingViews[0].typeface = NativeListFonts.medium(context)
2978
- TextViewCompat.setLineHeight(trailingViews[0], dp(24))
2979
- // OneKey patch: the migrated media button shares the original 24-point text box.
2980
- if (isExplicitNetworkHeader) trailingViews[0].setPadding(0, 0, 0, 0)
2981
- else trailingViews[0].setPadding(dp(14), dp(6), dp(14), dp(6))
2982
- trailingViews[0].layoutParams = wrap()
2983
- } else if (!isGallery && !isHistory && !isTokenManager) {
2984
- val value = item.json.optString("value")
2985
- val checkboxData = item.json.optJSONObject("checkbox")
2986
- if (isExplicitNetworkHeader && checkboxData != null) {
2987
- // OneKey patch: the asset section title reserves its original 8dp trailing margin.
2988
- (mainColumn.layoutParams as LayoutParams).marginEnd = dp(8)
2989
- }
2990
- if (checkboxData != null && value.isNotEmpty()) {
2991
- // Value and checkbox share the trailing edge as one compound accessory.
2992
- trailingColumn.orientation = HORIZONTAL
2993
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2994
- trailingViews[0].layoutParams = wrap()
2995
- checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginStart = dp(12) }
2996
- }
2997
- addView(trailingColumn, wrap())
2998
- showTrailing(0, value, true)
2999
- if (isTable) {
3000
- trailingColumn.orientation = HORIZONTAL
3001
- trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
3002
- trailingViews[0].includeFontPadding = false
3003
- trailingViews[0].textSize = sp(11f)
3004
- trailingViews[0].typeface = NativeListFonts.regular(context)
3005
- trailingViews[0].setTextColor(color(theme, "secondaryText", "#0000009B"))
3006
- item.json.optJSONObject("valueIcon")?.let { icon ->
3007
- headerValueIcon.visibility = VISIBLE
3008
- headerValueIcon.iconName = icon.optString("name")
3009
- headerValueIcon.tintColor = safeColor(
3010
- icon.optString("tintColor"),
3011
- color(theme, "iconSubdued", "#00000072"),
3012
- )
3013
- trailingColumn.addView(
3014
- headerValueIcon,
3015
- LayoutParams(dp(12), dp(12)).apply { marginStart = dp(4) },
3016
- )
3017
- if (icon.optString("name") != "ChevronGrabberVerOutline") {
3018
- trailingViews[0].setTextColor(color(theme, "primaryText", "#000000DF"))
3019
- }
3020
- }
3021
- }
3022
- checkboxData?.let { bindCheckbox(item, it, checkboxState) }
3023
- }
3024
- applyValueSegments(trailingViews[0], item.json.optJSONArray("valueSegments"))
3025
- }
3026
-
3027
- // OneKey patch: preserve compact zero-count digits without changing their baseline.
3028
- private fun applyValueSegments(view: TextView, segments: JSONArray?, fontSize: Int = 16, lineHeight: Int = 24, medium: Boolean = true) {
3029
- if (segments == null || segments.length() == 0) return
3030
- val value = SpannableStringBuilder()
3031
- for (index in 0 until segments.length()) {
3032
- val segment = segments.getJSONObject(index)
3033
- val start = value.length
3034
- value.append(segment.optString("text"))
3035
- if (segment.optString("style") == "subscript") {
3036
- val size = kotlin.math.ceil(fontSize * 0.6).toFloat()
3037
- val span = if (selectorUsesSourceScale) AbsoluteSizeSpan(kotlin.math.ceil((size * resources.displayMetrics.density).toDouble()).toInt(), false) else AbsoluteSizeSpan(sp(size).roundToInt(), true)
3038
- value.setSpan(span, start, value.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
3039
- }
3040
- }
3041
- view.textSize = sp(fontSize.toFloat())
3042
- view.typeface = if (medium) NativeListFonts.medium(context) else NativeListFonts.regular(context)
3043
- TextViewCompat.setLineHeight(view, dp(lineHeight))
3044
- view.text = value
3045
- }
3046
-
3047
- private fun bindAction(
3048
- item: NativeListItem,
3049
- theme: JSONObject?,
3050
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
3051
- ) {
3052
- val isAccountSelector = item.json.optString("presentation") == "accountSelector"
3053
- item.json.optJSONObject("icon")?.let { icon ->
3054
- addLeading(icon, if (isAccountSelector) 32 else 40)
3055
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
3056
- if (!icon.has("backgroundColor")) leadingFrame.background = null
3057
- if (isAccountSelector) leadingFrame.background = roundedFill(safeColor(icon.optString("backgroundColor"), color(theme, "strongBackground", "#0000000F")), 8f)
3058
- }
3059
- addView(mainColumn, weighted())
3060
- showText(title, item.json.optString("title"), 1)
3061
- if (isAccountSelector) {
3062
- title.typeface = if (item.json.has("icon")) NativeListFonts.medium(context) else NativeListFonts.regular(context)
3063
- title.setTextColor(color(theme, if (item.json.optString("tone") == "primary") "primaryText" else "secondaryText", "#0000009B"))
3064
- } else if (item.json.optString("tone") == "danger") {
3065
- title.setTextColor(color(theme, "negative", "#C40006D3"))
3066
- }
3067
- item.json.optJSONObject("checkbox")?.let {
3068
- bindCheckbox(item, it, checkboxState)
3069
- addView(trailingColumn, wrap())
3070
- } ?: item.json.optJSONArray("trailing")?.let { accessories ->
3071
- if (accessories.length() > 0) {
3072
- addView(trailingColumn, wrap())
3073
- bindAccessories(item, accessories, theme, checkboxState)
3074
- }
3075
- }
3076
- setOnClickListener {
3077
- onRowPress?.invoke(item, actionOrigin(this, "row"))
3078
- }
3079
- }
3080
-
3081
- private fun bindSystem(item: NativeListItem, theme: JSONObject?) {
3082
- val variant = item.json.optString("variant")
3083
- val isMarket = item.json.optString("presentation") == "market"
3084
- if (isMarket) setPadding(dp(20), dp(12), dp(20), dp(12))
3085
- if (variant == "loading" && item.json.optString("loadingStyle") == "skeleton") {
3086
- setPadding(dp(20), dp(12), dp(20), dp(12))
3087
- addView(
3088
- NativeListMarketSkeleton(context, color(theme, "background", "#FFFFFF")),
3089
- LayoutParams(LayoutParams.MATCH_PARENT, dp(32)),
3090
- )
3091
- return
3092
- }
3093
- if (variant == "loading" && item.json.optString("loadingStyle") == "spinner") {
3094
- gravity = Gravity.CENTER
3095
- setPadding(0, dp(16), 0, dp(16))
3096
- addView(
3097
- ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
3098
- isIndeterminate = true
3099
- indeterminateTintList = android.content.res.ColorStateList.valueOf(color(theme, "icon", "#0000009B"))
3100
- },
3101
- LayoutParams(dp(20), dp(20)),
3102
- )
3103
- return
3104
- }
3105
- if (isMarket && variant == "retry") {
3106
- val message = item.json.optString("message")
3107
- orientation = VERTICAL
3108
- gravity = Gravity.CENTER
3109
- setPadding(dp(32), dp(if (message.isEmpty()) 11 else 32), dp(32), dp(if (message.isEmpty()) 11 else 27))
3110
- if (message.isNotEmpty()) {
3111
- showText(title, message, 2)
3112
- val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3113
- applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3114
- applyMarketTextMetrics(title, style)
3115
- addView(mainColumn, wrap().apply { bottomMargin = kotlin.math.ceil(7.0 * resources.displayMetrics.density).toInt() })
3116
- }
3117
- showTrailing(0, item.json.optString("actionText", "Retry"), true, item.json.optString("actionKey"))
3118
- val button = trailingViews[0]
3119
- val style = JSONObject().put("fontSize", 14).put("lineHeight", 20)
3120
- applyMarketTextStyle(button, style, 14f, 20, "medium", color(theme, "secondaryText", "#0000009B"), "center")
3121
- applyMarketTextMetrics(button, style)
3122
- button.background = null
3123
- // Match Yoga's text rounding before adding the tertiary Button's border/padding.
3124
- val density = resources.displayMetrics.density
3125
- val buttonWidth = (kotlin.math.ceil(button.paint.measureText(button.text.toString()).toDouble()) + 18 * density).roundToInt()
3126
- val buttonHeight = kotlin.math.ceil(kotlin.math.ceil(20.0 * density) + 10 * density).toInt()
3127
- button.setPadding(0, 0, 0, 0)
3128
- button.layoutParams = LayoutParams(buttonWidth, buttonHeight)
3129
- addView(trailingColumn, wrap())
3130
- return
3131
- }
3132
- if (isMarket && variant == "noMatch") {
3133
- gravity = Gravity.CENTER
3134
- setPadding(dp(32), dp(32), dp(32), dp(32))
3135
- showText(title, item.json.optString("message"), 1)
3136
- val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3137
- applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3138
- applyMarketTextMetrics(title, style)
3139
- addView(mainColumn, wrap())
3140
- return
3141
- }
3142
- // OneKey patch: warning title/description wrap inside the actual scroll content.
3143
- if (variant == "warning") {
3144
- setPadding(dp(12), dp(14), dp(12), dp(14))
3145
- addView(mainColumn, weighted())
3146
- showText(title, item.json.optString("title"), Int.MAX_VALUE)
3147
- showText(subtitle, item.json.optString("message"), Int.MAX_VALUE)
3148
- title.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
3149
- subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { topMargin = dp(4) }
3150
- titleLine.packsChildrenAtStart = false
3151
- return
3152
- }
3153
- if (variant == "spacer") {
3154
- minimumHeight = dp(item.json.optInt("height", 0))
3155
- return
3156
- }
3157
- if (variant == "end") {
3158
- // components/ListEndIndicator: py=16, gap=8, 80x1 lines and a 4dp dot.
3159
- gravity = Gravity.CENTER
3160
- setPadding(0, dp(16), 0, dp(16))
3161
- val indicatorColor = color(theme, "separator", "#0000001F")
3162
- addView(View(context).apply { setBackgroundColor(indicatorColor) }, LayoutParams(dp(80), 1))
3163
- addView(
3164
- View(context).apply { background = roundedFill(indicatorColor, 2f) },
3165
- LayoutParams(dp(4), dp(4)).apply {
3166
- marginStart = dp(8)
3167
- marginEnd = dp(8)
3168
- },
3169
- )
3170
- addView(View(context).apply { setBackgroundColor(indicatorColor) }, LayoutParams(dp(80), 1))
3171
- return
3172
- }
3173
- gravity = Gravity.CENTER
3174
- if (variant == "loading") {
3175
- addLeading(JSONObject().put("kind", "skeleton"), if (isMarket) 32 else 40)
3176
- leadingFallback.text = ""
3177
- leadingFallback.background = roundedFill(
3178
- color(theme, "strongBackground", "#0000000F"),
3179
- if (isMarket) 16f else 20f,
3180
- )
3181
- addSkeleton(skeletonPrimary, 120, 12, theme, bottomMarginDp = 8)
3182
- addSkeleton(skeletonSecondary, 80, 12, theme)
3183
- }
3184
- showText(title, item.json.optString("message"), 2)
3185
- title.gravity = if (variant == "retry" || variant == "noMatch") {
3186
- Gravity.START
3187
- } else {
3188
- Gravity.CENTER
3189
- }
3190
- mainColumn.gravity = Gravity.CENTER_VERTICAL
3191
- if (variant == "noMatch") {
3192
- setPadding(dp(12), 0, dp(12), 0)
3193
- title.setTextColor(color(theme, "secondaryText", "#0000009B"))
3194
- } else if (variant == "retry") {
3195
- setPadding(dp(12), dp(8), dp(12), dp(8))
3196
- title.setTextColor(color(theme, "secondaryText", "#0000009B"))
3197
- }
3198
- addView(
3199
- mainColumn,
3200
- if (variant == "retry" || variant == "noMatch") weighted() else wrap(),
3201
- )
3202
- if (variant == "retry") {
3203
- setOnClickListener {
3204
- onRowPress?.invoke(item, actionOrigin(this, "row"))
3205
- }
3206
- showTrailing(0, "Retry", true, item.json.optString("actionKey"))
3207
- trailingViews[0].textSize = sp(14f)
3208
- trailingViews[0].background = roundedFill(color(theme, "strongBackground", "#0000000F"), 16f)
3209
- trailingViews[0].setPadding(dp(10), dp(4), dp(10), dp(4))
3210
- TextViewCompat.setLineHeight(trailingViews[0], dp(20))
3211
- addView(trailingColumn, wrap().apply { marginStart = dp(12) })
3212
- }
3213
- }
3214
-
3215
- private fun addLeading(
3216
- visual: JSONObject?,
3217
- sizeDp: Int = 40,
3218
- secondaryVisual: JSONObject? = null,
3219
- spacingDp: Int = 12,
3220
- heightDp: Int = sizeDp,
3221
- cornerRadiusDp: Float? = null,
3222
- ) {
3223
- leadingFrame.visibility = VISIBLE
3224
- leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(heightDp)).apply {
3225
- val item = tag as? NativeListItem
3226
- // OneKey patch: Yoga rounds cumulative selector edges, not each 12dp gap separately.
3227
- marginEnd = if (item?.json?.has("height") == true && item.json.optString("presentation") in setOf("accountSelector", "networkSelector")) dp(12 + sizeDp + spacingDp) - dp(12) - dp(sizeDp) else dp(spacingDp)
3228
- }
3229
- addView(leadingFrame)
3230
- leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
3231
- if (visual == null) return
3232
- val kind = visual.optString("kind")
3233
- val shape = visual.optString(
3234
- "shape",
3235
- if (kind == "image" || isMediaTile) "rounded" else "circle",
3236
- )
3237
- val sources = visualSources(visual).toMutableList()
3238
- secondaryVisual?.let { sources.addAll(visualSources(it).take(1)) }
3239
- val isIcon = kind == "icon"
3240
- val cornerIconData = visual.optJSONObject("cornerIcon")
3241
- val fallback = visual.optString("fallbackText").take(2)
3242
- val fallbackIconData = visual.optJSONObject("fallbackIcon")
3243
- val fallbackIconSizeDp = fallbackIconData?.let {
3244
- val slotSizeDp = minOf(sizeDp, heightDp)
3245
- if (it.optString("name") == "GlobusOutline") {
3246
- (slotSizeDp * 1.2f).roundToInt()
3247
- } else {
3248
- slotSizeDp
3249
- }
3250
- }
3251
- val sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
3252
- val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
3253
- val handlesSourceFallback =
3254
- !isIcon && sources.isNotEmpty() &&
3255
- showsSourcePlaceholder &&
3256
- (visual.has("fallbackText") || fallbackIconData != null)
3257
- val sourceFallbackKey = if (handlesSourceFallback) {
3258
- sources.firstOrNull()?.first?.let(::sourceFallbackStateKey)
3259
- } else null
3260
- val restoresSourceFallback =
3261
- sourceFallbackKey?.let(NativeListSourceFallbackState::has) == true
3262
- leadingFallback.text =
3263
- if (handlesSourceFallback && !restoresSourceFallback) "" else fallback
3264
- leadingFallback.setTextColor(parseNativeListColor("#00000072"))
3265
- val visualBackground = safeColor(
3266
- visual.optString("backgroundColor"),
3267
- // OneKey patch: source-backed visuals default to no placeholder/background.
3268
- if (!isIcon && sources.isNotEmpty()) Color.TRANSPARENT else parseNativeListColor(imagePlaceholderColor),
3269
- )
3270
- if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
3271
- leadingFrame.background = roundedFill(
3272
- visualBackground,
3273
- cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
3274
- )
3275
- }
3276
- leadingFallback.background = roundedFill(
3277
- if (handlesSourceFallback) parseNativeListColor(imagePlaceholderColor) else visualBackground,
3278
- cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
3279
- )
3280
- leadingFallback.visibility =
3281
- if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
3282
- if (handlesSourceFallback && fallbackIconData != null) {
3283
- leadingIcon.iconName = fallbackIconData.optString("name")
3284
- leadingIcon.tintColor = safeColor(
3285
- fallbackIconData.optString("tintColor"),
3286
- parseNativeListColor("#0000009B"),
3287
- )
3288
- leadingIcon.layoutParams = FrameLayout.LayoutParams(
3289
- dp(fallbackIconSizeDp ?: sizeDp),
3290
- dp(fallbackIconSizeDp ?: heightDp),
3291
- Gravity.CENTER,
3292
- )
3293
- leadingIcon.glyphSizeDp = fallbackIconSizeDp
3294
- // Keep the fallback measured while the source loads so an asynchronous
3295
- // failure can reveal it without waiting for another RecyclerView layout.
3296
- leadingFallback.visibility = if (restoresSourceFallback) GONE else VISIBLE
3297
- leadingIcon.visibility = if (restoresSourceFallback) VISIBLE else INVISIBLE
3298
- }
3299
- if (isIcon) {
3300
- leadingFrame.background = GradientDrawable().apply {
3301
- setColor(visualBackground)
3302
- setStroke(1, parseNativeListColor("#0000001F"))
3303
- cornerRadius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
3304
- }
3305
- leadingIcon.iconName = visual.optString("name")
3306
- leadingIcon.tintColor = safeColor(
3307
- visual.optString("tintColor"),
3308
- parseNativeListColor("#0000009B"),
3309
- )
3310
- leadingIcon.visibility = VISIBLE
3311
- } else if (sources.isEmpty() && fallbackIconData != null) {
3312
- leadingFallback.visibility = GONE
3313
- leadingIcon.iconName = fallbackIconData.optString("name")
3314
- leadingIcon.tintColor = safeColor(
3315
- fallbackIconData.optString("tintColor"),
3316
- parseNativeListColor("#0000009B"),
3317
- )
3318
- leadingIcon.layoutParams = FrameLayout.LayoutParams(
3319
- dp(fallbackIconSizeDp ?: sizeDp),
3320
- dp(fallbackIconSizeDp ?: heightDp),
3321
- Gravity.CENTER,
3322
- )
3323
- leadingIcon.glyphSizeDp = fallbackIconSizeDp
3324
- leadingIcon.visibility = VISIBLE
3325
- }
3326
- val visibleSources = sources.take(leadingImages.size)
3327
- val tokenPair = kind == "token" && visibleSources.size > 1
3328
- val hasAccountSelectorBadge =
3329
- kind == "account" &&
3330
- (tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" &&
3331
- (visual.optJSONArray("overlays")?.length() ?: 0) > 0
3332
- if (tokenPair || hasAccountSelectorBadge) {
3333
- // Network badges intentionally extend past the avatar frame.
3334
- clipChildren = false
3335
- }
3336
- if (tokenPair) {
3337
- leadingOverlayBackground.visibility = VISIBLE
3338
- leadingOverlayBackground.background = roundedFill(visualBackdropColor, 10f)
3339
- val item = tag as? NativeListItem
3340
- val marketPadding = if (item?.type == "market") item.json.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0 else null
3341
- val density = resources.displayMetrics.density
3342
- // Yoga rounds the absolute horizontal edges, including the avatar's fractional origin.
3343
- val badgeRight = marketPadding?.let { ((it + sizeDp + 4) * density).roundToInt() }
3344
- val badgeLeft = marketPadding?.let { ((it + sizeDp - 16) * density).roundToInt() }
3345
- val avatarRight = marketPadding?.let { ((it + sizeDp) * density).roundToInt() }
3346
- leadingOverlayBackground.layoutParams = FrameLayout.LayoutParams(
3347
- if (badgeRight != null && badgeLeft != null) badgeRight - badgeLeft else dp(20),
3348
- dp(20),
3349
- Gravity.END or Gravity.BOTTOM,
3350
- ).apply {
3351
- marginEnd = if (badgeRight != null && avatarRight != null) avatarRight - badgeRight else -dp(4)
3352
- bottomMargin = -dp(4)
3353
- }
3354
- }
3355
- if (cornerIconData != null) {
3356
- leadingCornerIconFrame.visibility = VISIBLE
3357
- leadingCornerIconFrame.background = roundedFill(
3358
- safeColor(cornerIconData.optString("backgroundColor"), visualBackdropColor),
3359
- 10f,
3360
- )
3361
- leadingCornerIconFrame.layoutParams = FrameLayout.LayoutParams(
3362
- dp(20),
3363
- dp(20),
3364
- Gravity.END or Gravity.BOTTOM,
3365
- ).apply {
3366
- marginEnd = -dp(4)
3367
- bottomMargin = -dp(4)
3368
- }
3369
- leadingCornerIcon.iconName = cornerIconData.optString("name")
3370
- leadingCornerIcon.tintColor = safeColor(
3371
- cornerIconData.optString("tintColor"),
3372
- parseNativeListColor("#0000009B"),
3373
- )
3374
- }
3375
- visibleSources.forEachIndexed { index, (source, variant) ->
3376
- val image = leadingImages[index]
3377
- // OneKey patch: Decorative badges and source-owned fallbacks stay hidden until loaded.
3378
- // image.visibility = VISIBLE
3379
- val ownsSourceFallback = index == 0 && handlesSourceFallback
3380
- image.visibility = if (index > 0 || ownsSourceFallback) INVISIBLE else VISIBLE
3381
- image.layoutParams = leadingImageLayout(
3382
- index = index,
3383
- count = visibleSources.size,
3384
- sizeDp = sizeDp,
3385
- heightDp = heightDp,
3386
- tokenPair = tokenPair,
3387
- )
3388
- image.outlineProvider = when {
3389
- tokenPair && index == 1 -> circleOutlineProvider
3390
- cornerRadiusDp != null -> roundedOutlineProvider(cornerRadiusDp)
3391
- else -> leadingOutlineProvider(shape)
3392
- }
3393
- image.clipToOutline = true
3394
- if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3395
- val inset = dp(1)
3396
- val density = resources.displayMetrics.density
3397
- val sourceRadius = cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp))
3398
- // OneKey patch: retain the source's fractional border and background rendering.
3399
- leadingFrame.background = null
3400
- BackgroundStyleApplicator.setBackgroundColor(leadingFrame, visualBackground)
3401
- // Match the source's physical edges: React Native rasterizes ALL as a different stroke path.
3402
- for (edge in arrayOf(LogicalEdge.LEFT, LogicalEdge.TOP, LogicalEdge.RIGHT, LogicalEdge.BOTTOM)) {
3403
- BackgroundStyleApplicator.setBorderWidth(leadingFrame, edge, 1f)
3404
- BackgroundStyleApplicator.setBorderColor(leadingFrame, edge, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3405
- }
3406
- BackgroundStyleApplicator.setBorderRadius(leadingFrame, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(sourceRadius, LengthPercentageType.POINT))
3407
- // OneKey patch: round the source image's absolute edges inside its border.
3408
- val sourceLeft = ((tag as? NativeListItem)?.json?.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0) * density
3409
- val imageWidth = (sourceLeft + (sizeDp - 1) * density).roundToInt() - (sourceLeft + density).roundToInt()
3410
- image.layoutParams = FrameLayout.LayoutParams(imageWidth, dp(heightDp - 2)).apply {
3411
- leftMargin = inset
3412
- topMargin = inset
3413
- }
3414
- marketLeadingUsesSourceClip = true
3415
- image.clipToOutline = false
3416
- }
3417
- val fallbackIcon = if (index == 0) fallbackIconData else null
3418
- val expectedEpoch = bindingEpoch
3419
- bindImage(source, image, boundKey ?: "", index, variant,
3420
- round = shape == "circle",
3421
- onLoad = if (!ownsSourceFallback) null else ({
3422
- if (bindingEpoch == expectedEpoch) {
3423
- sourceFallbackKey?.let(NativeListSourceFallbackState::forget)
3424
- image.visibility = VISIBLE
3425
- leadingFallback.visibility = GONE
3426
- leadingIcon.visibility = GONE
3427
- }
3428
- }),
3429
- onError = if (!ownsSourceFallback) null else ({
3430
- if (bindingEpoch == expectedEpoch) {
3431
- sourceFallbackKey?.let(NativeListSourceFallbackState::remember)
3432
- image.visibility = GONE
3433
- if (fallbackIcon != null) {
3434
- leadingFallback.visibility = GONE
3435
- leadingIcon.iconName = fallbackIcon.optString("name")
3436
- leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
3437
- leadingIcon.visibility = VISIBLE
3438
- } else {
3439
- leadingIcon.visibility = GONE
3440
- leadingFallback.text = fallback
3441
- leadingFallback.visibility = VISIBLE
3442
- }
3443
- }
3444
- }),
3445
- hideUntilLoaded = index > 0 || ownsSourceFallback,
3446
- )
3447
- }
3448
- // OneKey patch: wallet overlays retain source images, provider colors and QR text.
3449
- visual.optJSONArray("overlays")?.let { overlays ->
3450
- for (index in 0 until overlays.length()) {
3451
- val overlay = overlays.getJSONObject(index)
3452
- val size = overlay.optInt("size", 20)
3453
- val inset = dp(overlay.optInt("padding", 0))
3454
- val isWalletText = selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && overlay.optString("text").isNotEmpty() && overlay.optJSONObject("image") == null && overlay.optString("name").isEmpty()
3455
- val width = overlay.optInt("width", size)
3456
- val height = overlay.optInt("height", if (isWalletText) 16 else size)
3457
- val offsetX = dp(overlay.optInt("offsetX", overlay.optInt("offset", 2)))
3458
- val offsetY = dp(overlay.optInt("offsetY", overlay.optInt("offset", 2)))
3459
- val frame = FrameLayout(context).apply {
3460
- setPadding(if (isWalletText) dp(2) else inset, if (isWalletText) 0 else inset, if (isWalletText) dp(2) else inset, if (isWalletText) 0 else inset)
3461
- background = roundedFill(safeColor(overlay.optString("backgroundColor"), Color.TRANSPARENT), minOf(width, height) / 2f)
3462
- outlineProvider = ViewOutlineProvider.BACKGROUND
3463
- clipToOutline = true
3464
- }
3465
- val image = overlay.optJSONObject("image")
3466
- val view = when {
3467
- image != null -> (selectorImages.getOrNull(index) ?: OneKeyImageReusableView(reactContext).also(selectorImages::add)).also {
3468
- bindImage(image, it, boundKey ?: "", 10 + index, "generic", hideUntilLoaded = true)
3469
- }
3470
- overlay.optString("text").isNotEmpty() -> TextView(context).apply {
3471
- text = overlay.optString("text")
3472
- textSize = sp(if (isWalletText) 12f else 10f)
3473
- typeface = if (isWalletText) NativeListFonts.regular(context) else NativeListFonts.medium(context)
3474
- includeFontPadding = false
3475
- gravity = Gravity.CENTER
3476
- if (isWalletText) TextViewCompat.setLineHeight(this, dp(16))
3477
- setTextColor(safeColor(overlay.optString("tintColor"), color(null, "secondaryText", "#0000009B")))
3478
- }
3479
- else -> OneKeyIconView(context).apply {
3480
- iconName = overlay.optString("name")
3481
- tintColor = safeColor(overlay.optString("tintColor"), parseNativeListColor("#0000009B"))
3482
- }
3483
- }
3484
- frame.addView(view, FrameLayout.LayoutParams(if (isWalletText && !overlay.has("width")) FrameLayout.LayoutParams.WRAP_CONTENT else FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT))
3485
- val topLeft = overlay.optString("position") == "topLeft"
3486
- leadingFrame.addView(frame, FrameLayout.LayoutParams(if (isWalletText && !overlay.has("width")) FrameLayout.LayoutParams.WRAP_CONTENT else dp(width), dp(height), if (topLeft) Gravity.START or Gravity.TOP else Gravity.END or Gravity.BOTTOM).apply {
3487
- if (topLeft) { marginStart = -offsetX; topMargin = -offsetY } else { marginEnd = -offsetX; bottomMargin = -offsetY }
3488
- })
3489
- selectorViews.add(frame)
3490
- }
3491
- }
3492
- visual.optJSONObject("fallbackIcon")?.takeIf { sources.isEmpty() }?.let { fallbackIcon ->
3493
- leadingFallback.visibility = GONE
3494
- leadingIcon.visibility = VISIBLE
3495
- leadingIcon.iconName = fallbackIcon.optString("name")
3496
- leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
3497
- if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && leadingIcon.iconName == "PlusSmallOutline") {
3498
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
3499
- leadingIcon.glyphSizeDp = 24
3500
- }
3501
- if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && leadingIcon.iconName == "LockSolid") {
3502
- leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(40), dp(40), Gravity.CENTER)
3503
- leadingIcon.glyphSizeDp = 40
3504
- }
3505
- }
3506
- if (visual.optString("borderStyle") == "dashed") {
3507
- leadingFrame.background = GradientDrawable().apply {
3508
- setColor(visualBackground)
3509
- cornerRadius = dp(sizeDp / 2).toFloat()
3510
- setStroke(dp(if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar") 1 else 2), safeColor(visual.optString("borderColor"), parseNativeListColor("#00000072")), dp(4).toFloat(), dp(4).toFloat())
3511
- }
3512
- leadingFallback.background = null
3513
- }
3514
- }
3515
-
3516
- private fun visualSources(visual: JSONObject): List<Pair<JSONObject, String>> {
3517
- return when (val kind = visual.optString("kind")) {
3518
- "stackedImages" -> {
3519
- val images = visual.optJSONArray("images") ?: return emptyList()
3520
- (0 until minOf(3, images.length())).mapNotNull { index ->
3521
- images.optJSONObject(index)?.let { it to "generic" }
3522
- }
3523
- }
3524
- "icon" -> emptyList()
3525
- else -> buildList {
3526
- visual.optJSONObject("image")?.let {
3527
- val variant = when (kind) {
3528
- "token" -> "token"
3529
- "network" -> "network"
3530
- "account", "wallet" -> "avatar"
3531
- else -> "generic"
3532
- }
3533
- add(it to variant)
3534
- }
3535
- if (kind == "token") {
3536
- visual.optJSONObject("networkImage")?.let { add(it to "network") }
3537
- }
3538
- }
3539
- }
3540
- }
3541
-
3542
- private fun sourceFallbackStateKey(source: JSONObject): String? =
3543
- nativeListSourceFallbackStateKey(
3544
- uri = source.optString("uri"),
3545
- headers = source.optJSONObject("headers")?.let { headers ->
3546
- headers.keys().asSequence().associateWith { headers.optString(it) }
3547
- }.orEmpty(),
3548
- )
3549
-
3550
- private fun leadingImageLayout(
3551
- index: Int,
3552
- count: Int,
3553
- sizeDp: Int,
3554
- heightDp: Int,
3555
- tokenPair: Boolean,
3556
- ): FrameLayout.LayoutParams {
3557
- if (count == 1 || tokenPair && index == 0) {
3558
- return FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
3559
- }
3560
- if (tokenPair && index == 1) {
3561
- return FrameLayout.LayoutParams(dp(16), dp(16), Gravity.END or Gravity.BOTTOM).apply {
3562
- marginEnd = -dp(2)
3563
- bottomMargin = -dp(2)
3564
- }
3565
- }
3566
- val imageSize = (sizeDp * 0.72f).toInt()
3567
- return FrameLayout.LayoutParams(dp(imageSize), dp(imageSize), Gravity.START or Gravity.CENTER_VERTICAL).apply {
3568
- marginStart = dp(index * (sizeDp - imageSize) / maxOf(1, count - 1))
3569
- }
3570
- }
3571
-
3572
- private fun bindAccessories(
3573
- item: NativeListItem,
3574
- accessories: JSONArray?,
3575
- theme: JSONObject?,
3576
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
3577
- ) {
3578
- if (accessories == null) return
3579
- var textIndex = 0
3580
- for (index in 0 until minOf(2, accessories.length())) {
3581
- val accessory = accessories.getJSONObject(index)
3582
- when (accessory.optString("kind")) {
3583
- "value" -> {
3584
- showTrailing(textIndex, accessory.optString("text"), !accessory.optBoolean("secondary", false))
3585
- applyValueSegments(trailingViews[textIndex], accessory.optJSONArray("textSegments"))
3586
- textIndex++
3587
- }
3588
- "valuePair" -> showTrailingValuePair(textIndex++, accessory, theme)
3589
- "checkbox" -> bindCheckbox(item, accessory, checkboxState)
3590
- "radio" -> showTrailing(
3591
- textIndex++,
3592
- if (accessory.optBoolean("checked")) "●" else "○",
3593
- true,
3594
- accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
3595
- )
3596
- "switch" -> showTrailing(
3597
- textIndex++,
3598
- if (accessory.optBoolean("value")) "ON" else "OFF",
3599
- true,
3600
- accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
3601
- )
3602
- "chevron" -> showTrailingIcon(
3603
- textIndex++,
3604
- item,
3605
- JSONObject(accessory.toString()).apply {
3606
- put("name", "ChevronRightSmallOutline")
3607
- if (optString("actionKey").isEmpty()) put("actionKey", "press")
3608
- },
3609
- theme,
3610
- )
3611
- "menu" -> showTrailingMenu(textIndex++, accessory.optString("actionKey"))
3612
- "drag" -> showTrailingIcon(
3613
- textIndex++,
3614
- item,
3615
- JSONObject(accessory.toString()).apply { put("name", "DragOutline") },
3616
- theme,
3617
- )
3618
- "icon" -> showTrailingIcon(textIndex++, item, accessory, theme)
3619
- "spinner" -> spinner.visibility = VISIBLE
3620
- "progress" -> showTrailing(textIndex++, "${(accessory.optDouble("value") * 100).toInt()}%", false)
3621
- }
3622
- }
3623
- }
3624
-
3625
- private fun bindCheckbox(
3626
- item: NativeListItem,
3627
- json: JSONObject,
3628
- checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
3629
- ) {
3630
- boundCheckboxData = json
3631
- val targetJson = json.optJSONObject("target")
3632
- val scope = targetJson?.optString("scope", "row") ?: "row"
3633
- val target = NativeSelectionTarget(
3634
- scope,
3635
- when (scope) {
3636
- "section" -> targetJson?.optString("sectionKey")
3637
- "row" -> item.key
3638
- else -> null
3639
- },
3640
- )
3641
- val state = checkboxState(item, target, json.optString("state", "unchecked"))
3642
- val accessoryDisabled = json.optBoolean("disabled", false)
3643
- if (json.optBoolean("loading", false)) {
3644
- checkbox.visibility = GONE
3645
- checkbox.setOnClickListener(null)
3646
- spinner.visibility = VISIBLE
3647
- spinner.alpha = if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
3648
- return
3649
- }
3650
- checkbox.visibility = VISIBLE
3651
- checkbox.setState(state, checkboxIconColor)
3652
- val usesSourceCheckboxGeometry = checkboxUsesSelectorStyle && item.json.has("height")
3653
- checkbox.usesSelectorGeometry = usesSourceCheckboxGeometry
3654
- if (usesSourceCheckboxGeometry) {
3655
- // OneKey patch: source Yoga children may round into padding; retain their complete border.
3656
- clipToPadding = false
3657
- // OneKey patch: even a transparent source border changes RN's background clipping path.
3658
- checkbox.background = null
3659
- BackgroundStyleApplicator.setBackgroundColor(checkbox, if (state == "unchecked") checkboxUncheckedColor else checkboxCheckedColor)
3660
- BackgroundStyleApplicator.setBorderWidth(checkbox, LogicalEdge.ALL, 2f)
3661
- BackgroundStyleApplicator.setBorderColor(checkbox, LogicalEdge.ALL, if (state == "unchecked") checkboxBorderColor else Color.TRANSPARENT)
3662
- BackgroundStyleApplicator.setBorderRadius(checkbox, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(4f, LengthPercentageType.POINT))
3663
- } else {
3664
- checkbox.background = if (state == "unchecked") {
3665
- if (checkboxUsesSelectorStyle) GradientDrawable().apply { setColor(checkboxUncheckedColor); setStroke(dp(2), checkboxBorderColor); cornerRadius = scaledDp(4f) }
3666
- else roundedStroke(checkboxBorderColor, checkboxUncheckedColor, 4f)
3667
- } else {
3668
- roundedFill(checkboxCheckedColor, 4f)
3669
- }
3670
- }
3671
- // Row-level disabled opacity already applies to this child. Only apply a
3672
- // local 0.5 when the accessory alone is disabled, never 0.5 * 0.5.
3673
- checkbox.alpha = if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
3674
- checkbox.isEnabled =
3675
- !item.json.optBoolean("disabled", false) &&
3676
- !accessoryDisabled &&
3677
- !json.optBoolean("loading", false)
3678
- checkbox.setOnClickListener {
3679
- emitAction(
3680
- item,
3681
- json.optString("actionKey", "selection"),
3682
- target,
3683
- checkbox,
3684
- "trailingAccessory",
3685
- )
3686
- }
3687
- }
3688
-
3689
- private fun applySelectionState(
3690
- item: NativeListItem,
3691
- theme: JSONObject?,
3692
- layout: String,
3693
- itemIndex: Int?,
3694
- selected: Boolean,
3695
- ) {
3696
- var rowBackground = color(
3697
- theme,
3698
- if (selected) "rowSelectedBackground" else "rowBackground",
3699
- if (selected) "#0000000F" else "#FFFFFF",
3700
- )
3701
- if (item.type == "rail" || item.type == "mediaTile") {
3702
- // The source FavoriteTokenItem has no active/resting fill, and the NFT
3703
- // tile changes only the image opacity while pressed.
3704
- rowBackground = color(theme, "rowBackground", "#FFFFFF")
3705
- } else if (item.type == "metricCard" && !selected) {
3706
- rowBackground = color(theme, "subduedBackground", "#F9F9F9")
3707
- }
3708
- val groupPosition = when {
3709
- item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> "single"
3710
- // OneKey patch: explicit selector rows preserve the v1 ListItem corner radius.
3711
- item.type == "identity" && item.json.optString("presentation") in setOf("accountSelector", "networkSelector") && item.json.has("height") -> "single"
3712
- else -> when (item.type) {
3713
- "metricCard" -> "single"
3714
- "rail" -> "rail"
3715
- else -> item.json.optString("groupPosition")
3716
- }
3717
- }
3718
- var backgroundGroupPosition = if (item.type == "mediaTile") "mediaTile" else groupPosition
3719
- if (layout == "sectioned" && !item.json.optBoolean("selected", false)) {
3720
- // Selection in sectioned lists is represented by the OneKey checkbox,
3721
- // matching iOS and the app-monorepo network selector.
3722
- rowBackground = color(theme, "rowBackground", "#FFFFFF")
3723
- if (item.type == "sectionHeader") backgroundGroupPosition = ""
3724
- } else if (
3725
- layout == "table" &&
3726
- item.type == "dataRow" &&
3727
- (if (item.json.has("index")) item.json.optInt("index") else itemIndex)?.rem(2) == 0 &&
3728
- !selected
3729
- ) {
3730
- rowBackground = color(theme, "subduedBackground", "#F9F9F9")
3731
- backgroundGroupPosition = ""
3732
- }
3733
- // OneKey patch: section heading backgrounds are independent of list rows.
3734
- if (item.json.has("backgroundColor")) rowBackground = safeColor(item.json.optString("backgroundColor"), rowBackground)
3735
- restingRowBackground = groupedBackground(backgroundGroupPosition, rowBackground)
3736
- background = restingRowBackground
3737
- }
3738
-
3739
- private fun showText(view: TextView, value: String, lines: Int) {
3740
- if (value.isEmpty()) return
3741
- view.text = value
3742
- view.maxLines = lines
3743
- view.visibility = VISIBLE
3744
- }
3745
-
3746
- private fun showTrailing(
3747
- index: Int,
3748
- value: String,
3749
- primary: Boolean,
3750
- actionKey: String? = null,
3751
- textColor: Int? = null,
3752
- ) {
3753
- if (index !in trailingViews.indices || value.isEmpty()) return
3754
- val view = trailingViews[index]
3755
- view.maxLines = 1
3756
- view.text = value
3757
- view.textSize = sp(if (primary) 16f else 14f)
3758
- view.typeface = if (primary) NativeListFonts.medium(context) else NativeListFonts.regular(context)
3759
- view.includeFontPadding = false
3760
- view.fontFeatureSettings = "tnum"
3761
- TextViewCompat.setLineHeight(view, dp(if (primary) 24 else 20))
3762
- textColor?.let(view::setTextColor)
3763
- view.visibility = VISIBLE
3764
- if (!actionKey.isNullOrEmpty()) {
3765
- view.setOnClickListener {
3766
- (tag as? NativeListItem)
3767
- ?.takeUnless { item -> item.json.optBoolean("disabled", false) }
3768
- ?.let { item ->
3769
- emitAction(item, actionKey, null, view, "trailingAccessory", index)
3770
- }
3771
- }
3772
- }
3773
- }
3774
-
3775
- private fun showTrailingValuePair(index: Int, accessory: JSONObject, theme: JSONObject?) {
3776
- if (index !in trailingViews.indices) return
3777
- val primary = accessory.optString("primary")
3778
- val secondary = accessory.optString("secondary")
3779
- if (primary.isEmpty() && secondary.isEmpty()) return
3780
- val value = SpannableStringBuilder()
3781
- val primaryStart = value.length
3782
- value.append(primary)
3783
- value.setSpan(
3784
- ForegroundColorSpan(accessoryTextColor(accessory.optString("primaryTone"), "primary", theme)),
3785
- primaryStart,
3786
- value.length,
3787
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
3788
- )
3789
- value.setSpan(
3790
- AbsoluteSizeSpan(
3791
- TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(16f), resources.displayMetrics).roundToInt(),
3792
- ),
3793
- primaryStart,
3794
- value.length,
3795
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
3796
- )
3797
- if (primary.isNotEmpty() && secondary.isNotEmpty()) value.append('\n')
3798
- val secondaryStart = value.length
3799
- value.append(secondary)
3800
- value.setSpan(
3801
- ForegroundColorSpan(accessoryTextColor(accessory.optString("secondaryTone"), "secondary", theme)),
3802
- secondaryStart,
3803
- value.length,
3804
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
3805
- )
3806
- value.setSpan(
3807
- AbsoluteSizeSpan(
3808
- TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(14f), resources.displayMetrics).roundToInt(),
3809
- ),
3810
- secondaryStart,
3811
- value.length,
3812
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
3813
- )
3814
- val view = trailingViews[index]
3815
- view.text = value
3816
- view.maxLines = 2
3817
- view.gravity = Gravity.END
3818
- view.includeFontPadding = false
3819
- view.typeface = NativeListFonts.regular(context)
3820
- view.fontFeatureSettings = "tnum"
3821
- TextViewCompat.setLineHeight(view, dp(20))
3822
- view.visibility = VISIBLE
3823
- }
3824
-
3825
- private fun showTrailingMenu(index: Int, actionKey: String) {
3826
- showTrailing(index, "⋮", true, actionKey)
3827
- if (index !in trailingViews.indices) return
3828
- trailingViews[index].apply {
3829
- gravity = Gravity.CENTER
3830
- textSize = sp(20f)
3831
- layoutParams = LayoutParams(dp(24), dp(24)).apply {
3832
- if (trailingColumn.orientation == HORIZONTAL && index > 0) marginStart = dp(8)
3833
- }
3834
- }
3835
- }
3836
-
3837
- private fun accessoryTextColor(tone: String, defaultTone: String, theme: JSONObject?): Int =
3838
- when (tone.ifEmpty { defaultTone }) {
3839
- "positive" -> color(theme, "positive", "#00713FDE")
3840
- "negative" -> color(theme, "negative", "#C40006D3")
3841
- "secondary" -> color(theme, "secondaryText", "#0000009B")
3842
- else -> color(theme, "primaryText", "#000000DF")
3843
- }
3844
-
3845
- private fun showTrailingIcon(index: Int, item: NativeListItem, data: JSONObject, theme: JSONObject?) {
3846
- if (index !in trailingIcons.indices) return
3847
- val icon = trailingIcons[index]
3848
- icon.iconName = data.optString("name")
3849
- icon.tintColor = safeColor(data.optString("tintColor"), iconSubduedColor)
3850
- // OneKey patch: preserve the original 38dp press target around its 24dp layout slot.
3851
- icon.setTag(com.facebook.react.R.id.react_test_id, data.optString("testID").takeIf { it.isNotEmpty() })
3852
- icon.accessibilityDelegate = selectorAccessibilityDelegate
3853
- icon.contentDescription = data.optString("accessibilityLabel").takeIf { it.isNotEmpty() }
3854
- if (item.json.optString("presentation") == "accountSelector") {
3855
- icon.glyphSizeDp = 24
3856
- val isSourceMenu = item.json.has("height") && icon.iconName == "DotHorOutline"
3857
- val size = if (item.json.has("height") && icon.iconName == "PlusSmallOutline") 36 else 38
3858
- icon.layoutParams = LayoutParams(dp(size), dp(size)).apply {
3859
- gravity = Gravity.CENTER_VERTICAL
3860
- marginStart = -dp(7)
3861
- marginEnd = -dp(7)
3862
- }
3863
- // The frame overhangs its 24dp layout slot by 7dp on each side. The column
3864
- // clips children and the row clips to padding by default, which would cut
3865
- // the pressed circle down to a 24dp-wide pill.
3866
- trailingColumn.clipChildren = false
3867
- clipToPadding = false
3868
- if (isSourceMenu && data.optString("actionKey").isNotEmpty()) {
3869
- icon.background = StateListDrawable().apply {
3870
- addState(
3871
- intArrayOf(android.R.attr.state_pressed),
3872
- roundedFill(color(theme, "rowPressedBackground", "#00000017"), 19f),
3873
- )
3874
- addState(intArrayOf(), roundedFill(Color.TRANSPARENT, 19f))
3875
- }
3876
- }
3877
- } else if (icon.iconName == "ChevronRightSmallOutline") {
3878
- icon.glyphSizeDp = null
3879
- // ListItem.DrillIn is a 24dp icon with mx=-6, for a 12dp layout footprint.
3880
- icon.layoutParams = LayoutParams(dp(24), dp(24)).apply {
3881
- marginStart = -dp(6)
3882
- marginEnd = -dp(6)
3883
- }
3884
- } else {
3885
- // ListItem.IconButton medium keeps a 24dp glyph in a 36dp frame. Its
3886
- // m=-7 moves the last frame 7dp through the row's trailing padding. For
3887
- // the Bookmark XStack, gap=$6 plus both negative margins leaves 10dp
3888
- // between the physical frames (46dp between glyph centers).
3889
- icon.glyphSizeDp = 24
3890
- setPadding(paddingLeft, paddingTop, dp(5), paddingBottom)
3891
- icon.layoutParams = LayoutParams(dp(36), dp(36)).apply {
3892
- gravity = Gravity.END
3893
- if (trailingColumn.orientation == HORIZONTAL && index > 0) {
3894
- marginStart = dp(10)
3895
- }
3896
- }
3897
- }
3898
- icon.visibility = VISIBLE
3899
- icon.isEnabled = !data.optBoolean("disabled", false)
3900
- icon.alpha = if (icon.isEnabled) 1f else 0.4f
3901
- val actionKey = data.optString("actionKey")
3902
- if (icon.isEnabled && actionKey.isNotEmpty()) {
3903
- icon.setOnClickListener {
3904
- emitAction(item, actionKey, null, icon, "trailingAccessory", index)
3905
- }
3906
- }
3907
- }
3908
-
3909
- private fun applySize(item: NativeListItem) {
3910
- if (item.type == "system" && item.json.optString("presentation") == "market" &&
3911
- item.json.optString("variant") in setOf("retry", "noMatch")) {
3912
- val defaultHeight = if (item.json.optString("variant") == "noMatch") 88.0 else if (item.json.optString("message").isEmpty()) 52.0 else 120.0
3913
- minimumHeight = (item.json.optDouble("height", defaultHeight).toFloat() * resources.displayMetrics.density).roundToInt()
3914
- selectorHeight = if (item.json.has("height")) minimumHeight else null
3915
- return
3916
- }
3917
- if (item.type == "market") {
3918
- val style = item.json.optJSONObject("style")
3919
- val imageHeight = style?.optJSONObject("image")?.optDouble(
3920
- "height",
3921
- if (item.json.optString("variant") == "stock") 40.0 else 32.0,
3922
- ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3923
- val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3924
- val defaultHeight = if (item.json.optString("variant") == "stock") 72.0 else 68.0
3925
- // OneKey patch: preserve fractional DP until the final physical pixel edge.
3926
- val rowHeight = item.json.optDouble(
3927
- "height",
3928
- maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3929
- )
3930
- val sourceScale = if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)
3931
- minimumHeight = (rowHeight * sourceScale * resources.displayMetrics.density).roundToInt()
3932
- // OneKey patch: explicit Market row heights must not grow after child pixel rounding.
3933
- selectorHeight = if (item.json.has("height")) minimumHeight else null
3934
- return
3935
- }
3936
- val isWalletSidebar =
3937
- item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
3938
- val isAccountSelectorIdentity =
3939
- item.type == "identity" && item.json.optString("presentation") == "accountSelector"
3940
- val isNetworkSelectorIdentity =
3941
- item.type == "identity" && item.json.optString("presentation") == "networkSelector"
3942
- val isAccountSelectorAction =
3943
- item.type == "action" && item.json.optString("presentation") == "accountSelector"
3944
- val isNetworkSelectorSection =
3945
- item.type == "sectionHeader" && item.json.optString("presentation") == "networkSelector"
3946
- title.textSize = sp(
3947
- if (isWalletSidebar) {
3948
- 12f
3949
- } else {
3950
- when (item.type) {
3951
- "message" -> 14f
3952
- "sectionHeader" -> when {
3953
- isNetworkSelectorSection && item.json.optString("variant") != "summary" -> 14f
3954
- item.json.optString("variant") == "gallery" -> 18f
3955
- item.json.optString("variant") == "summary" -> 16f
3956
- currentLayout == "table" -> 11f
3957
- item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true -> 12f
3958
- else -> 14f
3959
- }
3960
- "rail" -> 12f
3961
- "system" -> 14f
3962
- "metricCard" -> if (item.json.optString("size") == "large") 24f else 18f
3963
- else -> 16f
3964
- }
3965
- },
3966
- )
3967
- title.typeface = if (isAccountSelectorAction && item.json.has("icon")) {
3968
- NativeListFonts.medium(context)
3969
- } else if (isWalletSidebar || isAccountSelectorIdentity || isAccountSelectorAction) {
3970
- NativeListFonts.regular(context)
3971
- } else {
3972
- when (item.type) {
3973
- "sectionHeader" -> when {
3974
- isNetworkSelectorSection && item.json.has("height") && item.json.optString("variant") != "summary" && (item.json.optJSONObject("checkbox") != null || item.json.optString("titleActionKey").isEmpty()) -> NativeListFonts.semibold(context)
3975
- isNetworkSelectorSection -> NativeListFonts.medium(context)
3976
- currentLayout == "table" -> NativeListFonts.regular(context)
3977
- item.json.optString("variant") == "summary" -> NativeListFonts.medium(context)
3978
- item.sectionKey in setOf("linear-tokens", "action-tokens") -> NativeListFonts.regular(context)
3979
- else -> NativeListFonts.semibold(context)
3980
- }
3981
- "system" -> NativeListFonts.regular(context)
3982
- "message", "metricCard" -> NativeListFonts.semibold(context)
3983
- else -> NativeListFonts.medium(context)
3984
- }
3985
- }
3986
- subtitle.textSize = sp(when (item.type) {
3987
- "mediaTile" -> 12f
3988
- "metricCard" -> 11f
3989
- else -> 14f
3990
- })
3991
- tertiary.textSize = sp(14f)
3992
- if (isNetworkSelectorSection && item.json.optString("variant") != "summary") {
3993
- TextViewCompat.setLineHeight(title, dp(20))
3994
- } else if (item.type == "sectionHeader" && item.json.optString("variant") == "gallery") {
3995
- TextViewCompat.setLineHeight(title, dp(24))
3996
- } else if (item.type == "sectionHeader" && item.json.optString("variant") == "summary") {
3997
- TextViewCompat.setLineHeight(title, dp(24))
3998
- } else if (item.type == "sectionHeader" && currentLayout == "table") {
3999
- TextViewCompat.setLineHeight(title, dp(14))
4000
- TextViewCompat.setLineHeight(trailingViews[0], dp(14))
4001
- } else if (
4002
- item.type == "sectionHeader" &&
4003
- (item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true)
4004
- ) {
4005
- TextViewCompat.setLineHeight(title, dp(16))
4006
- } else if (item.type == "sectionHeader" && item.sectionKey in setOf("linear-tokens", "action-tokens")) {
4007
- TextViewCompat.setLineHeight(title, dp(20))
4008
- } else if (item.type == "identity" && item.json.optString("presentation") == "walletSidebar") {
4009
- TextViewCompat.setLineHeight(title, dp(16))
4010
- } else if (item.type == "identity") {
4011
- TextViewCompat.setLineHeight(title, dp(24))
4012
- TextViewCompat.setLineHeight(subtitle, dp(20))
4013
- TextViewCompat.setLineHeight(tertiary, dp(20))
4014
- } else if (item.type == "action") {
4015
- TextViewCompat.setLineHeight(title, dp(24))
4016
- } else if (item.type == "system") {
4017
- TextViewCompat.setLineHeight(title, dp(20))
4018
- } else if (item.type == "mediaTile") {
4019
- TextViewCompat.setLineHeight(title, dp(24))
4020
- TextViewCompat.setLineHeight(subtitle, dp(16))
4021
- }
4022
- status.textSize = sp(12f)
4023
- badgeLine.textSize = sp(12f)
4024
- dataColumns.forEach { column ->
4025
- column.textSize = sp(16f)
4026
- column.typeface = NativeListFonts.medium(context)
4027
- column.fontFeatureSettings = "tnum"
4028
- }
4029
- // OneKey patch: exact selector row dimensions override template minimums.
4030
- selectorHeight = if (item.json.has("height")) dp(item.json.optInt("height")) else null
4031
- // OneKey patch: React Native's section spacers and letter blocks truncate physical heights.
4032
- val isSelectorLetter = isNetworkSelectorSection && item.json.has("height") &&
4033
- item.json.optString("variant") != "summary" && item.json.optString("titleActionKey").isEmpty() &&
4034
- item.json.optJSONObject("checkbox") == null
4035
- val isSelectorSectionSpacer = selectorUsesSourceScale && currentLayout == "sectioned" &&
4036
- item.type == "system" && item.json.optString("variant") == "spacer"
4037
- if (isSelectorLetter || isSelectorSectionSpacer) {
4038
- selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).toInt()
4039
- }
4040
- if (isSelectorLetter) {
4041
- val inset = (20 * resources.displayMetrics.density).toInt() - dp(8)
4042
- // OneKey patch: SectionHeader has a fixed height and centered text, without vertical padding.
4043
- setPadding(inset, 0, inset, 0)
4044
- }
4045
- if (item.json.has("height") && isNetworkSelectorSection && item.json.optString("variant") != "summary" && item.json.optString("titleActionKey").isNotEmpty() && item.json.optJSONObject("checkbox") == null) {
4046
- // OneKey patch: the text-and-underline header's fractional measured height rounds up in React Native.
4047
- selectorHeight = kotlin.math.ceil((item.json.optInt("height") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toDouble()).toInt()
4048
- }
4049
- // OneKey patch: an explicit per-row policy preserves each source list's measured heights.
4050
- if (item.json.has("height")) {
4051
- when (item.json.optString("heightRounding")) {
4052
- "floor" -> selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).toInt()
4053
- "nearest" -> selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).roundToInt()
4054
- }
4055
- }
4056
- val baseHeight = when {
4057
- item.json.has("height") -> item.json.optInt("height")
4058
- item.type == "system" && item.json.optString("variant") == "spacer" -> item.json.optInt("height", 0)
4059
- item.type == "walletGroup" -> {
4060
- val childCount = item.json.optJSONArray("children")?.length() ?: 0
4061
- // OneKey patch: include badge heights in the group layout.
4062
- // (childCount + 1) * 68 + childCount * 12
4063
- val members = listOf(item.json.getJSONObject("parent")) + (0 until childCount).map { item.json.getJSONArray("children").getJSONObject(it) }
4064
- members.sumOf { it.optInt("height", if ((it.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68) } + childCount * 12 + if (members.first().has("height")) 2 else 0
4065
- }
4066
- isNetworkSelectorIdentity -> 47
4067
- else -> when (item.type) {
4068
- "rail" -> 28
4069
- "activity" -> if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
4070
- "message" -> 0
4071
- "mediaTile" -> 0
4072
- "metricCard" -> when (item.json.optString("variant")) {
4073
- "activity" -> 0
4074
- "performance" -> 0
4075
- else -> 132
4076
- }
4077
- "sectionHeader" -> when {
4078
- isNetworkSelectorSection -> 44
4079
- currentLayout == "table" -> 28
4080
- item.json.optString("variant") == "summary" -> 80
4081
- item.json.optString("variant") == "gallery" -> 32
4082
- item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true -> 16
4083
- item.sectionKey in setOf("linear-tokens", "action-tokens") -> 30
4084
- item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null -> 40
4085
- else -> 36
4086
- }
4087
- "system" -> when (item.json.optString("variant")) {
4088
- "loading" -> when (item.json.optString("loadingStyle")) {
4089
- "skeleton" -> 56
4090
- "spinner" -> 52
4091
- else -> if (item.json.optString("presentation") == "market") 68 else 56
4092
- }
4093
- "noMatch", "retry" -> if (item.json.optString("presentation") == "market") 44 else if (item.json.optString("variant") == "noMatch") 36 else 44
4094
- "warning" -> 0
4095
- "end" -> if (item.json.optString("presentation") == "market") 44 else 36
4096
- else -> 56
4097
- }
4098
- "action" -> when {
4099
- item.json.optString("presentation") == "accountSelector" -> 48
4100
- item.json.has("icon") -> 60
4101
- else -> 44
4102
- }
4103
- "dataRow" -> if (currentLayout == "table") {
4104
- 60
4105
- } else if ((0 until item.json.getJSONArray("columns").length()).any {
4106
- item.json.getJSONArray("columns").getJSONObject(it).optString("secondaryText").isNotEmpty()
4107
- }) {
4108
- 64
4109
- } else {
4110
- 56
4111
- }
4112
- else -> when {
4113
- item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> if ((item.json.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68
4114
- item.type == "identity" && item.json.optString("tertiary").isNotEmpty() -> 72
4115
- item.type == "identity" && item.json.optString("subtitle").isNotEmpty() -> 60
4116
- else -> 56
4117
- }
4118
- }
4119
- }
4120
- val modifier = if (item.json.has("height") || isNetworkSelectorIdentity) {
4121
- 0
4122
- } else if (
4123
- item.type == "sectionHeader" && item.json.optString("variant") in listOf("summary", "gallery")
4124
- ) {
4125
- 0
4126
- } else {
4127
- when (item.json.optString("size", "medium")) { "small" -> -8; "large" -> 12; else -> 0 }
4128
- }
4129
- val sectionSpacing = 0
4130
- val hasSecondaryColumn = item.type == "dataRow" &&
4131
- (0 until item.json.getJSONArray("columns").length()).any {
4132
- item.json.getJSONArray("columns").getJSONObject(it).optString("secondaryText").isNotEmpty()
4133
- }
4134
- val tableAdjustment = if (item.type == "dataRow" && currentLayout == "table" && !hasSecondaryColumn) -8 else 0
4135
- minimumHeight = dp((baseHeight + modifier + sectionSpacing + tableAdjustment).coerceAtLeast(0))
4136
- }
4137
-
4138
- private fun addSkeleton(
4139
- view: View,
4140
- widthDp: Int,
4141
- heightDp: Int,
4142
- theme: JSONObject?,
4143
- bottomMarginDp: Int = 0,
4144
- ) {
4145
- view.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 6f)
4146
- mainColumn.addView(
4147
- view,
4148
- LayoutParams(dp(widthDp), dp(heightDp)).apply { bottomMargin = dp(bottomMarginDp) },
4149
- )
4150
- }
4151
-
4152
- private fun applyListOrientation(item: NativeListItem, listOrientation: String) {
4153
- val params = layoutParams ?: return
4154
- if (listOrientation == "horizontal") {
4155
- params.width = when (item.type) {
4156
- "rail" -> railWidth()
4157
- "mediaTile" -> dp(200)
4158
- else -> dp(280)
4159
- }
4160
- params.height = ViewGroup.LayoutParams.WRAP_CONTENT
4161
- } else {
4162
- params.width = ViewGroup.LayoutParams.MATCH_PARENT
4163
- params.height = ViewGroup.LayoutParams.WRAP_CONTENT
4164
- }
4165
- layoutParams = params
4166
- }
4167
-
4168
- private fun railWidth(): Int {
4169
- var width = dp(4 + 20 + 6).toFloat() + title.paint.measureText(title.text.toString())
4170
- if (badgeLine.visibility == VISIBLE) {
4171
- width += dp(6) + badgeLine.paint.measureText(badgeLine.text.toString())
4172
- }
4173
- if (status.visibility == VISIBLE) {
4174
- width += dp(6) + status.paint.measureText(status.text.toString())
4175
- }
4176
- width += dp(4)
4177
- return width.roundToInt()
4178
- }
4179
-
4180
- private fun leadingCornerRadius(shape: String, sizeDp: Int): Float = when (shape) {
4181
- "square" -> 0f
4182
- "rounded" -> minOf(10f, sizeDp / 4f)
4183
- else -> sizeDp / 2f
4184
- }
4185
-
4186
- private fun leadingOutlineProvider(shape: String) = object : ViewOutlineProvider() {
4187
- override fun getOutline(view: View, outline: Outline) {
4188
- when (shape) {
4189
- "square" -> outline.setRect(0, 0, view.width, view.height)
4190
- "rounded" -> {
4191
- // OneKey patch: the account avatar has an 8-point radius at its 32-point size.
4192
- // outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
4193
- val radius = if ((tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" && (tag as? NativeListItem)?.json?.has("height") == true) dp(8).toFloat() else dp(10).toFloat()
4194
- outline.setRoundRect(0, 0, view.width, view.height, radius)
4195
- }
4196
- else -> outline.setOval(0, 0, view.width, view.height)
4197
- }
4198
- }
4199
- }
4200
-
4201
- private fun roundedOutlineProvider(radiusDp: Float) = object : ViewOutlineProvider() {
4202
- override fun getOutline(view: View, outline: Outline) {
4203
- outline.setRoundRect(0, 0, view.width, view.height, scaledDp(radiusDp))
4204
- }
4205
- }
4206
-
4207
- private fun JSONArray?.hasAccessory(kind: String): Boolean {
4208
- if (this == null) return false
4209
- return (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
4210
- }
4211
-
4212
- private fun JSONArray?.isBookmarkEditActions(): Boolean =
4213
- this != null && length() == 2 &&
4214
- optJSONObject(0)?.optString("kind") == "icon" &&
4215
- optJSONObject(0)?.optString("name") == "PencilOutline" &&
4216
- optJSONObject(1)?.optString("kind") == "icon" &&
4217
- optJSONObject(1)?.optString("name") == "DragOutline"
4218
-
4219
- private fun JSONArray?.isValuePairMenu(): Boolean =
4220
- this != null && length() == 2 &&
4221
- optJSONObject(0)?.optString("kind") == "valuePair" &&
4222
- optJSONObject(1)?.optString("kind") == "menu"
4223
-
4224
- private fun bindImage(
4225
- source: JSONObject,
4226
- imageView: OneKeyImageReusableView,
4227
- token: String,
4228
- slot: Int,
4229
- variant: String,
4230
- round: Boolean = false,
4231
- onLoad: (() -> Unit)? = null,
4232
- onError: (() -> Unit)? = null,
4233
- hideUntilLoaded: Boolean = false,
4234
- retryAttempt: Int = 0,
4235
- ) {
4236
- selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
4237
- val expectedEpoch = bindingEpoch
4238
- val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
4239
- val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
4240
- val sourceHeadersJson = source.optJSONObject("headers")?.toString()
4241
- val recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt"
4242
- if (hideUntilLoaded) {
4243
- imageView.visibility = if (
4244
- imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)
4245
- ) VISIBLE else INVISIBLE
4246
- }
4247
- val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
4248
- if (bindingEpoch == expectedEpoch) {
4249
- imageView.visibility = VISIBLE
4250
- onLoad?.invoke()
4251
- }
4252
- }) else onLoad
4253
- val handleError: (() -> Unit)? = if (hideUntilLoaded) ({
4254
- if (bindingEpoch == expectedEpoch) {
4255
- imageView.visibility = INVISIBLE
4256
- onError?.invoke()
4257
- }
4258
- }) else onError
4259
- imageView.configure(
4260
- sourceUri = uri,
4261
- sourceHeadersJson = sourceHeadersJson,
4262
- variant = variant,
4263
- contentFit = source.optString("contentFit", "cover"),
4264
- cachePolicy = source.optString("cachePolicy", "memory-disk"),
4265
- autoplay = source.optBoolean("autoplay", false),
4266
- recyclingKey = recyclingKey,
4267
- optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
4268
- round = round,
4269
- overscan = source.optDouble("overscan", 1.1),
4270
- loadingStrategy = source.optString("loadingStrategy", "none"),
4271
- placeholderColor = imagePlaceholderColor,
4272
- onLoad = if (retryLimit == 0) handleLoad else ({
4273
- if (bindingEpoch == expectedEpoch) {
4274
- selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
4275
- handleLoad?.invoke()
4276
- }
4277
- }),
4278
- onError = if (retryLimit == 0) handleError else ({
4279
- if (bindingEpoch == expectedEpoch) {
4280
- handleError?.invoke()
4281
- if (retryAttempt < retryLimit && !selectorImageRetries.containsKey(imageView)) {
4282
- val retry = Runnable {
4283
- if (bindingEpoch == expectedEpoch) {
4284
- selectorImageRetries.remove(imageView)
4285
- bindImage(
4286
- source = source,
4287
- imageView = imageView,
4288
- token = token,
4289
- slot = slot,
4290
- variant = variant,
4291
- round = round,
4292
- onLoad = onLoad,
4293
- onError = onError,
4294
- hideUntilLoaded = hideUntilLoaded,
4295
- retryAttempt = retryAttempt + 1,
4296
- )
4297
- }
4298
- }
4299
- selectorImageRetries[imageView] = retry
4300
- imageView.postDelayed(retry, kotlin.random.Random.nextLong(3) * 1000L)
4301
- }
4302
- }
4303
- }),
4304
- )
4305
- }
4306
-
4307
- private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
4308
- private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
4309
- private fun dp(value: Int): Int = if (selectorUsesSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
4310
- private fun scaledDp(value: Float): Float = if (selectorUsesSourceScale) (value * resources.displayMetrics.density).roundToInt().toFloat() else NativeListScale.dp(resources, value)
4311
- private fun sp(value: Float): Float = if (selectorUsesSourceScale) value else NativeListScale.font(resources, value)
4312
-
4313
- private fun color(theme: JSONObject?, key: String, fallback: String): Int =
4314
- safeColor(theme?.optString(key, fallback), parseNativeListColor(fallback))
4315
-
4316
- private fun safeColor(value: String?, fallback: Int): Int = try {
4317
- if (value.isNullOrEmpty()) fallback else parseNativeListColor(value)
4318
- } catch (_: IllegalArgumentException) {
4319
- fallback
4320
- }
4321
-
4322
- private fun roundedFill(color: Int, radiusDp: Float) = GradientDrawable().apply {
4323
- setColor(color)
4324
- cornerRadius = scaledDp(radiusDp)
4325
- }
4326
-
4327
- private fun roundedStroke(stroke: Int, fill: Int, radiusDp: Float) = GradientDrawable().apply {
4328
- setColor(fill)
4329
- setStroke(dp(2), stroke)
4330
- cornerRadius = scaledDp(radiusDp)
4331
- }
4332
-
4333
- private fun roundedHairlineStroke(stroke: Int, radiusDp: Float) = GradientDrawable().apply {
4334
- setColor(Color.TRANSPARENT)
4335
- setStroke(1, stroke)
4336
- cornerRadius = scaledDp(radiusDp)
4337
- }
4338
-
4339
- private fun groupedBackground(position: String, color: Int) = GradientDrawable().apply {
4340
- setColor(color)
4341
- val radius = scaledDp(12f)
4342
- cornerRadii = when (position) {
4343
- "first" -> floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f)
4344
- "last" -> floatArrayOf(0f, 0f, 0f, 0f, radius, radius, radius, radius)
4345
- "single" -> FloatArray(8) { radius }
4346
- "rail" -> FloatArray(8) { scaledDp(8f) }
4347
- "mediaTile" -> FloatArray(8) { scaledDp(16f) }
4348
- else -> FloatArray(8)
4349
- }
4350
- }
4351
-
4352
- }
4353
-
4354
- private class OneKeyIconView(context: android.content.Context) : View(context) {
4355
- var useSourceScale = false
4356
- var iconName: String = ""
4357
- set(value) {
4358
- field = value
4359
- invalidate()
4360
- }
4361
- var tintColor: Int = Color.rgb(100, 100, 100)
4362
- set(value) {
4363
- field = value
4364
- invalidate()
4365
- }
4366
- var glyphSizeDp: Int? = null
4367
- set(value) {
4368
- field = value
4369
- invalidate()
4370
- }
4371
-
4372
- private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
4373
-
4374
- override fun onDraw(canvas: Canvas) {
4375
- super.onDraw(canvas)
4376
- val pathData = iconPaths[iconName] ?: return
4377
- val drawSize = minOf(
4378
- minOf(width, height).toFloat(),
4379
- glyphSizeDp?.let { if (useSourceScale) (it * resources.displayMetrics.density).roundToInt().toFloat() else NativeListScale.dp(resources, it).toFloat() } ?: Float.MAX_VALUE,
4380
- )
4381
- // OneKey patch: custom account-error artwork uses an 18-point viewBox.
4382
- // val scale = drawSize / 24f
4383
- val scale = drawSize / (selectorIconViewBoxes[iconName] ?: 24f)
4384
- fill.color = tintColor
4385
- canvas.save()
4386
- canvas.translate((width - drawSize) / 2f, (height - drawSize) / 2f)
4387
- canvas.scale(scale, scale)
4388
- val sourceFillTypes = actionIconFillTypes[iconName]
4389
- pathData.forEachIndexed { index, data ->
4390
- PathParser.createPathFromPathData(data)?.let { path ->
4391
- path.fillType = sourceFillTypes?.getOrNull(index) ?: Path.FillType.EVEN_ODD
4392
- // OneKey patch: provider illustration colors are part of their source asset.
4393
- fill.color = selectorIconColors[iconName]?.getOrNull(index) ?: tintColor
4394
- canvas.drawPath(path, fill)
4395
- }
4396
- }
4397
- canvas.restore()
4398
- }
4399
-
4400
- companion object {
4401
- // OneKey patch: preserve provider colors and non-24 viewBoxes.
4402
- private val selectorIconColors: Map<String, List<Int?>> = mapOf(
4403
- "GlobusOutline" to listOf(null),
4404
- "LockSolid" to listOf(null),
4405
- "GoogleIllus" to listOf(parseNativeListColor("#4285F4"), parseNativeListColor("#34A853"), parseNativeListColor("#FBBC05"), parseNativeListColor("#EA4335")),
4406
- "AppleBrand" to listOf(null),
4407
- "BotIllus" to listOf(parseNativeListColor("#8897A5"), parseNativeListColor("#3FA9F5"), parseNativeListColor("#8897A5"), parseNativeListColor("#8897A5"), parseNativeListColor("#10243E"), parseNativeListColor("#10243E"), parseNativeListColor("#10243E")),
4408
- "AllNetworksSolid" to listOf(null, null),
4409
- "CrossedSmallSolid" to listOf(null),
4410
- "AccountErrorCustom" to listOf(Color.argb(0x72, 0, 0, 0), Color.argb(0x72, 0, 0, 0)),
4411
- "Circle" to listOf(null),
4412
- )
4413
- private val selectorIconViewBoxes = mapOf(
4414
- "GlobusOutline" to 24f,
4415
- "LockSolid" to 24f,
4416
- "GoogleIllus" to 24f,
4417
- "AppleBrand" to 16f,
4418
- "BotIllus" to 24f,
4419
- "AllNetworksSolid" to 24f,
4420
- "CrossedSmallSolid" to 24f,
4421
- "AccountErrorCustom" to 18f,
4422
- "Circle" to 24f,
4423
- )
4424
- // Keep the SVG fill-rule used by each Action-row source path. React Native
4425
- // SVG defaults to nonzero (WINDING); only paths declaring fillRule="evenodd"
4426
- // use EVEN_ODD. Other existing icons retain their prior rendering behavior.
4427
- private val actionIconFillTypes = mapOf(
4428
- "GlobusOutline" to listOf(Path.FillType.EVEN_ODD),
4429
- "LockSolid" to listOf(Path.FillType.EVEN_ODD),
4430
- "GoogleIllus" to listOf(Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING),
4431
- "AppleBrand" to listOf(Path.FillType.WINDING),
4432
- "BotIllus" to listOf(Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING),
4433
- "AllNetworksSolid" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
4434
- "CrossedSmallSolid" to listOf(Path.FillType.WINDING),
4435
- "AccountErrorCustom" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
4436
- "Circle" to listOf(Path.FillType.WINDING),
4437
- "BadgeVerifiedSolid" to listOf(Path.FillType.EVEN_ODD),
4438
- "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
4439
- "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
4440
- "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
4441
- "PlusSmallOutline" to listOf(Path.FillType.WINDING),
4442
- "DotHorOutline" to listOf(Path.FillType.WINDING),
4443
- "MinusCircleSolid" to listOf(Path.FillType.EVEN_ODD),
4444
- "PencilOutline" to listOf(Path.FillType.EVEN_ODD),
4445
- "DragOutline" to listOf(Path.FillType.WINDING),
4446
- "StarSolid" to listOf(Path.FillType.WINDING),
4447
- "ChevronGrabberVerOutline" to listOf(Path.FillType.WINDING),
4448
- "ChevronBottomOutline" to listOf(Path.FillType.WINDING),
4449
- "ChevronTopOutline" to listOf(Path.FillType.WINDING),
4450
- "ImageSquareWavesOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
4451
- )
4452
-
4453
- // Exact 24x24 paths from app-monorepo packages/components Icon sources.
4454
- private val iconPaths = mapOf(
4455
- // OneKey patch: official selector SVG path geometry.
4456
- "GlobusOutline" to listOf("M12 2c5.185 0 9.448 3.947 9.95 9H22v2h-.05c-.502 5.053-4.765 9-9.95 9s-9.448-3.947-9.95-9H2v-2h.05C2.552 5.947 6.815 2 12 2M9.523 13c.09 1.982.438 3.726.934 5.002.29.746.612 1.282.917 1.614.304.331.517.384.626.384s.322-.053.626-.384c.305-.332.627-.868.917-1.614.496-1.276.845-3.02.934-5.002zm-5.459 0a8 8 0 0 0 4.8 6.36 10 10 0 0 1-.271-.633C7.994 17.187 7.61 15.189 7.52 13zm12.416 0c-.09 2.189-.474 4.187-1.073 5.727a10 10 0 0 1-.271.633 8 8 0 0 0 4.8-6.36zM8.863 4.639A8 8 0 0 0 4.064 11h3.457c.09-2.189.473-4.187 1.072-5.727q.127-.327.27-.634M12 4c-.109 0-.322.053-.626.384-.305.332-.627.868-.917 1.614-.496 1.276-.844 3.02-.934 5.002h4.954c-.09-1.982-.438-3.726-.934-5.002-.29-.746-.612-1.282-.917-1.614C12.322 4.053 12.109 4 12 4m3.136.639q.144.307.271.634c.599 1.54.982 3.538 1.073 5.727h3.456a8 8 0 0 0-4.8-6.361"),
4457
- "LockSolid" to listOf("M12 2a5 5 0 0 1 5 5v2h3v13H4V9h3V7a5 5 0 0 1 5-5m-1 11v5h2v-5zm1-9a3 3 0 0 0-3 3v2h6V7a3 3 0 0 0-3-3"),
4458
- "GoogleIllus" to listOf("M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09", "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23", "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22z", "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53"),
4459
- "AppleBrand" to listOf("M11.67.834c.117 1.074-.315 2.153-.955 2.928-.64.773-1.692 1.378-2.718 1.298-.14-1.054.38-2.151.971-2.836C9.63 1.45 10.746.872 11.67.834M14.994 7.093c-.176.108-1.992 1.224-1.972 3.482.025 2.769 2.428 3.693 2.46 3.705l-.004.015a10.1 10.1 0 0 1-1.264 2.593c-.764 1.116-1.556 2.229-2.806 2.254-.598.011-1-.162-1.416-.343-.437-.19-.891-.386-1.609-.386-.751 0-1.226.203-1.683.398-.397.169-.78.333-1.32.354-1.208.047-2.124-1.207-2.895-2.32C.909 14.57-.294 10.414 1.322 7.612c.803-1.395 2.237-2.275 3.794-2.298.671-.014 1.32.244 1.89.47.434.172.821.326 1.135.326.282 0 .659-.149 1.099-.323.692-.273 1.539-.607 2.41-.518.599.026 2.276.24 3.354 1.818z"),
4460
- "BotIllus" to listOf("M11 2a1 1 0 1 1 2 0v1.8l1.6 1.6a1 1 0 1 1-1.4 1.4L12 5.6l-1.2 1.2a1 1 0 0 1-1.4-1.4L11 3.8z", "M8.0 6.0h8.0a5.0 5.0 0 0 1 5.0 5.0v4.0a5.0 5.0 0 0 1 -5.0 5.0h-8.0a5.0 5.0 0 0 1 -5.0 -5.0v-4.0a5.0 5.0 0 0 1 5.0 -5.0z", "M3.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z", "M21.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z", "M7.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0", "M13.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0", "M8.5 15.4c.9.8 2.08 1.2 3.5 1.2s2.6-.4 3.5-1.2c.24-.2.6-.18.8.06.2.23.17.6-.06.8-1.14.98-2.58 1.46-4.24 1.46s-3.1-.48-4.24-1.46a.58.58 0 0 1-.06-.8c.2-.24.56-.26.8-.06"),
4461
- "AllNetworksSolid" to listOf("M15.333 13.998a1.335 1.335 0 1 1 0 2.67 1.335 1.335 0 0 1 0-2.67", "M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0M8 12.668A2 2 0 0 0 6 14.666V16c0 1.103.895 1.997 1.998 1.998h1.334A2 2 0 0 0 11.33 16v-1.334a2 2 0 0 0-1.998-1.998zm7.333 0a2.665 2.665 0 1 0 0 5.33 2.665 2.665 0 0 0 0-5.33M7.999 6.001A2 2 0 0 0 6.001 8v1.334c0 1.103.895 1.998 1.998 1.998h1.334a2 2 0 0 0 1.998-1.998V7.999a2 2 0 0 0-1.998-1.998zm6.667 0A2 2 0 0 0 12.668 8v1.334c0 1.103.895 1.998 1.998 1.998H16a2 2 0 0 0 1.998-1.998V7.999A2 2 0 0 0 16 6.001z"),
4462
- "CrossedSmallSolid" to listOf("M17.87 8.25 14.12 12l3.75 3.75-2.12 2.121-3.75-3.75-3.75 3.75-2.121-2.121L9.879 12l-3.75-3.75 2.12-2.121L12 9.879l3.75-3.75 2.122 2.121Z"),
4463
- "AccountErrorCustom" to listOf("M12.5 12.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5", "M0 3.5A3.5 3.5 0 0 1 3.5 0h8.088A2.41 2.41 0 0 1 14 2.412V5h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H4a4 4 0 0 1-4-4zm2 3.163V14a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H3.5c-.537 0-1.045-.12-1.5-.337M2 3.5A1.5 1.5 0 0 0 3.5 5H12V2.412A.41.41 0 0 0 11.588 2H3.5A1.5 1.5 0 0 0 2 3.5"),
4464
- "Circle" to listOf("M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0"),
4465
- "BadgeVerifiedSolid" to listOf("M9.483 11.458v3.5h-1v-3.5z M10.467 2.698a2.03 2.03 0 0 1 3.065 0l1.358 1.564a.03.03 0 0 0 .028.01l2.046-.325a2.03 2.03 0 0 1 2.347 1.971l.037 2.07q0 .016.014.026l1.776 1.066a2.03 2.03 0 0 1 .532 3.019l-1.304 1.609a.03.03 0 0 0-.005.03l.675 1.956a2.03 2.03 0 0 1-1.533 2.656l-2.033.394a.03.03 0 0 0-.023.019l-.741 1.933a2.03 2.03 0 0 1-2.88 1.05l-1.811-1.006a.03.03 0 0 0-.03 0l-1.811 1.005a2.03 2.03 0 0 1-2.88-1.049l-.742-1.933a.03.03 0 0 0-.023-.019l-2.033-.394a2.03 2.03 0 0 1-1.532-2.656l.675-1.957a.03.03 0 0 0-.005-.029l-1.304-1.61a2.03 2.03 0 0 1 .532-3.018l1.776-1.066a.03.03 0 0 0 .014-.026l.035-2.07a2.03 2.03 0 0 1 2.349-1.97l2.045.324a.03.03 0 0 0 .028-.01zm1.516 3.76a.5.5 0 0 0-.447.276l-1.861 3.724H8.483a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h6.692a2 2 0 0 0 1.981-1.73l.341-2.5a2 2 0 0 0-1.982-2.27h-1.939l.197-1.269a1.5 1.5 0 0 0-1.481-1.731z"),
4466
- "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
4467
- "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
4468
- "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),
4469
- "SwapHorOutline" to listOf("M21 16H6.914l2.293 2.293-1.414 1.414L2.086 14H21zm.914-6H3V8h14.086l-2.293-2.293 1.414-1.414z"),
4470
- "ShieldExclamationOutline" to listOf(
4471
- "M12 12.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5m0-4.75a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1",
4472
- "M11.352 2.223c.42-.145.876-.145 1.296 0l6.98 2.4a1.995 1.995 0 0 1 1.347 1.886v5.432c0 2.799-1.146 4.817-2.805 6.387-1.61 1.525-3.735 2.652-5.696 3.71a1 1 0 0 1-.947 0c-1.961-1.058-4.086-2.185-5.697-3.71-1.659-1.57-2.805-3.588-2.805-6.386V6.508c0-.852.542-1.61 1.347-1.887l6.98-2.4ZM5.02 6.509v5.432c0 2.16.848 3.676 2.18 4.938 1.272 1.204 2.957 2.149 4.799 3.145 1.842-.996 3.527-1.94 4.799-3.145 1.332-1.262 2.181-2.778 2.181-4.938V6.51L12 4.109z",
4473
- ),
4474
- "InfoCircleOutline" to listOf(
4475
- "M13 17h-2v-5h-1v-2h3zm0-8h-2V7h2z",
4476
- "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
4477
- ),
4478
- "SpeakerPromoteOutline" to listOf("M21.996 11a4 4 0 0 1-3 3.874v5.495l-5.361-1.711A3.998 3.998 0 0 1 5.996 17v-.779l-4-1.276v-7.89l17-5.424v5.495c1.725.444 3 2.01 3 3.874m-2 0c0-.74-.402-1.385-1-1.731v3.46c.597-.345 1-.989 1-1.729M7.998 7.24v7.521l8.998 2.871V4.368l-8.998 2.87ZM3.996 8.516v4.967l2.002.638V7.877zm4 8.484a2 2 0 0 0 3.706 1.041L7.996 16.86z"),
4479
- "PlusCircleOutline" to listOf(
4480
- "M13 11h4v2h-4v4h-2v-4l-4 .001v-2L11 11V7h2z",
4481
- "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
4482
- ),
4483
- "PlusSmallOutline" to listOf("M13 11h5v2h-5v5h-2v-5H6v-2h5V6h2z"),
4484
- "DotHorOutline" to listOf("M6 14H2v-4h4zm8 0h-4v-4h4zm8 0h-4v-4h4z"),
4485
- "MinusCircleOutline" to listOf(
4486
- "M17 13H7v-2h10z",
4487
- "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
4488
- ),
4489
- "MinusCircleSolid" to listOf("M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2M7 13h10v-2H7z"),
4490
- "ErrorSolid" to listOf("M23.256 20H.742L12 1.041zM11 15v2h2v-2zm0-6v5h2V9z"),
4491
- "QuestionmarkSolid" to listOf("M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m-1 15h2v-2h-2zM9 7v3h2V9h2v1l-2 1.5V14h2v-1.5l2-1.5V7z"),
4492
- "PencilOutline" to listOf("M22.414 7.5 7.914 22H2v-5.914l14.5-14.5zM4 16.914V20h3.086l9.5-9.5L13.5 7.414zM14.914 6 18 9.086 19.586 7.5 16.5 4.414z"),
4493
- "ChevronRightSmallOutline" to listOf("M15.414 12 10 17.414 8.586 16l4-4-4-4L10 6.586z"),
4494
- "DragOutline" to listOf("M11 21H7v-4h4zm6 0h-4v-4h4zm-6-7H7v-4h4zm6 0h-4v-4h4zm-6-7H7V3h4zm6 0h-4V3h4z"),
4495
- "StarOutline" to listOf("m15.455 7.243 7.729 1.123-5.592 5.45 1.32 7.698L12 17.879l-6.911 3.635 1.32-7.698-5.592-5.45 7.728-1.123L12 .24zM9.872 9.071l-4.759.69 3.444 3.358-.814 4.738L12 15.62l.465.245 3.791 1.993-.813-4.739 3.443-3.357-4.758-.69L12 4.758z"),
4496
- "StarSolid" to listOf("m15.405 7.313 7.84 1.034-5.735 5.443 1.44 7.774L12 17.793l-6.948 3.771 1.44-7.774L.756 8.347l7.839-1.034L12 .178z"),
4497
- "ChevronGrabberVerOutline" to listOf("M17.414 15 12 20.414 6.586 15 8 13.586l4 4 4-4zm0-6L16 10.414l-4-4-4 4L6.586 9 12 3.586z"),
4498
- "ChevronBottomOutline" to listOf("M20.707 9.707 12 18.414 3.293 9.707l1.414-1.414L12 15.586l7.293-7.293z"),
4499
- "ChevronTopOutline" to listOf("m20.707 14.293-1.414 1.414L12 8.414l-7.293 7.293-1.414-1.414L12 5.586z"),
4500
- "ImageSquareWavesOutline" to listOf(
4501
- "M14.25 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4",
4502
- "M21 21H3V3h18zM5 16.414V19h12.586L14 15.414l-2 2-4-4zm0-2.828 3-3 4 4 2-2 5 5V5H5z",
4503
- ),
4504
- )
4505
- }
4506
- }
4507
-
4508
- private class OneKeyCheckboxView(context: android.content.Context) : View(context) {
4509
- var usesSelectorGeometry = false
4510
- private val glyphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
4511
- private var state = "unchecked"
4512
-
4513
- fun setState(value: String, color: Int) {
4514
- state = value
4515
- glyphPaint.color = color
4516
- invalidate()
4517
- }
4518
-
4519
- override fun onDraw(canvas: Canvas) {
4520
- super.onDraw(canvas)
4521
- val pathData = when (state) {
4522
- "checked" -> "M12.204 5.043a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414 0l-2-2a1 1 0 1 1 1.414-1.414l1.293 1.293 3.793-3.793a1 1 0 0 1 1.414 0"
4523
- "indeterminate" -> "M4 8a1 1 0 0 1 1-1h6a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1"
4524
- else -> return
4525
- }
4526
- // OneKey patch: the source icon is a 16dp child after the 2dp border, not 80% of a rounded frame.
4527
- val drawSize = if (usesSelectorGeometry) (16 * resources.displayMetrics.density).roundToInt().toFloat() else minOf(width, height) * 0.8f
4528
- val borderOffset = (2 * resources.displayMetrics.density).roundToInt().toFloat()
4529
- canvas.save()
4530
- canvas.translate(if (usesSelectorGeometry) borderOffset else (width - drawSize) / 2f, if (usesSelectorGeometry) borderOffset else (height - drawSize) / 2f)
4531
- canvas.scale(drawSize / 16f, drawSize / 16f)
4532
- PathParser.createPathFromPathData(pathData)?.let { canvas.drawPath(it, glyphPaint) }
4533
- canvas.restore()
4534
- }
4535
- }
4536
-
4537
- internal class NativeListViewHolder(val rowView: NativeListRowView) :
4538
- androidx.recyclerview.widget.RecyclerView.ViewHolder(rowView)