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

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,464 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.animation.ValueAnimator
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.LinearGradient
7
+ import android.graphics.Matrix
8
+ import android.graphics.Paint
9
+ import android.graphics.RectF
10
+ import android.graphics.Shader
11
+ import android.graphics.drawable.GradientDrawable
12
+ import android.view.Gravity
13
+ import android.view.View
14
+ import android.view.animation.LinearInterpolator
15
+ import android.widget.LinearLayout
16
+ import android.widget.ProgressBar
17
+ import com.facebook.react.uimanager.ThemedReactContext
18
+ import kotlin.math.ceil
19
+ import kotlin.math.roundToInt
20
+ import org.json.JSONObject
21
+
22
+ /** System owns status content; placement and actions remain in the list host. */
23
+ internal class NativeListSystemRowView(context: ThemedReactContext) :
24
+ NativeListRendererRowView(context) {
25
+ private val title = NativeListTextView(context)
26
+ private val message = NativeListTextView(context)
27
+ private val action = NativeListTextView(context)
28
+ private val column = LinearLayout(context).apply { orientation = VERTICAL }
29
+ private val leading = View(context)
30
+ private val bars = List(2) { View(context) }
31
+
32
+ override fun defaultHeight(item: NativeListItem, layout: String): Int =
33
+ when (item.json.optString("variant")) {
34
+ "warning",
35
+ "spacer" -> 0
36
+ "loading" ->
37
+ when (item.json.optString("loadingStyle")) {
38
+ "skeleton" -> 56
39
+ "spinner" -> 52
40
+ else -> if (item.json.optString("presentation") == "market") 68 else 56
41
+ }
42
+ "noMatch",
43
+ "end" -> if (item.json.optString("presentation") == "market") 44 else 36
44
+ "retry" -> 44
45
+ else -> 56
46
+ }
47
+
48
+ override fun minimumContentHeight(item: NativeListItem, layout: String, sizeDelta: Int): Int {
49
+ val variant = item.json.optString("variant")
50
+ if (variant == "warning") return 0
51
+ if (item.json.optString("presentation") == "market" && variant in setOf("retry", "noMatch")) {
52
+ return stylePx(
53
+ if (variant == "noMatch") 88.0
54
+ else if (item.json.optString("message").isEmpty()) 52.0 else 120.0
55
+ )
56
+ }
57
+ return super.minimumContentHeight(item, layout, sizeDelta)
58
+ }
59
+
60
+ override fun modelHeight(item: NativeListItem, layout: String): Int? {
61
+ // Legacy market retry/noMatch: round(height * density).
62
+ if (
63
+ item.json.has("height") &&
64
+ item.json.optString("presentation") == "market" &&
65
+ item.json.optString("variant") in setOf("retry", "noMatch")
66
+ )
67
+ return stylePx(item.json.optDouble("height"))
68
+ if (
69
+ sourceScale &&
70
+ layout == "sectioned" &&
71
+ item.json.optString("variant") == "spacer" &&
72
+ !item.json.has("heightRounding")
73
+ )
74
+ return (item.json.optDouble("height") * resources.displayMetrics.density).toInt()
75
+ return super.modelHeight(item, layout)
76
+ }
77
+
78
+ private fun color(theme: JSONObject?, name: String, fallback: String) =
79
+ parseNativeListColor(theme?.optString(name, fallback) ?: fallback)
80
+
81
+ private fun fill(color: Int, radius: Int) =
82
+ GradientDrawable().apply {
83
+ setColor(color)
84
+ cornerRadius = dp(radius).toFloat()
85
+ }
86
+
87
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
88
+
89
+ // Legacy: market retry rows always use source (unscaled) dimensions.
90
+ override fun usesSourceScale(item: NativeListItem, provided: Boolean) =
91
+ (item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry") ||
92
+ super.usesSourceScale(item, provided)
93
+
94
+ override fun bindContent(
95
+ item: NativeListItem,
96
+ theme: JSONObject?,
97
+ layout: String,
98
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
99
+ ) {
100
+ recycleContent()
101
+ val data = item.json
102
+ val variant = data.optString("variant")
103
+ val market = data.optString("presentation") == "market"
104
+ val style = data.optJSONObject("style") ?: JSONObject()
105
+ orientation = HORIZONTAL
106
+ gravity = Gravity.CENTER_VERTICAL
107
+ val hp = dp(if (layout == "table") 16 else 12)
108
+ setPadding(hp, dp(8), hp, dp(8))
109
+ if (market) setPadding(dp(20), dp(12), dp(20), dp(12))
110
+ column.gravity = Gravity.CENTER_VERTICAL
111
+ val primary = color(theme, "primaryText", "#000000DF")
112
+ val secondary = color(theme, "secondaryText", "#0000009B")
113
+ fun text(
114
+ view: NativeListTextView,
115
+ value: String,
116
+ slot: String,
117
+ size: Float = 14f,
118
+ weight: String = "regular",
119
+ line: Int = 20,
120
+ lines: Int = 2,
121
+ alignment: String = "start",
122
+ foreground: Int = secondary,
123
+ physical: Boolean = false,
124
+ ) {
125
+ val override = style.optJSONObject(slot)
126
+ val merged =
127
+ JSONObject(override?.toString() ?: "{}").apply {
128
+ if (!has("alignment")) put("alignment", alignment)
129
+ }
130
+ var resolved =
131
+ NativeListResolvedText.resolve(
132
+ context,
133
+ value,
134
+ merged,
135
+ size,
136
+ weight,
137
+ foreground,
138
+ line,
139
+ lines,
140
+ sourceScale,
141
+ )
142
+ // Warning defaults wrap without a cap; explicit style line limits remain bounded.
143
+ if (override?.has("lines") != true) resolved = resolved.copy(lines = lines)
144
+ if (physical)
145
+ resolved =
146
+ resolved.copy(
147
+ fontSize =
148
+ if (override?.has("fontSize") == true) resolved.fontSize
149
+ else
150
+ ceil(
151
+ (resolved.fontSize * resources.displayMetrics.density /
152
+ resources.displayMetrics.scaledDensity)
153
+ .toDouble()
154
+ )
155
+ .toFloat(),
156
+ explicitLineHeight = true,
157
+ )
158
+ resolved.bind(view)
159
+ view.paintFlags =
160
+ if (physical) view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
161
+ else view.paintFlags and Paint.LINEAR_TEXT_FLAG.inv()
162
+ view.layoutParams = wrap()
163
+ }
164
+ when {
165
+ variant == "loading" && data.optString("loadingStyle") == "skeleton" -> {
166
+ setPadding(dp(20), dp(12), dp(20), dp(12))
167
+ addView(
168
+ NativeListMarketSkeleton(context, color(theme, "background", "#FFFFFF")),
169
+ LayoutParams(LayoutParams.MATCH_PARENT, dp(32)),
170
+ )
171
+ }
172
+ variant == "loading" && data.optString("loadingStyle") == "spinner" -> {
173
+ gravity = Gravity.CENTER
174
+ setPadding(0, dp(16), 0, dp(16))
175
+ addView(
176
+ ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
177
+ isIndeterminate = true
178
+ indeterminateTintList =
179
+ android.content.res.ColorStateList.valueOf(color(theme, "icon", "#0000009B"))
180
+ },
181
+ LayoutParams(dp(20), dp(20)),
182
+ )
183
+ }
184
+ market && variant == "retry" -> {
185
+ val value = data.optString("message")
186
+ orientation = VERTICAL
187
+ gravity = Gravity.CENTER
188
+ setPadding(
189
+ dp(32),
190
+ dp(if (value.isEmpty()) 11 else 32),
191
+ dp(32),
192
+ dp(if (value.isEmpty()) 11 else 27),
193
+ )
194
+ if (value.isNotEmpty()) {
195
+ // Legacy market text: one line, ellipsized unless style sets lines/truncate.
196
+ text(title, value, "message", 16f, line = 24, lines = 1, alignment = "center", physical = true)
197
+ if (!hasExplicitTruncation(style, "message"))
198
+ title.ellipsize = android.text.TextUtils.TruncateAt.END
199
+ column.addView(title)
200
+ addView(
201
+ column,
202
+ wrap().apply { bottomMargin = ceil(7.0 * resources.displayMetrics.density).toInt() },
203
+ )
204
+ }
205
+ text(
206
+ action,
207
+ data.optString("actionText", "Retry"),
208
+ "actionText",
209
+ 14f,
210
+ "medium",
211
+ 20,
212
+ 1,
213
+ "center",
214
+ physical = true,
215
+ )
216
+ // Legacy retry button ellipsizes; an explicit style truncate/lines wins.
217
+ if (!hasExplicitTruncation(style, "actionText"))
218
+ action.ellipsize = android.text.TextUtils.TruncateAt.END
219
+ val density = resources.displayMetrics.density
220
+ val width =
221
+ (ceil(action.paint.measureText(action.text.toString()).toDouble()) + 18 * density)
222
+ .roundToInt()
223
+ val height = ceil(ceil(20.0 * density) + 10 * density).toInt()
224
+ action.gravity = action.gravity or Gravity.CENTER_VERTICAL
225
+ addView(action, LayoutParams(width, height))
226
+ }
227
+ market && variant == "noMatch" -> {
228
+ gravity = Gravity.CENTER
229
+ setPadding(dp(32), dp(32), dp(32), dp(32))
230
+ text(
231
+ title,
232
+ data.optString("message"),
233
+ "message",
234
+ 16f,
235
+ line = 24,
236
+ lines = 1,
237
+ alignment = "center",
238
+ physical = true,
239
+ )
240
+ if (!hasExplicitTruncation(style, "message"))
241
+ title.ellipsize = android.text.TextUtils.TruncateAt.END
242
+ column.addView(title)
243
+ addView(column, wrap())
244
+ }
245
+ variant == "warning" -> {
246
+ setPadding(dp(12), dp(14), dp(12), dp(14))
247
+ text(
248
+ title,
249
+ data.optString("title"),
250
+ "title",
251
+ 14f,
252
+ "medium",
253
+ lines = Int.MAX_VALUE,
254
+ foreground = primary,
255
+ physical = true,
256
+ )
257
+ text(message, data.optString("message"), "message", lines = Int.MAX_VALUE, physical = true)
258
+ column.addView(title, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
259
+ column.addView(
260
+ message,
261
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
262
+ topMargin = if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else dp(4)
263
+ },
264
+ )
265
+ addView(column, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
266
+ }
267
+ variant == "spacer" -> Unit
268
+ variant == "end" -> {
269
+ gravity = Gravity.CENTER
270
+ setPadding(0, dp(16), 0, dp(16))
271
+ val ink = color(theme, "separator", "#0000001F")
272
+ listOf(80, 4, 80).forEach { w ->
273
+ addView(
274
+ View(context).apply { background = fill(ink, if (w == 4) 2 else 0) },
275
+ LayoutParams(dp(w), if (w == 4) dp(4) else 1).apply {
276
+ if (w == 4) {
277
+ marginStart = dp(8)
278
+ marginEnd = dp(8)
279
+ }
280
+ },
281
+ )
282
+ }
283
+ }
284
+ else -> {
285
+ gravity = Gravity.CENTER
286
+ if (variant == "loading") {
287
+ val size = if (market) 32 else 40
288
+ leading.background = fill(color(theme, "strongBackground", "#0000000F"), size / 2)
289
+ addView(
290
+ leading,
291
+ LayoutParams(dp(size), dp(size)).apply {
292
+ marginEnd =
293
+ if (style.has("leadingGap")) stylePx(style.optDouble("leadingGap")) else dp(12)
294
+ },
295
+ )
296
+ }
297
+ text(
298
+ title,
299
+ data.optString("message"),
300
+ "message",
301
+ alignment = if (variant in setOf("retry", "noMatch")) "start" else "center",
302
+ foreground = if (variant == "loading") primary else secondary,
303
+ )
304
+ column.addView(title)
305
+ if (variant == "loading")
306
+ bars.forEachIndexed { index, view ->
307
+ view.background = fill(color(theme, "strongBackground", "#0000000F"), 6)
308
+ column.addView(
309
+ view,
310
+ LayoutParams(dp(if (index == 0) 120 else 80), dp(12)).apply {
311
+ if (index == 0)
312
+ bottomMargin =
313
+ if (style.has("lineGap")) stylePx(style.optDouble("lineGap")) else dp(8)
314
+ },
315
+ )
316
+ }
317
+ if (variant == "noMatch") setPadding(dp(12), 0, dp(12), 0)
318
+ if (variant == "retry") setPadding(dp(12), dp(8), dp(12), dp(8))
319
+ addView(
320
+ column,
321
+ if (variant in setOf("retry", "noMatch")) LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
322
+ else wrap(),
323
+ )
324
+ if (variant == "retry") {
325
+ text(
326
+ action,
327
+ // Legacy: only the market retry reads actionText; this label is fixed.
328
+ "Retry",
329
+ "actionText",
330
+ 14f,
331
+ "medium",
332
+ 20,
333
+ 1,
334
+ "end",
335
+ primary,
336
+ )
337
+ action.background = fill(color(theme, "strongBackground", "#0000000F"), 16)
338
+ action.setPadding(dp(10), dp(4), dp(10), dp(4))
339
+ addView(action, wrap().apply { marginStart = dp(12) })
340
+ }
341
+ }
342
+ }
343
+ action.setOnClickListener {
344
+ // Legacy Retry was a text accessory: gated by the row's disabled state.
345
+ if (!data.optBoolean("disabled")) emitAction(data.optString("actionKey"), action, "trailingAccessory", 0)
346
+ }
347
+ if (style.has("horizontalPadding")) {
348
+ val padding = stylePx(style.optDouble("horizontalPadding"))
349
+ setPadding(padding, paddingTop, padding, paddingBottom)
350
+ }
351
+ if (style.has("verticalPadding")) {
352
+ val padding = stylePx(style.optDouble("verticalPadding"))
353
+ setPadding(paddingLeft, padding, paddingRight, padding)
354
+ }
355
+ style
356
+ .optJSONObject("container")
357
+ ?.optString("contentVerticalAlignment")
358
+ ?.takeIf { it.isNotEmpty() }
359
+ ?.let { alignment ->
360
+ gravity =
361
+ (gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()) or
362
+ when (alignment) {
363
+ "top" -> Gravity.TOP
364
+ "bottom" -> Gravity.BOTTOM
365
+ else -> Gravity.CENTER_VERTICAL
366
+ }
367
+ }
368
+ }
369
+
370
+ override fun recycleContent() {
371
+ removeAllViews()
372
+ column.removeAllViews()
373
+ action.background = null
374
+ action.setPadding(0, 0, 0, 0)
375
+ action.setOnClickListener(null)
376
+ }
377
+
378
+ override fun disposeContent() = recycleContent()
379
+ }
380
+
381
+ // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
382
+ private class NativeListMarketSkeleton(context: android.content.Context, backgroundColor: Int) :
383
+ View(context) {
384
+ private val marks = Array(5) { RectF() }
385
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
386
+ private val matrix = Matrix()
387
+ private val shaders = arrayOfNulls<LinearGradient>(5)
388
+ private val dark =
389
+ Color.red(backgroundColor) * 0.299 +
390
+ Color.green(backgroundColor) * 0.587 +
391
+ Color.blue(backgroundColor) * 0.114 < 128
392
+ private val colors =
393
+ intArrayOf(
394
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
395
+ Color.parseColor(if (dark) "#333333" else "#CDCDCD"),
396
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
397
+ )
398
+ private var phase = 0f
399
+ private val animator =
400
+ ValueAnimator.ofFloat(0f, 1f).apply {
401
+ duration = 3000
402
+ repeatCount = ValueAnimator.INFINITE
403
+ interpolator = LinearInterpolator()
404
+ addUpdateListener {
405
+ phase = it.animatedValue as Float
406
+ invalidate()
407
+ }
408
+ }
409
+
410
+ private fun dp(value: Float) = NativeListScale.dp(resources, value)
411
+
412
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
413
+ super.onSizeChanged(w, h, oldw, oldh)
414
+ marks[0].set(0f, 0f, dp(32f), dp(32f))
415
+ marks[1].set(dp(44f), 0f, dp(124f), dp(16f))
416
+ marks[2].set(dp(44f), dp(20f), dp(104f), dp(32f))
417
+ marks[3].set(w - dp(168f), dp(7f), w - dp(88f), dp(25f))
418
+ marks[4].set(w - dp(80f), dp(7f), w.toFloat(), dp(25f))
419
+ marks.forEachIndexed { index, mark ->
420
+ shaders[index] =
421
+ LinearGradient(
422
+ 0f,
423
+ 0f,
424
+ mark.width(),
425
+ 0f,
426
+ colors,
427
+ floatArrayOf(0f, 0.5f, 1f),
428
+ Shader.TileMode.CLAMP,
429
+ )
430
+ }
431
+ }
432
+
433
+ override fun onDraw(canvas: Canvas) {
434
+ super.onDraw(canvas)
435
+ marks.forEachIndexed { index, mark ->
436
+ matrix.setTranslate(mark.left - mark.width() + phase * mark.width() * 3f, 0f)
437
+ shaders[index]?.setLocalMatrix(matrix)
438
+ paint.shader = shaders[index]
439
+ val radius = dp(if (index == 0) 16f else 8f)
440
+ canvas.drawRoundRect(mark, radius, radius, paint)
441
+ }
442
+ }
443
+
444
+ override fun onAttachedToWindow() {
445
+ super.onAttachedToWindow()
446
+ if (windowVisibility == VISIBLE) animator.start()
447
+ }
448
+
449
+ override fun onDetachedFromWindow() {
450
+ animator.cancel()
451
+ super.onDetachedFromWindow()
452
+ }
453
+
454
+ override fun onWindowVisibilityChanged(visibility: Int) {
455
+ super.onWindowVisibilityChanged(visibility)
456
+ if (visibility == VISIBLE && isAttachedToWindow) {
457
+ if (!animator.isStarted) animator.start()
458
+ } else animator.cancel()
459
+ }
460
+ }
461
+
462
+ /** True when the caller's text style for [slot] sets its own truncation (`lines` / `truncate`). */
463
+ private fun hasExplicitTruncation(style: JSONObject, slot: String): Boolean =
464
+ style.optJSONObject(slot)?.let { it.has("lines") || it.has("truncate") } == true
@@ -0,0 +1,25 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.graphics.Canvas
4
+ import android.widget.TextView
5
+
6
+ internal open class NativeListTextView(context: android.content.Context) : TextView(context) {
7
+ var rowHorizontallyScrolling = false
8
+ private set
9
+
10
+ override fun setHorizontallyScrolling(whether: Boolean) {
11
+ rowHorizontallyScrolling = whether
12
+ super.setHorizontallyScrolling(whether)
13
+ }
14
+
15
+ var opticalOffsetY = 0f
16
+ set(value) { field = value; invalidate() }
17
+
18
+ override fun onDraw(canvas: Canvas) {
19
+ if (opticalOffsetY == 0f) { super.onDraw(canvas); return }
20
+ val checkpoint = canvas.save()
21
+ canvas.translate(0f, opticalOffsetY)
22
+ super.onDraw(canvas)
23
+ canvas.restoreToCount(checkpoint)
24
+ }
25
+ }