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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -0,0 +1,169 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.view.Gravity
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import org.json.JSONArray
6
+ import org.json.JSONObject
7
+
8
+ internal class NativeListActionRowView(context: ThemedReactContext) :
9
+ NativeListRendererRowView(context) {
10
+ private val visual = NativeListLeadingVisual(context)
11
+ private val title = NativeListTextView(context)
12
+ private val accessories = NativeListAccessoryStack(context)
13
+ override val assetFields = listOf("icon")
14
+
15
+ init {
16
+ visual.glyphSize = 24
17
+ visual.iconBorder = false
18
+ orientation = HORIZONTAL
19
+ gravity = Gravity.CENTER_VERTICAL
20
+ addView(visual)
21
+ addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
22
+ addView(accessories)
23
+ accessories.onAction = { key, view, slot, target ->
24
+ emitAction(key, view, "trailingAccessory", slot, target, accessories.anchorInset(view))
25
+ }
26
+ }
27
+
28
+ private val titlePaintFlags = title.paintFlags
29
+ private val fallbackPaintFlags = visual.fallbackTextView.paintFlags
30
+
31
+ override fun defaultHeight(item: NativeListItem, layout: String) =
32
+ when {
33
+ item.json.optString("presentation") == "accountSelector" -> 48
34
+ item.json.has("icon") -> 60
35
+ else -> 44
36
+ }
37
+
38
+ override fun bindContent(
39
+ item: NativeListItem,
40
+ theme: JSONObject?,
41
+ layout: String,
42
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
43
+ ) {
44
+ val style = item.json.optJSONObject("style") ?: JSONObject()
45
+ val icon = item.json.optJSONObject("icon")
46
+ val alignment = style.optJSONObject("container")?.optString("contentVerticalAlignment")
47
+ gravity =
48
+ when (alignment) {
49
+ "top" -> Gravity.TOP
50
+ "bottom" -> Gravity.BOTTOM
51
+ else -> Gravity.CENTER_VERTICAL
52
+ }
53
+ val selector = item.json.optString("presentation") == "accountSelector"
54
+ visual.roundedGlyphOrigin = selector && item.hasExplicitHeight
55
+ val descriptors =
56
+ item.json.optJSONObject("checkbox")?.let { JSONArray().put(it) }
57
+ ?: item.json.optJSONArray("trailing")
58
+ ?: JSONArray()
59
+ accessories.bind(item, descriptors, theme, style, sourceScale, checkboxState)
60
+ // Legacy: overhanging selector icons/checkboxes must not be clipped by row padding.
61
+ clipToPadding = !accessories.requestsUnclippedHost
62
+ val hp =
63
+ if (style.has("horizontalPadding")) stylePx(style.optDouble("horizontalPadding"))
64
+ else dp(if (layout == "table") 16 else 12)
65
+ val vp =
66
+ if (style.has("verticalPadding")) stylePx(style.optDouble("verticalPadding")) else dp(8)
67
+ setPadding(hp, vp, if (style.has("horizontalPadding")) hp else accessories.endInset ?: hp, vp)
68
+ val titleParams = title.layoutParams as LayoutParams
69
+ // Legacy: the weighted title abuts the trailing accessories without a gap.
70
+ titleParams.marginEnd = 0
71
+ title.layoutParams = titleParams
72
+ val tone = item.json.optString("tone")
73
+ val token =
74
+ if (selector && tone != "primary") "secondaryText"
75
+ else if (!selector && tone == "danger") "negative" else "primaryText"
76
+ val fallback =
77
+ if (token == "negative") "#C40006D3"
78
+ else if (selector || token == "secondaryText") "#0000009B" else "#000000DF"
79
+ NativeListResolvedText.resolve(
80
+ context,
81
+ item.json.optString("title"),
82
+ style.optJSONObject("title"),
83
+ 16f,
84
+ if (selector && icon == null) "regular" else "medium",
85
+ parseNativeListColor(theme?.optString(token, fallback) ?: fallback),
86
+ 24,
87
+ 1,
88
+ sourceScale,
89
+ )
90
+ .bind(title)
91
+ title.paintFlags = titlePaintFlags
92
+ // Legacy selector typography covered every text in the row, including the
93
+ // trailing accessory text; explicit style fontSize/lineHeight stay as set.
94
+ if (selector && sourceScale) {
95
+ applyNativeListSourceTypography(title, style.optJSONObject("title"))
96
+ accessories.applySourceTypography(style)
97
+ }
98
+ visual.visibility = if (icon == null) GONE else VISIBLE
99
+ // Legacy: the 1px icon outline is kept only for a non-selector icon with an
100
+ // explicit background; selector icons use a plain 8dp rounded fill.
101
+ visual.iconBorder = !selector && icon?.has("backgroundColor") == true
102
+ if (icon != null) {
103
+ val source = JSONObject(icon.toString())
104
+ if (!source.has("backgroundColor"))
105
+ source.put(
106
+ "backgroundColor",
107
+ if (selector) theme?.optString("strongBackground", "#0000000F") ?: "#0000000F"
108
+ else "#00000000",
109
+ )
110
+ val image = JSONObject(style.optJSONObject("image")?.toString() ?: "{}")
111
+ if (selector && !image.has("cornerRadius") && !image.has("shape"))
112
+ image.put("cornerRadius", scaledDp(8f) / resources.displayMetrics.density)
113
+ visual.layoutParams =
114
+ LayoutParams(
115
+ if (image.has("width")) stylePx(image.optDouble("width"))
116
+ else dp(if (selector) 32 else 40),
117
+ if (image.has("height")) stylePx(image.optDouble("height"))
118
+ else dp(if (selector) 32 else 40),
119
+ )
120
+ .apply {
121
+ marginEnd =
122
+ if (style.has("leadingGap")) stylePx(style.optDouble("leadingGap"))
123
+ // Legacy: Yoga rounds the cumulative selector edges, not each gap.
124
+ else if (selector && item.hasExplicitHeight) dp(56) - dp(12) - dp(32)
125
+ else dp(12)
126
+ }
127
+ visual.bind(source, image, item.key, theme, false, sourceScale)
128
+ } else visual.recycle()
129
+ visual.fallbackTextView.paintFlags = fallbackPaintFlags
130
+ // Legacy selector typography also covered the leading fallback text.
131
+ if (selector && sourceScale && icon != null)
132
+ applyNativeListSourceTypography(visual.fallbackTextView, null)
133
+ }
134
+
135
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
136
+ super.onLayout(changed, l, t, r, b)
137
+ val item = tag as? NativeListItem ?: return
138
+ if (
139
+ item.hasExplicitHeight &&
140
+ item.json.optString("presentation") == "accountSelector" &&
141
+ item.json
142
+ .optJSONObject("style")
143
+ ?.optJSONObject("container")
144
+ ?.has("contentVerticalAlignment") != true
145
+ ) {
146
+ for (view in listOf(title, accessories)) if (view.visibility != GONE)
147
+ view.offsetTopAndBottom(
148
+ paddingTop + (height - paddingTop - paddingBottom - view.height + 1) / 2 - view.top
149
+ )
150
+ }
151
+ }
152
+
153
+ override fun bindSelectionContent(
154
+ item: NativeListItem,
155
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
156
+ ) {
157
+ accessories.updateSelection(item, checkboxState)
158
+ }
159
+
160
+ override fun recycleContent() {
161
+ visual.recycle()
162
+ accessories.reset()
163
+ }
164
+
165
+ override fun disposeContent() {
166
+ visual.dispose()
167
+ accessories.reset()
168
+ }
169
+ }
@@ -0,0 +1,243 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.drawable.GradientDrawable
4
+ import android.text.TextUtils
5
+ import android.view.Gravity
6
+ import android.widget.LinearLayout
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import org.json.JSONObject
9
+
10
+ internal class NativeListActivityRowView(context: ThemedReactContext) :
11
+ NativeListRendererRowView(context) {
12
+ private val content =
13
+ LinearLayout(context).apply {
14
+ orientation = HORIZONTAL
15
+ gravity = Gravity.CENTER_VERTICAL
16
+ }
17
+ private val visual = NativeListLeadingVisual(context)
18
+ private val column = LinearLayout(context).apply { orientation = VERTICAL }
19
+ private val titleLine =
20
+ LinearLayout(context).apply {
21
+ orientation = HORIZONTAL
22
+ gravity = Gravity.CENTER_VERTICAL
23
+ }
24
+ private val title = NativeListTextView(context)
25
+ private val description = NativeListTextView(context)
26
+ private val status = NativeListTextView(context)
27
+ private val failure = NativeListTextView(context)
28
+ private val amounts =
29
+ LinearLayout(context).apply {
30
+ orientation = VERTICAL
31
+ gravity = Gravity.END
32
+ }
33
+ private val values = List(2) { NativeListTextView(context) }
34
+ private val actions = LinearLayout(context).apply { orientation = HORIZONTAL }
35
+ private val buttons = List(3) { NativeListTextView(context) }
36
+ override val assetFields = listOf("leading", "secondaryLeading")
37
+
38
+ init {
39
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
40
+ titleLine.addView(failure)
41
+ column.addView(titleLine)
42
+ column.addView(description)
43
+ column.addView(status)
44
+ values.forEach(amounts::addView)
45
+ buttons.forEach(actions::addView)
46
+ content.addView(visual)
47
+ content.addView(column, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
48
+ content.addView(amounts)
49
+ addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
50
+ addView(actions)
51
+ }
52
+
53
+ override fun defaultHeight(item: NativeListItem, layout: String) =
54
+ if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
55
+
56
+ override fun bindContent(
57
+ item: NativeListItem,
58
+ theme: JSONObject?,
59
+ layout: String,
60
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
61
+ ) {
62
+ val data = item.json
63
+ val style = data.optJSONObject("style") ?: JSONObject()
64
+ val image = style.optJSONObject("image") ?: JSONObject()
65
+ fun color(name: String, fallback: String) =
66
+ parseNativeListColor(theme?.optString(name, fallback) ?: fallback)
67
+ fun fill(value: Int, radius: Int) =
68
+ GradientDrawable().apply {
69
+ setColor(value)
70
+ cornerRadius = dp(radius).toFloat()
71
+ }
72
+ fun bind(
73
+ view: NativeListTextView,
74
+ value: String,
75
+ slot: String,
76
+ size: Float,
77
+ weight: String = "regular",
78
+ ink: Int = color("secondaryText", "#0000009B"),
79
+ line: Int = 0,
80
+ lines: Int = 1,
81
+ align: String = "start",
82
+ ) {
83
+ val textStyle =
84
+ JSONObject(style.optJSONObject(slot)?.toString() ?: "{}").apply {
85
+ if (!has("alignment")) put("alignment", align)
86
+ }
87
+ NativeListResolvedText.resolve(
88
+ context,
89
+ value,
90
+ textStyle,
91
+ size,
92
+ weight,
93
+ ink,
94
+ line,
95
+ lines,
96
+ sourceScale,
97
+ )
98
+ .bind(view)
99
+ view.ellipsize =
100
+ if (textStyle.optString("truncate") == "clip") null else TextUtils.TruncateAt.END
101
+ }
102
+ orientation = VERTICAL
103
+ val hasFooterActions = (data.optJSONArray("footerActions")?.length() ?: 0) > 0
104
+ gravity =
105
+ when (style.optJSONObject("container")?.optString("contentVerticalAlignment")) {
106
+ "bottom" -> Gravity.BOTTOM
107
+ "top" -> Gravity.TOP
108
+ "center" -> Gravity.CENTER_VERTICAL
109
+ // Legacy: the footer-action column is top/start aligned.
110
+ else -> if (hasFooterActions) Gravity.START else Gravity.CENTER_VERTICAL
111
+ }
112
+ content.gravity =
113
+ when (style.optJSONObject("container")?.optString("contentVerticalAlignment")) {
114
+ "bottom" -> Gravity.BOTTOM
115
+ "top" -> Gravity.TOP
116
+ else -> Gravity.CENTER_VERTICAL
117
+ }
118
+ val hp =
119
+ if (style.has("horizontalPadding")) stylePx(style.optDouble("horizontalPadding"))
120
+ else dp(if (layout == "table") 16 else 12)
121
+ val vp =
122
+ if (style.has("verticalPadding")) stylePx(style.optDouble("verticalPadding")) else dp(8)
123
+ setPadding(hp, vp, hp, vp)
124
+ val iw = if (image.has("width")) stylePx(image.optDouble("width")) else dp(40)
125
+ val ih = if (image.has("height")) stylePx(image.optDouble("height")) else dp(40)
126
+ val gap = if (style.has("leadingGap")) stylePx(style.optDouble("leadingGap")) else dp(12)
127
+ visual.layoutParams = LayoutParams(iw, ih).apply { marginEnd = gap }
128
+ visual.bind(
129
+ // Legacy: a missing visual still reserves an empty, transparent slot.
130
+ data.optJSONObject("leading") ?: JSONObject().put("backgroundColor", "#00000000"),
131
+ image,
132
+ item.key,
133
+ theme,
134
+ false,
135
+ sourceScale,
136
+ data.optJSONObject("secondaryLeading"),
137
+ )
138
+ bind(
139
+ title,
140
+ data.optString("title"),
141
+ "title",
142
+ 16f,
143
+ "medium",
144
+ color("primaryText", "#000000DF"),
145
+ 24,
146
+ )
147
+ bind(description, data.optString("description"), "description", 14f, line = 20, lines = 2)
148
+ val failed = data.optString("status") == "Failed"
149
+ bind(status, if (failed) "" else data.optString("status"), "status", 12f)
150
+ bind(
151
+ failure,
152
+ if (failed) "Failed" else "",
153
+ "status",
154
+ 12f,
155
+ "medium",
156
+ color("negative", "#C40006D3"),
157
+ )
158
+ failure.background = fill(color("criticalBackground", "#F3000D14"), 4)
159
+ failure.setPadding(dp(8), dp(2), dp(8), dp(2))
160
+ (failure.layoutParams as LayoutParams).apply {
161
+ marginStart =
162
+ if (style.has("titleBadgeGap")) stylePx(style.optDouble("titleBadgeGap")) else 0
163
+ }
164
+ for (view in listOf(description, status)) (view.layoutParams as LayoutParams).apply {
165
+ topMargin = if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else 0
166
+ }
167
+ for ((index, slot) in listOf("primaryAmount", "secondaryAmount").withIndex()) {
168
+ val value = data.optString(slot)
169
+ bind(
170
+ values[index],
171
+ value,
172
+ slot,
173
+ if (index == 0) 16f else 14f,
174
+ if (index == 0) "medium" else "regular",
175
+ if (index == 0)
176
+ color(
177
+ if (value.startsWith("+")) "positive" else "primaryText",
178
+ if (value.startsWith("+")) "#00713FDE" else "#000000DF",
179
+ )
180
+ // Legacy: both amounts default to primaryText; only "+" amounts are positive.
181
+ else color("primaryText", "#000000DF"),
182
+ line = if (index == 0) 24 else 20,
183
+ align = "end",
184
+ )
185
+ values[index].layoutParams =
186
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
187
+ if (index > 0)
188
+ topMargin = if (style.has("trailingGap")) stylePx(style.optDouble("trailingGap")) else 0
189
+ }
190
+ }
191
+ val descriptors = data.optJSONArray("footerActions")
192
+ val count = minOf(3, descriptors?.length() ?: 0)
193
+ actions.visibility = if (count == 0) GONE else VISIBLE
194
+ actions.layoutParams =
195
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
196
+ marginStart = if (style.has("leadingGap") || image.has("width")) iw + gap else dp(52)
197
+ topMargin = if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else dp(8)
198
+ bottomMargin = dp(4)
199
+ }
200
+ for ((index, button) in buttons.withIndex()) {
201
+ button.visibility = if (index < count) VISIBLE else GONE
202
+ button.setOnClickListener(null)
203
+ if (index >= count) continue
204
+ val action = descriptors!!.getJSONObject(index)
205
+ NativeListResolvedText.resolve(
206
+ context,
207
+ action.optString("label"),
208
+ null,
209
+ // Legacy footer action: 14sp semibold on a 20dp line.
210
+ 14f,
211
+ "semibold",
212
+ color(
213
+ if (action.optString("tone") == "danger") "negative" else "primaryText",
214
+ if (action.optString("tone") == "danger") "#C40006D3" else "#000000DF",
215
+ ),
216
+ 20,
217
+ 1,
218
+ sourceScale,
219
+ )
220
+ .bind(button)
221
+ button.background = fill(color("strongBackground", "#0000000F"), 8)
222
+ button.setPadding(dp(8), dp(4), dp(8), dp(4))
223
+ button.isEnabled = !data.optBoolean("disabled") && !action.optBoolean("disabled")
224
+ button.layoutParams =
225
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
226
+ marginEnd = dp(8)
227
+ }
228
+ button.setOnClickListener {
229
+ emitAction(action.optString("key"), button, "footerAction", index)
230
+ }
231
+ }
232
+ }
233
+
234
+ override fun recycleContent() {
235
+ visual.recycle()
236
+ buttons.forEach { it.setOnClickListener(null) }
237
+ }
238
+
239
+ override fun disposeContent() {
240
+ visual.dispose()
241
+ buttons.forEach { it.setOnClickListener(null) }
242
+ }
243
+ }
@@ -13,6 +13,9 @@ import java.util.WeakHashMap
13
13
  import kotlin.math.ceil
