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