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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -0,0 +1,147 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import com.facebook.react.uimanager.ThemedReactContext
4
+ import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
5
+ import org.json.JSONArray
6
+ import org.json.JSONObject
7
+
8
+ internal fun nativeListCanonical(value: Any?): String =
9
+ when (value) {
10
+ is JSONObject ->
11
+ value.keys().asSequence().sorted().joinToString(",", "{", "}") {
12
+ JSONObject.quote(it) + ":" + nativeListCanonical(value.opt(it))
13
+ }
14
+ is JSONArray ->
15
+ (0 until value.length()).joinToString(",", "[", "]") { nativeListCanonical(value.opt(it)) }
16
+ is String -> JSONObject.quote(value)
17
+ else -> value?.toString() ?: "null"
18
+ }
19
+
20
+ /** A request survives text-only binding; source/row/slot changes invalidate it. */
21
+ internal class NativeListImageSlot(context: ThemedReactContext) {
22
+ val view = OneKeyImageReusableView(context)
23
+ private var identity: String? = null
24
+ private var epoch = 0
25
+ private var retry: Runnable? = null
26
+ private var completion: ((Boolean) -> Unit)? = null
27
+ private var result: Boolean? = null
28
+
29
+ fun bind(
30
+ source: JSONObject,
31
+ key: String,
32
+ variant: String = "generic",
33
+ fit: String? = null,
34
+ placeholder: String = "#0000000F",
35
+ round: Boolean = false,
36
+ completion: ((Boolean) -> Unit)? = null,
37
+ ) {
38
+ this.completion = completion
39
+ val next =
40
+ nativeListCanonical(
41
+ JSONObject()
42
+ .put("source", source)
43
+ .put("key", key)
44
+ .put("variant", variant)
45
+ .put("fit", fit ?: source.optString("contentFit", "cover"))
46
+ .put("placeholder", placeholder)
47
+ .put("round", round)
48
+ )
49
+ if (identity == next) {
50
+ result?.let { completion?.invoke(it) }
51
+ return
52
+ }
53
+ recycle()
54
+ this.completion = completion
55
+ identity = next
56
+ configure(source, key, variant, fit, placeholder, round, 0, epoch)
57
+ }
58
+
59
+ private fun configure(
60
+ source: JSONObject,
61
+ key: String,
62
+ variant: String,
63
+ fit: String?,
64
+ placeholder: String,
65
+ round: Boolean,
66
+ attempt: Int,
67
+ token: Int,
68
+ ) {
69
+ if (epoch != token) return
70
+ var completed = false
71
+ view.configure(
72
+ source.optString("uri").trim(),
73
+ source.optJSONObject("headers")?.toString(),
74
+ variant,
75
+ fit ?: source.optString("contentFit", "cover"),
76
+ source.optString("cachePolicy", "memory-disk"),
77
+ source.optBoolean("autoplay"),
78
+ if (attempt == 0) key else "$key:retry:$attempt",
79
+ attempt == 0 && source.optBoolean("optimizeTos", true),
80
+ source.optDouble("overscan", 1.1),
81
+ source.optString("loadingStrategy", "none"),
82
+ placeholder,
83
+ round = round,
84
+ onLoad = {
85
+ if (epoch == token && !completed) {
86
+ completed = true
87
+ retry?.let(view::removeCallbacks)
88
+ retry = null
89
+ result = true
90
+ completion?.invoke(true)
91
+ }
92
+ },
93
+ onError = {
94
+ if (epoch == token && !completed) {
95
+ completed = true
96
+ result = false
97
+ completion?.invoke(false)
98
+ if (attempt < source.optInt("retryTimes") && retry == null) {
99
+ val task = Runnable {
100
+ if (epoch == token) {
101
+ retry = null
102
+ configure(source, key, variant, fit, placeholder, round, attempt + 1, token)
103
+ }
104
+ }
105
+ retry = task
106
+ view.postDelayed(task, (0..2).random() * 1000L)
107
+ }
108
+ }
109
+ },
110
+ )
111
+ }
112
+
113
+ fun recycle() {
114
+ epoch++
115
+ retry?.let(view::removeCallbacks)
116
+ retry = null
117
+ identity = null
118
+ completion = null
119
+ result = null
120
+ view.prepareForReuse()
121
+ }
122
+
123
+ fun dispose() {
124
+ recycle()
125
+ view.dispose()
126
+ }
127
+ }
128
+
129
+ internal object NativeListSourceFallbackState {
130
+ private const val CACHE_LIMIT = 128
131
+ private val sources = LinkedHashMap<String, Unit>(CACHE_LIMIT, 0.75f, true)
132
+
133
+ fun has(key: String): Boolean = synchronized(sources) { sources[key] != null }
134
+
135
+ fun remember(key: String) {
136
+ synchronized(sources) {
137
+ sources[key] = Unit
138
+ while (sources.size > CACHE_LIMIT) {
139
+ sources.remove(sources.entries.first().key)
140
+ }
141
+ }
142
+ }
143
+
144
+ fun forget(key: String) {
145
+ synchronized(sources) { sources.remove(key) }
146
+ }
147
+ }
@@ -0,0 +1,453 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Outline
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.view.Gravity
7
+ import android.view.View
8
+ import android.view.ViewOutlineProvider
9
+ import android.widget.FrameLayout
10
+ import android.widget.TextView
11
+ import com.facebook.react.uimanager.ThemedReactContext
12
+ import kotlin.math.roundToInt
13
+ import org.json.JSONObject
14
+
15
+ /** Fixed visual slots; no row-template or business-key branching. */
16
+ internal class NativeListLeadingVisual(private val reactContext: ThemedReactContext) :
17
+ FrameLayout(reactContext) {
18
+ private val fallback =
19
+ TextView(context).apply {
20
+ gravity = Gravity.CENTER
21
+ typeface = NativeListFonts.bold(context)
22
+ }
23
+ private val icon = OneKeyIconView(context)
24
+ private val unread = View(context)
25
+ private val networkBackdrop = View(context)
26
+ private val corner = OneKeyIconView(context)
27
+ private val cornerBackdrop = View(context)
28
+ private val slots = mutableListOf<NativeListImageSlot>()
29
+ private val overlaySlots = mutableListOf<NativeListImageSlot>()
30
+ private val overlays = mutableListOf<Pair<FrameLayout, JSONObject>>()
31
+ private var visual = JSONObject()
32
+ private var style = JSONObject()
33
+ private var sources = emptyList<Pair<JSONObject, String>>()
34
+ val fallbackTextView: TextView
35
+ get() = fallback
36
+
37
+ var bitmapBorderWidth = 0
38
+
39
+ /**
40
+ * Legacy wallet-sidebar source-scale leading visual: text-only overlays are 16dp
41
+ * tall pills with 12sp text on a 16dp line box, and a dashed frame border is 1dp.
42
+ */
43
+ var walletTextOverlays = false
44
+
45
+ private fun isWalletText(data: JSONObject) =
46
+ walletTextOverlays &&
47
+ data.optString("text").isNotEmpty() &&
48
+ data.optJSONObject("image") == null &&
49
+ data.optString("name").isEmpty()
50
+
51
+ private fun overlayHeight(data: JSONObject) =
52
+ data.optInt("height", if (isWalletText(data)) 16 else data.optInt("size", 20))
53
+
54
+ fun layoutNetworkBackdrop(top: Int, height: Int) {
55
+ networkBackdrop.layout(networkBackdrop.left, top, networkBackdrop.right, top + height)
56
+ }
57
+
58
+ // Legacy: loaded images in a "rounded" slot clip at a fixed radius (10dp;
59
+ // 8dp for explicit-height account selectors), independent of slot size.
60
+ var roundedImageRadius = 10
61
+
62
+ /** Market resolves its own radius; legacy still decoded circular bitmaps by shape. */
63
+ var roundFollowsVisualShape = false
64
+ var glyphSize = 18
65
+ var roundedGlyphOrigin = false
66
+ var iconBorder = true
67
+ private var sourceScale = false
68
+
69
+ private fun dp(value: Int) =
70
+ if (sourceScale) (value * resources.displayMetrics.density).roundToInt()
71
+ else NativeListScale.dp(resources, value)
72
+
73
+ private fun color(value: String, fallback: Int = Color.TRANSPARENT) =
74
+ runCatching { parseNativeListColor(value) }.getOrDefault(fallback)
75
+
76
+ private fun fill(value: Int, radius: Float) =
77
+ GradientDrawable().apply {
78
+ setColor(value)
79
+ cornerRadius = radius
80
+ }
81
+
82
+ init {
83
+ listOf(fallback, icon, networkBackdrop, cornerBackdrop, corner, unread).forEach { addView(it) }
84
+ clipChildren = false
85
+ clipToPadding = false
86
+ }
87
+
88
+ fun bind(
89
+ visual: JSONObject,
90
+ style: JSONObject,
91
+ key: String,
92
+ theme: JSONObject?,
93
+ isUnread: Boolean,
94
+ sourceScale: Boolean,
95
+ secondaryVisual: JSONObject? = null,
96
+ ) {
97
+ this.visual = visual
98
+ this.style = style
99
+ this.sourceScale = sourceScale
100
+ icon.useSourceScale = sourceScale
101
+ corner.useSourceScale = sourceScale
102
+ val kind = visual.optString("kind")
103
+ val variant =
104
+ when (kind) {
105
+ "token" -> "token"
106
+ "network" -> "network"
107
+ "account",
108
+ "wallet" -> "avatar"
109
+ else -> "generic"
110
+ }
111
+ sources =
112
+ if (kind == "stackedImages")
113
+ visual
114
+ .optJSONArray("images")
115
+ ?.let { images ->
116
+ (0 until minOf(3, images.length())).mapNotNull {
117
+ images.optJSONObject(it)?.let { image -> image to "generic" }
118
+ }
119
+ }
120
+ .orEmpty()
121
+ else if (kind == "icon") emptyList()
122
+ else visual.optJSONObject("image")?.let { listOf(it to variant) }.orEmpty()
123
+ if (kind == "token" && sources.isNotEmpty())
124
+ visual.optJSONObject("networkImage")?.let { sources = sources + (it to "network") }
125
+ secondaryVisual?.let { second ->
126
+ val kind = second.optString("kind")
127
+ val source =
128
+ when (kind) {
129
+ "stackedImages" -> second.optJSONArray("images")?.optJSONObject(0)
130
+ "icon" -> null
131
+ else -> second.optJSONObject("image")
132
+ }
133
+ if (source != null)
134
+ sources =
135
+ sources +
136
+ (source to
137
+ when (kind) {
138
+ "token" -> "token"
139
+ "network" -> "network"
140
+ "account",
141
+ "wallet" -> "avatar"
142
+ else -> "generic"
143
+ })
144
+ }
145
+ while (slots.size < sources.size) {
146
+ val slot = NativeListImageSlot(reactContext)
147
+ slots.add(slot)
148
+ addView(slot.view, indexOfChild(if (slots.size == 1) networkBackdrop else cornerBackdrop))
149
+ }
150
+ val placeholder = theme?.optString("strongBackground", "#0000000F") ?: "#0000000F"
151
+ val fallbackIcon = visual.optJSONObject("fallbackIcon")
152
+ val sourceFallback =
153
+ sources.firstOrNull()?.first?.optString("loadingStrategy", "none")?.let { it != "none" } ==
154
+ true && (visual.has("fallbackText") || fallbackIcon != null)
155
+ val fallbackKey =
156
+ if (sourceFallback)
157
+ sources.firstOrNull()?.first?.let { source ->
158
+ nativeListSourceFallbackStateKey(
159
+ source.optString("uri"),
160
+ source
161
+ .optJSONObject("headers")
162
+ ?.let { headers ->
163
+ headers.keys().asSequence().associateWith { headers.optString(it) }
164
+ }
165
+ .orEmpty(),
166
+ )
167
+ }
168
+ else null
169
+ val restored = fallbackKey?.let(NativeListSourceFallbackState::has) == true
170
+ fallback.textSize = if (sourceScale) 13f else NativeListScale.font(resources, 13f)
171
+ fallback.text = visual.optString("fallbackText").take(2)
172
+ fallback.setTextColor(color("#00000072"))
173
+ fallback.visibility =
174
+ if (kind != "icon" && fallbackIcon == null && (sources.isEmpty() || restored)) VISIBLE
175
+ else GONE
176
+ val glyph = if (kind == "icon") visual else fallbackIcon
177
+ icon.iconName = glyph?.optString("name") ?: ""
178
+ icon.tintColor = color(glyph?.optString("tintColor", "#0000009B") ?: "#0000009B")
179
+ icon.visibility =
180
+ if (kind == "icon" || (sources.isEmpty() || restored) && fallbackIcon != null) VISIBLE
181
+ else GONE
182
+ val visualBackground =
183
+ color(
184
+ visual.optString("backgroundColor", if (sources.isEmpty()) placeholder else "#00000000")
185
+ )
186
+ val backgroundColor = if (sourceFallback) color(placeholder) else visualBackground
187
+ background =
188
+ fill(backgroundColor, 0f).apply {
189
+ if (kind == "icon" && iconBorder) setStroke(1, color("#0000001F"))
190
+ if (visual.optString("borderStyle") == "dashed")
191
+ setStroke(
192
+ dp(if (walletTextOverlays) 1 else 2),
193
+ color(visual.optString("borderColor", "#00000072")),
194
+ dp(4).toFloat(),
195
+ dp(4).toFloat(),
196
+ )
197
+ }
198
+ slots.forEachIndexed { index, slot ->
199
+ if (index >= sources.size) {
200
+ slot.recycle()
201
+ slot.view.visibility = GONE
202
+ } else {
203
+ slot.view.visibility = if (index > 0 || sourceFallback) INVISIBLE else VISIBLE
204
+ slot.bind(
205
+ sources[index].first,
206
+ "$key:leading:$index",
207
+ sources[index].second,
208
+ if (index == 0) style.optString("contentFit").takeIf { it.isNotEmpty() } else null,
209
+ placeholder,
210
+ round =
211
+ visual.optString("shape", if (kind == "image") "rounded" else "circle") == "circle" &&
212
+ (roundFollowsVisualShape || !style.has("shape") && !style.has("cornerRadius")),
213
+ ) { loaded ->
214
+ slot.view.visibility =
215
+ if (!loaded && (index > 0 || sourceFallback)) INVISIBLE else VISIBLE
216
+ if (index == 0 && sourceFallback) {
217
+ fallbackKey?.let {
218
+ if (loaded) NativeListSourceFallbackState.forget(it)
219
+ else NativeListSourceFallbackState.remember(it)
220
+ }
221
+ (background as? GradientDrawable)?.setColor(
222
+ if (loaded) visualBackground else color(placeholder)
223
+ )
224
+ fallback.visibility = if (!loaded && fallbackIcon == null) VISIBLE else GONE
225
+ icon.visibility = if (!loaded && fallbackIcon != null) VISIBLE else GONE
226
+ }
227
+ }
228
+ }
229
+ }
230
+ networkBackdrop.visibility = if (kind == "token" && sources.size > 1) VISIBLE else GONE
231
+ networkBackdrop.background =
232
+ fill(color(theme?.optString("rowBackground", "#FFFFFF") ?: "#FFFFFF"), dp(10).toFloat())
233
+ val badge = visual.optJSONObject("cornerIcon")
234
+ corner.visibility = if (badge == null) GONE else VISIBLE
235
+ cornerBackdrop.visibility = corner.visibility
236
+ corner.iconName = badge?.optString("name") ?: ""
237
+ corner.tintColor = color(badge?.optString("tintColor", "#0000009B") ?: "#0000009B")
238
+ cornerBackdrop.background =
239
+ fill(
240
+ color(
241
+ badge?.optString(
242
+ "backgroundColor",
243
+ theme?.optString("rowBackground", "#FFFFFF") ?: "#FFFFFF",
244
+ ) ?: "#FFFFFF"
245
+ ),
246
+ dp(10).toFloat(),
247
+ )
248
+ unread.visibility = if (isUnread) VISIBLE else GONE
249
+ unread.background = fill(color("#E5484D"), dp(4).toFloat())
250
+ val previousOverlays = overlays.toList()
251
+ overlays.clear()
252
+ val descriptors =
253
+ visual
254
+ .optJSONArray("overlays")
255
+ ?.let { values -> (0 until minOf(2, values.length())).map { values.getJSONObject(it) } }
256
+ .orEmpty()
257
+ while (overlaySlots.size < descriptors.size) overlaySlots.add(NativeListImageSlot(reactContext))
258
+ descriptors.forEachIndexed { index, data ->
259
+ val frame = previousOverlays.getOrNull(index)?.first ?: FrameLayout(context)
260
+ frame.background =
261
+ fill(
262
+ color(data.optString("backgroundColor", "#00000000")),
263
+ minOf(
264
+ dp(data.optInt("width", data.optInt("size", 20))),
265
+ dp(overlayHeight(data)),
266
+ ) / 2f,
267
+ )
268
+ frame.outlineProvider = ViewOutlineProvider.BACKGROUND
269
+ frame.clipToOutline = true
270
+ val child: View =
271
+ data.optJSONObject("image")?.let { image ->
272
+ val slot = overlaySlots[index]
273
+ slot.view.visibility = INVISIBLE
274
+ slot.bind(image, "$key:overlay:$index", placeholder = placeholder) {
275
+ slot.view.visibility = if (it) VISIBLE else INVISIBLE
276
+ }
277
+ slot.view
278
+ }
279
+ ?: if (data.optString("text").isNotEmpty()) {
280
+ overlaySlots[index].recycle()
281
+ ((frame.getChildAt(0) as? TextView) ?: TextView(context)).apply {
282
+ text = data.optString("text")
283
+ gravity = Gravity.CENTER
284
+ includeFontPadding = false
285
+ val walletText = isWalletText(data)
286
+ val size = if (walletText) 12f else 10f
287
+ textSize = if (sourceScale) size else NativeListScale.font(resources, size)
288
+ // The overlay TextView is reused: reset the line box for non-wallet text.
289
+ if (walletText) androidx.core.widget.TextViewCompat.setLineHeight(this, dp(16))
290
+ else setLineSpacing(0f, 1f)
291
+ typeface =
292
+ if (isWalletText(data)) NativeListFonts.regular(context)
293
+ else NativeListFonts.medium(context)
294
+ setTextColor(color(data.optString("tintColor", "#0000009B")))
295
+ }
296
+ } else {
297
+ overlaySlots[index].recycle()
298
+ ((frame.getChildAt(0) as? OneKeyIconView) ?: OneKeyIconView(context)).apply {
299
+ useSourceScale = sourceScale
300
+ iconName = data.optString("name")
301
+ tintColor = color(data.optString("tintColor", "#0000009B"))
302
+ }
303
+ }
304
+ val inset = data.optInt("padding", 0)
305
+ if (isWalletText(data)) frame.setPadding(dp(2), 0, dp(2), 0)
306
+ else frame.setPadding(dp(inset), dp(inset), dp(inset), dp(inset))
307
+ if (child.parent !== frame) {
308
+ frame.removeAllViews()
309
+ // A reused slot view may still belong to a frame dropped by an earlier bind.
310
+ (child.parent as? android.view.ViewGroup)?.removeView(child)
311
+ frame.addView(child, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
312
+ }
313
+ if (frame.parent == null) addView(frame)
314
+ overlays.add(frame to data)
315
+ }
316
+ previousOverlays.drop(descriptors.size).forEach { (frame, _) ->
317
+ // Release the overlay child so its image slot can join a later frame.
318
+ frame.removeAllViews()
319
+ removeView(frame)
320
+ }
321
+ overlaySlots.drop(descriptors.size).forEach { it.recycle() }
322
+ unread.bringToFront()
323
+ requestLayout()
324
+ }
325
+
326
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
327
+ super.onLayout(changed, left, top, right, bottom)
328
+ fun place(view: View, x: Int, y: Int, w: Int, h: Int) {
329
+ val left = if (layoutDirection == LAYOUT_DIRECTION_RTL) width - x - w else x
330
+ view.measure(
331
+ MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
332
+ MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY),
333
+ )
334
+ view.layout(left, y, left + w, y + h)
335
+ }
336
+ val w = width
337
+ val h = height
338
+ val kind = visual.optString("kind")
339
+ val shape =
340
+ style.optString(
341
+ "shape",
342
+ visual.optString("shape", if (kind == "image") "rounded" else "circle"),
343
+ )
344
+ val radius =
345
+ if (style.has("cornerRadius"))
346
+ (style.optDouble("cornerRadius") * resources.displayMetrics.density).toFloat()
347
+ else
348
+ when (shape) {
349
+ "square" -> 0f
350
+ "rounded" ->
351
+ if (style.has("shape")) 10 * resources.displayMetrics.density
352
+ else minOf(dp(10).toFloat(), h / 4f)
353
+ else -> minOf(w, h) / 2f
354
+ }
355
+ (background as? GradientDrawable)?.apply {
356
+ this.shape =
357
+ if (style.optString("shape") == "circle" && !style.has("cornerRadius"))
358
+ GradientDrawable.OVAL
359
+ else GradientDrawable.RECTANGLE
360
+ cornerRadius = radius
361
+ }
362
+ if (bitmapBorderWidth > 0) {
363
+ (background as? GradientDrawable)?.setStroke(
364
+ bitmapBorderWidth,
365
+ color(visual.optString("borderColor")),
366
+ )
367
+ }
368
+ place(fallback, 0, 0, w, h)
369
+ val iconSize =
370
+ if (sources.isNotEmpty() && visual.optJSONObject("fallbackIcon") != null)
371
+ (minOf(w, h) *
372
+ if (visual.optJSONObject("fallbackIcon")?.optString("name") == "GlobusOutline") 1.2f
373
+ else 1f)
374
+ .roundToInt()
375
+ else dp(glyphSize)
376
+ place(
377
+ icon,
378
+ (w - iconSize + if (roundedGlyphOrigin) 1 else 0) / 2,
379
+ (h - iconSize + if (roundedGlyphOrigin) 1 else 0) / 2,
380
+ iconSize,
381
+ iconSize,
382
+ )
383
+ val tokenPair = kind == "token" && sources.size > 1
384
+ slots.take(sources.size).forEachIndexed { index, slot ->
385
+ val size =
386
+ if (tokenPair && index == 1) dp(16)
387
+ else if (sources.size == 1 || tokenPair) h else (h * 0.72f).roundToInt()
388
+ val x =
389
+ if (tokenPair && index == 1) w - size + dp(2)
390
+ else if (sources.size == 1 || tokenPair) 0
391
+ else (index * (w - size).toFloat() / maxOf(1, sources.size - 1)).roundToInt()
392
+ val y = if (tokenPair && index == 1) h - size + dp(2) else (h - size) / 2
393
+ val imageWidth = if (sources.size == 1 || tokenPair && index == 0) w else size
394
+ slot.view.measure(
395
+ MeasureSpec.makeMeasureSpec(imageWidth, MeasureSpec.EXACTLY),
396
+ MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY),
397
+ )
398
+ val inset = if (index == 0) bitmapBorderWidth else 0
399
+ place(slot.view, x + inset, y + inset, imageWidth - 2 * inset, size - 2 * inset)
400
+ val imageRadius =
401
+ if (shape == "rounded" && !style.has("shape") && !style.has("cornerRadius"))
402
+ dp(roundedImageRadius).toFloat()
403
+ else radius
404
+ val r = if (index == 0 && (sources.size == 1 || tokenPair)) imageRadius else size / 2f
405
+ slot.view.outlineProvider =
406
+ object : ViewOutlineProvider() {
407
+ override fun getOutline(view: View, outline: Outline) {
408
+ if (index == 0 && style.optString("shape") == "circle" && !style.has("cornerRadius"))
409
+ outline.setOval(0, 0, view.width, view.height)
410
+ else outline.setRoundRect(-inset, -inset, view.width + inset, view.height + inset, r)
411
+ }
412
+ }
413
+ slot.view.clipToOutline = true
414
+ }
415
+ place(networkBackdrop, w - dp(16), h - dp(16), dp(20), dp(20))
416
+ place(cornerBackdrop, w - dp(16), h - dp(16), dp(20), dp(20))
417
+ place(corner, w - dp(15), h - dp(15), dp(18), dp(18))
418
+ place(unread, w - dp(8), 0, dp(8), dp(8))
419
+ overlays.forEach { (frame, data) ->
420
+ val size = data.optInt("size", 20)
421
+ val oh = dp(overlayHeight(data))
422
+ val ow =
423
+ if (isWalletText(data) && !data.has("width")) {
424
+ // Legacy: wallet text pills wrap their label plus horizontal padding.
425
+ val child = frame.getChildAt(0)
426
+ child?.measure(
427
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
428
+ MeasureSpec.makeMeasureSpec(oh, MeasureSpec.EXACTLY),
429
+ )
430
+ (child?.measuredWidth ?: 0) + frame.paddingLeft + frame.paddingRight
431
+ } else dp(data.optInt("width", size))
432
+ val x = dp(data.optInt("offsetX", data.optInt("offset", 2)))
433
+ val y = dp(data.optInt("offsetY", data.optInt("offset", 2)))
434
+ val ox = if (data.optString("position") == "topLeft") -x else w - ow + x
435
+ val oy = if (data.optString("position") == "topLeft") -y else h - oh + y
436
+ frame.measure(
437
+ MeasureSpec.makeMeasureSpec(ow, MeasureSpec.EXACTLY),
438
+ MeasureSpec.makeMeasureSpec(oh, MeasureSpec.EXACTLY),
439
+ )
440
+ place(frame, ox, oy, ow, oh)
441
+ }
442
+ }
443
+
444
+ fun recycle() {
445
+ slots.forEach { it.recycle() }
446
+ overlaySlots.forEach { it.recycle() }
447
+ }
448
+
449
+ fun dispose() {
450
+ slots.forEach { it.dispose() }
451
+ overlaySlots.forEach { it.dispose() }
452
+ }
453
+ }