14
14
  import kotlin.math.roundToInt
15
15
 
16
+ internal class NativeListViewHolder(val rowView: NativeListRowHost) :
17
+ androidx.recyclerview.widget.RecyclerView.ViewHolder(rowView)
18
+
16
19
  internal class NativeListAdapter(
17
20
  private val context: ThemedReactContext,
18
21
  ) : RecyclerView.Adapter<NativeListViewHolder>() {
@@ -44,7 +47,7 @@ internal class NativeListAdapter(
44
47
  private var marketSourceItems: List<NativeListItem>? = null
45
48
  private var marketSourceEdges = DoubleArray(0)
46
49
  private val createdRows = Collections.newSetFromMap(
47
- WeakHashMap<NativeListRowView, Boolean>(),
50
+ WeakHashMap<NativeListRowHost, Boolean>(),
48
51
  )
49
52
  var usesSelectorSourceScale = false
50
53
  var theme: JSONObject? = null
@@ -52,18 +55,28 @@ internal class NativeListAdapter(
52
55
  if (field?.toString() != value?.toString()) needsThemeRebind = true
53
56
  field = value
54
57
  }
58
+ // Reuses the theme rebind path: chrome lives outside the row payload, so a
59
+ // changed list style would otherwise not reach rows DiffUtil considers equal.
60
+ var listStyle: JSONObject? = null
61
+ set(value) {
62
+ if (field?.toString() != value?.toString()) needsThemeRebind = true
63
+ field = value
64
+ }
55
65
  var layout: String = "linear"
56
66
  var orientation: String = "vertical"
57
67
  var selectedKeys: Set<String> = emptySet()
58
68
  var checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String = { _, _, fallback -> fallback }
59
69
  var onRowPress: ((NativeListItem, NativeListActionOrigin) -> Unit)? = null
60
70
  var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Unit)? = null
61
- var onBindingInvalidated: ((NativeListRowView, Long) -> Unit)? = null
71
+ var onBindingInvalidated: ((NativeListRowHost, Long) -> Unit)? = null
62
72
 
63
73
  override fun getItemCount(): Int = reorderItems?.size ?: differ.currentList.size
64
74
 
75
+ override fun getItemViewType(position: Int): Int =
76
+ requireNotNull(itemAt(position)).rendererKey.ordinal
77
+
65
78
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NativeListViewHolder {
66
- val view = NativeListRowView(context)
79
+ val view = NativeListRendererRegistry.create(context, viewType)
67
80
  view.layoutParams = ViewGroup.LayoutParams(
68
81
  ViewGroup.LayoutParams.MATCH_PARENT,
69
82
  ViewGroup.LayoutParams.WRAP_CONTENT,
@@ -79,6 +92,7 @@ internal class NativeListAdapter(
79
92
 
80
93
  override fun onBindViewHolder(holder: NativeListViewHolder, position: Int) {
81
94
  val item = itemAt(position) ?: return
95
+ holder.rowView.listStyle = listStyle
82
96
  holder.rowView.bind(
83
97
  item,
84
98
  theme,
@@ -135,7 +149,7 @@ internal class NativeListAdapter(
135
149
  items.forEachIndexed { index, item ->
136
150
  val style = item.json.optJSONObject("style")
137
151
  val height = item.json.optDouble("height", 0.0)
138
- val sourceHeight = if (item.type == "market" && style != null) {
152
+ val sourceHeight = item.styledHeight ?: if (item.type == "market" && style != null) {
139
153
  val titleHeight = ceil((style.optJSONObject("title")?.optDouble("lineHeight", 24.0) ?: 24.0) * density) / density
140
154
  val subtitleHeight = if (item.json.optString("subtitle").isNotEmpty() || item.json.optJSONObject("subtitlePrefix") != null) {
141
155
  ceil((style.optJSONObject("subtitle")?.optDouble("lineHeight", 20.0) ?: 20.0) * density) / density + style.optDouble("lineGap", 0.0)
@@ -201,14 +215,14 @@ internal class NativeListAdapter(
201
215
  marketSourceEdges = DoubleArray(0)
202
216
  reorderItems = null
203
217
  suppressDifferUpdates = false
204
- createdRows.forEach(NativeListRowView::dispose)
218
+ createdRows.forEach(NativeListRowHost::dispose)
205
219
  createdRows.clear()
206
220
  }
207
221
 
208
222
  companion object {
209
223
  private val DIFF = object : DiffUtil.ItemCallback<NativeListItem>() {
210
224
  override fun areItemsTheSame(oldItem: NativeListItem, newItem: NativeListItem): Boolean =
211
- oldItem.key == newItem.key && oldItem.type == newItem.type
225
+ oldItem.key == newItem.key
212
226
 
213
227
  override fun areContentsTheSame(oldItem: NativeListItem, newItem: NativeListItem): Boolean =
214
228
  oldItem.revision == newItem.revision && oldItem.content == newItem.content