@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,415 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Color
5
+ import android.graphics.Paint
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewOutlineProvider
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import kotlin.math.roundToInt
12
+ import org.json.JSONObject
13
+
14
+ internal enum class NativeListRendererUpdate {
15
+ UNCHANGED,
16
+ CONTENT,
17
+ ASSETS,
18
+ REPLACE,
19
+ }
20
+
21
+ // Shared chrome and binding lifetime; each renderer owns its content views.
22
+ internal abstract class NativeListRendererRowView(protected val reactContext: ThemedReactContext) :
23
+ NativeListRowHost(reactContext) {
24
+ protected abstract fun bindContent(
25
+ item: NativeListItem,
26
+ theme: JSONObject?,
27
+ layout: String,
28
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
29
+ )
30
+
31
+ protected open fun bindSelectionContent(
32
+ item: NativeListItem,
33
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
34
+ ) {}
35
+
36
+ protected abstract fun recycleContent()
37
+
38
+ protected abstract fun disposeContent()
39
+
40
+ // Evaluated on every bind (including unchanged rebinds), before bindContent.
41
+ protected open fun usesSourceScale(item: NativeListItem, provided: Boolean) =
42
+ item.usesSelectorSourceScale || provided
43
+
44
+ protected open fun onRowTouch(event: MotionEvent) {}
45
+
46
+ protected open fun consumesRowClick() = false
47
+
48
+ protected fun setTouchPressed(value: Boolean) {
49
+ touchPressed = value
50
+ }
51
+
52
+ protected fun retainBoundItem(item: NativeListItem): Boolean {
53
+ if (current?.key != item.key) return false
54
+ current = item
55
+ tag = item
56
+ return true
57
+ }
58
+
59
+ protected open fun accessibilityText(item: NativeListItem) =
60
+ item.json.optString("accessibilityLabel", item.json.optString("title"))
61
+
62
+ protected open val assetFields: List<String> = emptyList()
63
+
64
+ private fun update(old: NativeListItem?, next: NativeListItem): NativeListRendererUpdate {
65
+ if (old == null || old.key != next.key || old.rendererKey != next.rendererKey)
66
+ return NativeListRendererUpdate.REPLACE
67
+ if (old.content == next.content) return NativeListRendererUpdate.UNCHANGED
68
+ if (
69
+ assetFields.any {
70
+ nativeListCanonical(old.json.opt(it)) != nativeListCanonical(next.json.opt(it))
71
+ }
72
+ )
73
+ return NativeListRendererUpdate.ASSETS
74
+ return NativeListRendererUpdate.CONTENT
75
+ }
76
+
77
+ protected open fun defaultHeight(item: NativeListItem, layout: String) = 0
78
+
79
+ protected open fun minimumContentHeight(item: NativeListItem, layout: String, sizeDelta: Int) =
80
+ dp((defaultHeight(item, layout) + sizeDelta).coerceAtLeast(0))
81
+
82
+ protected open fun modelHeight(item: NativeListItem, layout: String): Int? =
83
+ if (item.json.has("height"))
84
+ when (item.json.optString("heightRounding")) {
85
+ "floor" -> (item.json.optDouble("height") * resources.displayMetrics.density).toInt()
86
+ "nearest" -> stylePx(item.json.optDouble("height"))
87
+ else -> dp(item.json.optInt("height"))
88
+ }
89
+ else null
90
+
91
+ protected open fun horizontalWidth(item: NativeListItem) = dp(280)
92
+
93
+ protected open val defaultBorderWidth = 0
94
+
95
+ protected open fun defaultBorderColor(theme: JSONObject?) = Color.TRANSPARENT
96
+
97
+ protected open fun resolvedMeasureHeight(explicit: Int?): Int? = explicit
98
+
99
+ protected open val defaultCornerRadius = 0
100
+ protected open val defaultSeparatorInset = 12
101
+ protected open val pressChangesBackground = true
102
+
103
+ protected open fun pressContent(pressed: Boolean) {}
104
+
105
+ protected open val showsSelection = true
106
+ protected open val pressedColorName = "rowPressedBackground"
107
+ protected open val pressedColorFallback = "#00000017"
108
+ private var current: NativeListItem? = null
109
+ private var theme: JSONObject? = null
110
+ private var listLayout = "linear"
111
+ private var itemIndex: Int? = null
112
+
113
+ protected open fun unselectedBackground(
114
+ item: NativeListItem,
115
+ theme: JSONObject?,
116
+ layout: String,
117
+ itemIndex: Int?,
118
+ ) = color(theme?.optString("rowBackground", "#FFFFFF") ?: "#FFFFFF")
119
+
120
+ protected open fun backgroundGroupPosition(
121
+ item: NativeListItem,
122
+ layout: String,
123
+ itemIndex: Int?,
124
+ selected: Boolean,
125
+ ) = item.json.optString("groupPosition")
126
+
127
+ private var selected = false
128
+ protected val rowSelected
129
+ get() = selected
130
+
131
+ protected var sourceScale = false
132
+ private var inputSignature = ""
133
+ private var explicitHeight: Int? = null
134
+ private var touchPressed = false
135
+ private var reorderActive = false
136
+ private val separator = Paint(Paint.ANTI_ALIAS_FLAG)
137
+
138
+ protected fun dp(value: Int) =
139
+ if (sourceScale) (value * resources.displayMetrics.density).roundToInt()
140
+ else NativeListScale.dp(resources, value)
141
+
142
+ protected fun scaledDp(value: Float) =
143
+ if (sourceScale) value * resources.displayMetrics.density
144
+ else NativeListScale.dp(resources, value)
145
+
146
+ protected fun stylePx(value: Double) = (value * resources.displayMetrics.density).roundToInt()
147
+
148
+ private fun color(value: String, fallback: Int = Color.TRANSPARENT) =
149
+ runCatching { parseNativeListColor(value) }.getOrDefault(fallback)
150
+
151
+ init {
152
+ orientation = HORIZONTAL
153
+ setOnClickListener {
154
+ if (consumesRowClick()) return@setOnClickListener
155
+ current
156
+ ?.takeIf { it.isRowPressEnabled }
157
+ ?.let { onRowPress?.invoke(it, NativeListActionOrigin(this, this, bindingEpoch, "row")) }
158
+ }
159
+ setOnTouchListener { _, event ->
160
+ val wasPressed = touchPressed
161
+ when (event.actionMasked) {
162
+ MotionEvent.ACTION_DOWN -> touchPressed = current?.isRowPressEnabled == true
163
+ // Legacy: leaving the row clears the pressed highlight before release.
164
+ MotionEvent.ACTION_MOVE ->
165
+ if (event.x < 0 || event.y < 0 || event.x >= width || event.y >= height)
166
+ touchPressed = false
167
+ MotionEvent.ACTION_UP,
168
+ MotionEvent.ACTION_CANCEL -> touchPressed = false
169
+ }
170
+ onRowTouch(event)
171
+ // Rebuild drawables only when the pressed state actually changes.
172
+ if (touchPressed != wasPressed) appearance()
173
+ false
174
+ }
175
+ }
176
+
177
+ override fun bind(
178
+ item: NativeListItem,
179
+ theme: JSONObject?,
180
+ layout: String,
181
+ listOrientation: String,
182
+ itemIndex: Int?,
183
+ selected: Boolean,
184
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
185
+ useSourceScale: Boolean,
186
+ ) {
187
+ val update = update(current, item)
188
+ val signature =
189
+ nativeListCanonical(
190
+ JSONObject()
191
+ .put("theme", theme)
192
+ .put("layout", layout)
193
+ .put("orientation", listOrientation)
194
+ .put("sourceScale", useSourceScale)
195
+ .put("listStyle", listStyle)
196
+ )
197
+ sourceScale = usesSourceScale(item, useSourceScale)
198
+ if (update != NativeListRendererUpdate.UNCHANGED || signature != inputSignature) {
199
+ if (current != null) onBindingInvalidated?.invoke(this, bindingEpoch)
200
+ bindingEpoch++
201
+ if (update == NativeListRendererUpdate.REPLACE) {
202
+ recycleContent()
203
+ touchPressed = false
204
+ }
205
+ bindContent(item, theme, layout, checkboxState)
206
+ inputSignature = signature
207
+ }
208
+ current = item
209
+ tag = item
210
+ this.theme = theme
211
+ listLayout = layout
212
+ this.itemIndex = itemIndex
213
+ this.selected = selected
214
+ explicitHeight = item.styledHeight?.let(::stylePx) ?: modelHeight(item, layout)
215
+ val sizeDelta =
216
+ when (item.json.optString("size")) {
217
+ "small" -> -8
218
+ "large" -> 12
219
+ else -> 0
220
+ }
221
+ minimumHeight = if (explicitHeight == null) minimumContentHeight(item, layout, sizeDelta) else 0
222
+ layoutParams =
223
+ (layoutParams
224
+ ?: android.view.ViewGroup.LayoutParams(
225
+ LayoutParams.MATCH_PARENT,
226
+ LayoutParams.WRAP_CONTENT,
227
+ ))
228
+ .apply {
229
+ width =
230
+ if (listOrientation == "horizontal") horizontalWidth(item)
231
+ else LayoutParams.MATCH_PARENT
232
+ height = LayoutParams.WRAP_CONTENT
233
+ }
234
+ contentDescription = accessibilityText(item)
235
+ setTag(
236
+ com.facebook.react.R.id.react_test_id,
237
+ item.json.optString("testID").takeIf { it.isNotEmpty() },
238
+ )
239
+ isEnabled = !item.json.optBoolean("disabled")
240
+ isClickable = item.isWholeRowInteractive
241
+ isFocusable = isClickable
242
+ if (!isEnabled) touchPressed = false
243
+ bindSelectionContent(item, checkboxState)
244
+ appearance()
245
+ requestLayout()
246
+ }
247
+
248
+ override fun bindSelection(
249
+ item: NativeListItem,
250
+ theme: JSONObject?,
251
+ layout: String,
252
+ itemIndex: Int?,
253
+ selected: Boolean,
254
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
255
+ ) {
256
+ if (current?.key != item.key) return
257
+ current = item
258
+ tag = item
259
+ this.theme = theme
260
+ this.selected = selected
261
+ listLayout = layout
262
+ this.itemIndex = itemIndex
263
+ bindSelectionContent(item, checkboxState)
264
+ appearance()
265
+ }
266
+
267
+ protected fun emitAction(
268
+ key: String,
269
+ view: View,
270
+ source: String,
271
+ slot: Int? = null,
272
+ target: NativeSelectionTarget? = null,
273
+ anchorInset: Int = 0,
274
+ ) {
275
+ val item = current ?: return
276
+ // Legacy: row disabled/deprecated state only gates the controls that opted
277
+ // into it (text accessories, checkboxes, footer actions, retry); icon menus,
278
+ // leading actions, badges and close controls keep working.
279
+ if (key.isNotEmpty())
280
+ onAction?.invoke(
281
+ item,
282
+ key,
283
+ target,
284
+ NativeListActionOrigin(view, this, bindingEpoch, source, slot, anchorInset),
285
+ )
286
+ }
287
+
288
+ protected open fun appearance() {
289
+ val item = current ?: return
290
+ val pressed = touchPressed || reorderActive
291
+ pressContent(pressed)
292
+ val container = item.json.optJSONObject("style")?.optJSONObject("container") ?: JSONObject()
293
+ val showSelected =
294
+ showsSelection && selected && (listLayout != "sectioned" || item.json.optBoolean("selected"))
295
+ val backgroundName =
296
+ if (pressed && pressChangesBackground) pressedColorName
297
+ else if (showSelected) "rowSelectedBackground" else "rowBackground"
298
+ val backgroundFallback =
299
+ if (pressed && pressChangesBackground) pressedColorFallback
300
+ else if (showSelected) "#0000000F" else "#FFFFFF"
301
+ var fill =
302
+ if (!pressed && !showSelected) unselectedBackground(item, theme, listLayout, itemIndex)
303
+ else color(theme?.optString(backgroundName, backgroundFallback) ?: backgroundFallback)
304
+ if (!pressed || !pressChangesBackground) {
305
+ if (item.json.has("backgroundColor"))
306
+ fill = color(item.json.optString("backgroundColor"), fill)
307
+ if (container.has("backgroundColor"))
308
+ fill = color(container.optString("backgroundColor"), fill)
309
+ }
310
+ val position = backgroundGroupPosition(item, listLayout, itemIndex, showSelected)
311
+ val radius =
312
+ if (container.has("cornerRadius")) stylePx(container.optDouble("cornerRadius")).toFloat()
313
+ else if (defaultCornerRadius > 0) dp(defaultCornerRadius).toFloat()
314
+ else if (listStyle?.has("groupCornerRadius") == true)
315
+ stylePx(listStyle!!.optDouble("groupCornerRadius")).toFloat()
316
+ else dp(12).toFloat()
317
+ val top =
318
+ container.has("cornerRadius") ||
319
+ defaultCornerRadius > 0 ||
320
+ position in setOf("first", "single")
321
+ val bottom =
322
+ container.has("cornerRadius") ||
323
+ defaultCornerRadius > 0 ||
324
+ position in setOf("last", "single")
325
+ // Legacy: the pressed fill is always a fully rounded 12dp "single" shape,
326
+ // independent of the resting group position.
327
+ val pressedShape =
328
+ pressed && pressChangesBackground && !container.has("cornerRadius") && defaultCornerRadius <= 0
329
+ val radii =
330
+ if (pressedShape) FloatArray(8) { scaledDp(12f) }
331
+ else FloatArray(8) { if (it < 4 && top || it >= 4 && bottom) radius else 0f }
332
+ outlineProvider = ViewOutlineProvider.BACKGROUND
333
+ clipToOutline = container.has("cornerRadius")
334
+ background =
335
+ GradientDrawable().apply {
336
+ setColor(fill)
337
+ cornerRadii = radii
338
+ }
339
+ foreground =
340
+ if (container.has("borderWidth") || defaultBorderWidth > 0)
341
+ GradientDrawable().apply {
342
+ setColor(Color.TRANSPARENT)
343
+ cornerRadii = radii
344
+ setStroke(
345
+ if (container.has("borderWidth")) stylePx(container.optDouble("borderWidth"))
346
+ else dp(defaultBorderWidth),
347
+ color(container.optString("borderColor"), defaultBorderColor(theme)),
348
+ )
349
+ }
350
+ else null
351
+ alpha =
352
+ container.optDouble("opacity", item.json.optDouble("opacity", 1.0)).toFloat() *
353
+ if (isEnabled) 1f else 0.5f
354
+ separator.color =
355
+ color(
356
+ listStyle
357
+ ?.optJSONObject("separator")
358
+ ?.optString("color", theme?.optString("separator", "#0000001F") ?: "#0000001F")
359
+ ?: theme?.optString("separator", "#0000001F")
360
+ ?: "#0000001F"
361
+ )
362
+ separator.strokeWidth = 1f
363
+ invalidate()
364
+ }
365
+
366
+ override fun setReorderActive(active: Boolean) {
367
+ reorderActive = active
368
+ appearance()
369
+ }
370
+
371
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
372
+ // Android's native measurement consumes the exact resolved text/image views.
373
+ super.onMeasure(
374
+ widthMeasureSpec,
375
+ resolvedMeasureHeight(explicitHeight)?.let {
376
+ MeasureSpec.makeMeasureSpec(it, MeasureSpec.EXACTLY)
377
+ } ?: heightMeasureSpec,
378
+ )
379
+ }
380
+
381
+ override fun dispatchDraw(canvas: Canvas) {
382
+ super.dispatchDraw(canvas)
383
+ current?.takeIf { it.json.optBoolean("separator") }?.let {
384
+ val start =
385
+ listStyle
386
+ ?.optJSONObject("separator")
387
+ ?.takeIf { it.has("inset") }
388
+ ?.let { stylePx(it.optDouble("inset")) } ?: dp(defaultSeparatorInset)
389
+ canvas.drawLine(start.toFloat(), height - 1f, width.toFloat(), height - 1f, separator)
390
+ }
391
+ }
392
+
393
+ override fun onInitializeAccessibilityNodeInfo(
394
+ info: android.view.accessibility.AccessibilityNodeInfo
395
+ ) {
396
+ super.onInitializeAccessibilityNodeInfo(info)
397
+ info.viewIdResourceName = getTag(com.facebook.react.R.id.react_test_id) as? String
398
+ }
399
+
400
+ override fun recycle() {
401
+ if (current != null) onBindingInvalidated?.invoke(this, bindingEpoch)
402
+ bindingEpoch++
403
+ current = null
404
+ tag = null
405
+ inputSignature = ""
406
+ touchPressed = false
407
+ reorderActive = false
408
+ recycleContent()
409
+ }
410
+
411
+ override fun dispose() {
412
+ recycle()
413
+ disposeContent()
414
+ }
415
+ }
@@ -0,0 +1,163 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.content.Context
4
+ import android.graphics.Paint
5
+ import android.graphics.Typeface
6
+ import android.text.Spannable
7
+ import android.text.SpannableStringBuilder
8
+ import android.text.TextUtils
9
+ import android.text.style.LineHeightSpan
10
+ import android.util.TypedValue
11
+ import android.view.Gravity
12
+ import android.widget.TextView
13
+ import kotlin.math.roundToInt
14
+ import org.json.JSONObject
15
+
16
+ internal class NativeListLineHeightSpan(private val height: Int) : LineHeightSpan {
17
+ override fun chooseHeight(
18
+ text: CharSequence,
19
+ start: Int,
20
+ end: Int,
21
+ spanstartv: Int,
22
+ v: Int,
23
+ fm: Paint.FontMetricsInt,
24
+ ) {
25
+ val leading = height - (fm.descent - fm.ascent)
26
+ fm.ascent -= kotlin.math.ceil(leading / 2.0).toInt()
27
+ fm.descent += kotlin.math.floor(leading / 2.0).toInt()
28
+ if (start == 0) fm.top = fm.ascent
29
+ if (end == text.length) fm.bottom = fm.descent
30
+ }
31
+ }
32
+
33
+ internal data class NativeListResolvedText(
34
+ val text: String,
35
+ val fontSize: Float,
36
+ val typeface: Typeface,
37
+ val color: Int,
38
+ val lineHeight: Int,
39
+ val explicitLineHeight: Boolean,
40
+ val lines: Int,
41
+ val explicitTruncation: Boolean,
42
+ val clip: Boolean,
43
+ val gravity: Int,
44
+ val offset: Float,
45
+ ) {
46
+ fun bind(view: NativeListTextView) {
47
+ view.includeFontPadding = false
48
+ view.fontFeatureSettings = "tnum"
49
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
50
+ view.typeface = typeface
51
+ view.setTextColor(color)
52
+ view.letterSpacing = 0f
53
+ view.gravity = gravity
54
+ view.opticalOffsetY = offset
55
+ view.maxLines = lines
56
+ view.setHorizontallyScrolling(lines == 1 && explicitTruncation)
57
+ view.ellipsize = if (explicitTruncation && !clip) TextUtils.TruncateAt.END else null
58
+ view.setLineSpacing(0f, 1f)
59
+ if (explicitLineHeight) {
60
+ view.text =
61
+ SpannableStringBuilder(text).apply {
62
+ setSpan(
63
+ NativeListLineHeightSpan(lineHeight),
64
+ 0,
65
+ length,
66
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
67
+ )
68
+ }
69
+ } else {
70
+ view.text = text
71
+ if (lineHeight > 0) androidx.core.widget.TextViewCompat.setLineHeight(view, lineHeight)
72
+ }
73
+ view.visibility = if (text.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE
74
+ }
75
+
76
+ companion object {
77
+ fun resolve(
78
+ context: Context,
79
+ text: String,
80
+ style: JSONObject?,
81
+ size: Float,
82
+ weight: String,
83
+ color: Int,
84
+ lineHeight: Int,
85
+ lines: Int,
86
+ sourceScale: Boolean,
87
+ ): NativeListResolvedText {
88
+ val density = context.resources.displayMetrics.density
89
+ val maxLines = (style?.optInt("lines", lines) ?: lines).coerceIn(1, 3)
90
+ val truncation = style?.let { it.has("lines") || it.has("truncate") } == true
91
+ val fontSize =
92
+ if (style?.has("fontSize") == true) (style.optDouble("fontSize") * density).toFloat()
93
+ else
94
+ (if (sourceScale) size else NativeListScale.font(context.resources, size)) *
95
+ context.resources.displayMetrics.scaledDensity
96
+ val face =
97
+ when (style?.optString("fontWeight", weight) ?: weight) {
98
+ "medium" -> NativeListFonts.medium(context)
99
+ "semibold" -> NativeListFonts.semibold(context)
100
+ "bold" -> NativeListFonts.bold(context)
101
+ else -> NativeListFonts.regular(context)
102
+ }
103
+ val line =
104
+ if (style?.has("lineHeight") == true) (style.optDouble("lineHeight") * density).roundToInt()
105
+ else if (sourceScale) (lineHeight * density).roundToInt()
106
+ else NativeListScale.dp(context.resources, lineHeight)
107
+ val horizontal =
108
+ when (style?.optString("alignment")) {
109
+ "center" -> Gravity.CENTER_HORIZONTAL
110
+ "end" -> Gravity.END
111
+ else -> Gravity.START
112
+ }
113
+ val vertical =
114
+ when (style?.optString("verticalAlignment")) {
115
+ "center" -> Gravity.CENTER_VERTICAL
116
+ "bottom" -> Gravity.BOTTOM
117
+ else -> Gravity.TOP
118
+ }
119
+ return NativeListResolvedText(
120
+ if (maxLines == 1 && truncation) text.replace(Regex("\\r\\n|[\\r\\n]"), " ") else text,
121
+ fontSize,
122
+ face,
123
+ style
124
+ ?.optString("color")
125
+ ?.takeIf { it.isNotEmpty() }
126
+ ?.let { runCatching { parseNativeListColor(it) }.getOrDefault(color) } ?: color,
127
+ line,
128
+ style?.has("lineHeight") == true,
129
+ maxLines,
130
+ truncation,
131
+ style?.optString("truncate") == "clip",
132
+ horizontal or vertical,
133
+ ((style?.optDouble("offsetY", 0.0) ?: 0.0) * density).toFloat(),
134
+ )
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Legacy selector source-scale typography (accountSelector / networkSelector /
141
+ * walletSidebar with source scaling): whole-pixel unscaled font size, unhinted
142
+ * fractional advances and the original line box. A property explicitly set by the
143
+ * caller's text style (`fontSize`, `lineHeight`) is already resolved in source
144
+ * units and is left untouched so user style still takes effect.
145
+ */
146
+ internal fun applyNativeListSourceTypography(view: TextView, style: JSONObject?) {
147
+ val metrics = view.resources.displayMetrics
148
+ val line = view.lineHeight
149
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
150
+ if (style?.has("fontSize") != true)
151
+ view.setTextSize(
152
+ TypedValue.COMPLEX_UNIT_PX,
153
+ kotlin.math.ceil((view.textSize * metrics.density / metrics.scaledDensity).toDouble()).toFloat(),
154
+ )
155
+ view.letterSpacing = 0f
156
+ if (style?.has("lineHeight") != true && view.text.isNotEmpty()) {
157
+ val text = SpannableStringBuilder(view.text)
158
+ text.getSpans(0, text.length, NativeListLineHeightSpan::class.java).forEach(text::removeSpan)
159
+ text.setSpan(NativeListLineHeightSpan(line), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
160
+ view.setLineSpacing(0f, 1f)
161
+ view.text = text
162
+ }
163
+ }
@@ -0,0 +1,70 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.animation.TimeInterpolator
4
+ import android.view.View
5
+ import android.widget.LinearLayout
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import org.json.JSONObject
8
+
9
+ internal data class NativeListActionOrigin(
10
+ val sourceView: View,
11
+ val ownerRowView: NativeListRowHost,
12
+ val bindingEpoch: Long,
13
+ val source: String,
14
+ val slot: Int? = null,
15
+ val anchorInsetPixels: Int = 0,
16
+ val windowPointPixels: android.graphics.PointF? = null,
17
+ )
18
+
19
+ /** List-facing lifecycle shared by template renderers. */
20
+ internal abstract class NativeListRowHost(context: ThemedReactContext) : LinearLayout(context) {
21
+ var bindingEpoch: Long = 0
22
+ protected set
23
+
24
+ var listStyle: JSONObject? = null
25
+ var onRowPress: ((NativeListItem, NativeListActionOrigin) -> Unit)? = null
26
+ var onAction:
27
+ ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Unit)? =
28
+ null
29
+ var onBindingInvalidated: ((NativeListRowHost, Long) -> Unit)? = null
30
+
31
+ abstract fun bind(
32
+ item: NativeListItem,
33
+ theme: JSONObject?,
34
+ layout: String,
35
+ listOrientation: String,
36
+ itemIndex: Int?,
37
+ selected: Boolean,
38
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
39
+ useSourceScale: Boolean = false,
40
+ )
41
+
42
+ abstract fun bindSelection(
43
+ item: NativeListItem,
44
+ theme: JSONObject?,
45
+ layout: String,
46
+ itemIndex: Int?,
47
+ selected: Boolean,
48
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
49
+ )
50
+
51
+ open fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {}
52
+
53
+ open fun bindStableSummary(item: NativeListItem) {}
54
+
55
+ abstract fun recycle()
56
+
57
+ abstract fun dispose()
58
+
59
+ open fun setReorderActive(active: Boolean) {}
60
+
61
+ open fun canStartWalletGroupReorder(localY: Float) = true
62
+
63
+ open fun finishWalletGroupReorder(
64
+ durationMs: Long,
65
+ interpolator: TimeInterpolator,
66
+ completion: (() -> Unit)? = null,
67
+ ) {
68
+ completion?.invoke()
69
+ }
70
+ }