@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.
- package/README.md +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- 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
|
+
}
|