@onekeyfe/react-native-native-list 6.7.0-alpha.397 → 6.7.0-alpha.399
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/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +43 -15
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +88 -19
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +94 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +29 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListActivityHostClipPolicyTest.kt +29 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListIdentityTrailingAxisTest.kt +36 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListLeadingSlotFrameTest.kt +68 -0
- package/docs/ROW_TEMPLATES.md +4 -0
- package/docs/SPEC.md +180 -37
- package/ios/NativeListDesignAssets.swift +6 -0
- package/ios/NativeListIdentityRenderer.swift +76 -4
- package/ios/NativeListLeadingVisual.swift +70 -8
- package/ios/RNCNativeListView.swift +6 -1
- package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/icon.svg +1 -0
- package/ios/tests/NativeListIdentityTitleIconLayoutTests.swift +81 -0
- package/ios/tests/NativeListLeadingVisualPairBackdropTests.swift +50 -0
- package/ios/tests/NativeListLifecycleTests.swift +60 -0
- package/lib/module/NativeList.web.js +9 -12
- package/lib/module/models.js +2 -0
- package/lib/module/validation.js +627 -114
- package/lib/module/web/NativeListWebEngine.js +49 -9
- package/lib/module/web/templates/IdentityRowRenderer.js +58 -4
- package/lib/typescript/src/models.d.ts +25 -14
- package/lib/typescript/src/validation.d.ts +7 -2
- package/package.json +3 -3
- package/src/NativeList.web.tsx +10 -14
- package/src/models.ts +144 -153
- package/src/validation.ts +1617 -167
- package/src/web/NativeListWebEngine.ts +62 -10
- package/src/web/templates/IdentityRowRenderer.ts +70 -4
|
@@ -54,6 +54,13 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
54
54
|
details.onAction = { key, view, source, slot -> emitAction(key, view, source, slot) }
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Host clipping for the current bind; see [nativeListActivityHostClips]. */
|
|
58
|
+
private fun setHostClipping(clips: Boolean) {
|
|
59
|
+
content.clipChildren = clips
|
|
60
|
+
clipChildren = clips
|
|
61
|
+
clipToPadding = clips
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
override fun defaultHeight(item: NativeListItem, layout: String) =
|
|
58
65
|
if (item.json.has("amounts")) 0 else if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
|
|
59
66
|
|
|
@@ -76,6 +83,10 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
76
83
|
setPadding(hp, vp, hp, vp)
|
|
77
84
|
visual.recycle()
|
|
78
85
|
details.bind(item, theme, sourceScale)
|
|
86
|
+
// This view may be reused from a legacy row whose overhanging badge
|
|
87
|
+
// switched clipping off; a rich row has no badge on the host, so restore
|
|
88
|
+
// the defaults before returning.
|
|
89
|
+
setHostClipping(nativeListActivityHostClips(rich = true, leadingOverhangs = false))
|
|
79
90
|
return
|
|
80
91
|
}
|
|
81
92
|
details.recycle()
|
|
@@ -157,6 +168,18 @@ internal class NativeListActivityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
157
168
|
sourceScale,
|
|
158
169
|
data.optJSONObject("secondaryLeading"),
|
|
159
170
|
)
|
|
171
|
+
// The token network badge hangs 4dp past the 40dp visual and Android clips a
|
|
172
|
+
// child to its own bounds while its parent clips children, so with the
|
|
173
|
+
// default clipping the history rows showed the badge cut flat (OK-66071).
|
|
174
|
+
// Stop clipping the content row and this row's padding exactly when the
|
|
175
|
+
// bound visual reports such a badge (SPEC "Leading badge overhang"), as the
|
|
176
|
+
// Identity renderer does.
|
|
177
|
+
setHostClipping(
|
|
178
|
+
nativeListActivityHostClips(
|
|
179
|
+
rich = false,
|
|
180
|
+
leadingOverhangs = !hideLeading && visual.requestsUnclippedHost,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
160
183
|
bind(
|
|
161
184
|
title,
|
|
162
185
|
data.optString("title"),
|
|
@@ -31,6 +31,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
31
31
|
private val titleLine = PackedTitleLineLayout(context)
|
|
32
32
|
private val title = NativeListTextView(context)
|
|
33
33
|
private val titleIcon = OneKeyIconView(context)
|
|
34
|
+
private var titleImage: NativeListImageSlot? = null
|
|
34
35
|
private val subtitle = NativeListTextView(context)
|
|
35
36
|
private val tertiary = NativeListTextView(context)
|
|
36
37
|
private val badgeLine = NativeListTextView(context)
|
|
@@ -79,7 +80,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
79
80
|
"single"
|
|
80
81
|
else item.json.optString("groupPosition")
|
|
81
82
|
|
|
82
|
-
override val assetFields = listOf("leading")
|
|
83
|
+
override val assetFields = listOf("leading", "titleImage")
|
|
83
84
|
|
|
84
85
|
override fun usesSourceScale(item: NativeListItem, provided: Boolean) =
|
|
85
86
|
item.usesSelectorSourceScale || provided
|
|
@@ -262,6 +263,29 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
262
263
|
leadingActionIcon.useSourceScale = sourceScale
|
|
263
264
|
leadingActionIcon.setOnClickListener(null)
|
|
264
265
|
bindIdentity(item, theme, item.json.optBoolean("selected"), checkboxState)
|
|
266
|
+
val titleImageDescriptor = item.json.optJSONObject("titleImage")
|
|
267
|
+
val titleImageSource = titleImageDescriptor?.optJSONObject("image")
|
|
268
|
+
if (titleImageSource != null && titleImageSource.optString("uri").isNotEmpty()) {
|
|
269
|
+
if (item.json.optString("presentation") == "walletSidebar") {
|
|
270
|
+
titleLine.packsChildrenAtStart = true
|
|
271
|
+
title.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
|
272
|
+
}
|
|
273
|
+
val slot = titleImage ?: NativeListImageSlot(reactContext).also { titleImage = it }
|
|
274
|
+
// Reinsert after icons so newly added suffixes cannot precede the image.
|
|
275
|
+
titleLine.removeView(slot.view)
|
|
276
|
+
titleLine.addView(slot.view, titleLine.indexOfChild(titleIcon) + 1)
|
|
277
|
+
val size = stylePx(titleImageSource.optDouble("width"))
|
|
278
|
+
slot.view.layoutParams = LayoutParams(size, size).apply { marginStart = dp(4) }
|
|
279
|
+
slot.view.visibility = VISIBLE
|
|
280
|
+
slot.view.contentDescription = titleImageDescriptor.optString("accessibilityLabel").takeIf(String::isNotEmpty)
|
|
281
|
+
slot.view.importantForAccessibility = if (slot.view.contentDescription == null)
|
|
282
|
+
View.IMPORTANT_FOR_ACCESSIBILITY_NO else View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
|
283
|
+
slot.bind(titleImageSource, "${item.key}:titleImage", placeholder = "#00000000", round = true)
|
|
284
|
+
} else {
|
|
285
|
+
titleImage?.recycle()
|
|
286
|
+
titleImage?.view?.visibility = GONE
|
|
287
|
+
titleImage?.view?.contentDescription = null
|
|
288
|
+
}
|
|
265
289
|
TextViewCompat.setLineHeight(
|
|
266
290
|
title,
|
|
267
291
|
dp(if (item.json.optString("presentation") == "walletSidebar") 16 else 24),
|
|
@@ -449,7 +473,10 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
449
473
|
val inner = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
|
|
450
474
|
.coerceAtLeast(0)
|
|
451
475
|
val leading = leadingFrame.layoutParams as LayoutParams
|
|
452
|
-
val
|
|
476
|
+
val imageWidth = titleImage?.view?.takeIf { it.visibility == VISIBLE }?.let {
|
|
477
|
+
it.layoutParams.width + dp(4)
|
|
478
|
+
} ?: 0
|
|
479
|
+
val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth
|
|
453
480
|
(inner * ratio).roundToInt()
|
|
454
481
|
.minus(leading.width + leading.marginEnd + icon)
|
|
455
482
|
.coerceAtLeast(0)
|
|
@@ -555,6 +582,9 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
555
582
|
longPressPoint = null
|
|
556
583
|
longPressPointEpoch = -1L
|
|
557
584
|
leadingFrame.recycle()
|
|
585
|
+
titleImage?.recycle()
|
|
586
|
+
titleImage?.view?.visibility = GONE
|
|
587
|
+
titleImage?.view?.contentDescription = null
|
|
558
588
|
trailingColumn.reset()
|
|
559
589
|
currentItem = null
|
|
560
590
|
}
|
|
@@ -562,6 +592,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
562
592
|
override fun disposeContent() {
|
|
563
593
|
recycleContent()
|
|
564
594
|
leadingFrame.dispose()
|
|
595
|
+
titleImage?.dispose()
|
|
565
596
|
}
|
|
566
597
|
|
|
567
598
|
private fun bindIdentity(
|
|
@@ -878,19 +909,16 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
878
909
|
sourceScale,
|
|
879
910
|
checkboxState,
|
|
880
911
|
horizontal =
|
|
881
|
-
(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
accessories.getJSONObject(0).optString("name") == "PencilOutline" &&
|
|
892
|
-
accessories.getJSONObject(1).optString("name") == "DragOutline")
|
|
893
|
-
},
|
|
912
|
+
nativeListIdentityTrailingIsHorizontal(
|
|
913
|
+
(0 until accessories.length()).map { index ->
|
|
914
|
+
val accessory = accessories.getJSONObject(index)
|
|
915
|
+
NativeListTrailingDescriptor(
|
|
916
|
+
kind = accessory.optString("kind"),
|
|
917
|
+
name = accessory.optString("name"),
|
|
918
|
+
actionKey = accessory.optString("actionKey"),
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
),
|
|
894
922
|
)
|
|
895
923
|
trailingColumn.endInset?.let { setPadding(paddingLeft, paddingTop, it, paddingBottom) }
|
|
896
924
|
}
|
|
@@ -23,6 +23,8 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
23
23
|
private val icon = OneKeyIconView(context)
|
|
24
24
|
private val unread = View(context)
|
|
25
25
|
private val networkBackdrop = View(context)
|
|
26
|
+
// Ring under the secondary token of an Activity pair (SPEC "Activity leading pair").
|
|
27
|
+
private val pairBackdrop = View(context)
|
|
26
28
|
private val corner = OneKeyIconView(context)
|
|
27
29
|
private val cornerBackdrop = View(context)
|
|
28
30
|
private val slots = mutableListOf<NativeListImageSlot>()
|
|
@@ -32,6 +34,10 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
32
34
|
private var visual = JSONObject()
|
|
33
35
|
private var style = JSONObject()
|
|
34
36
|
private var sources = emptyList<Pair<JSONObject, String>>()
|
|
37
|
+
// Roles inside [sources]: the token `networkImage` badge and the Activity
|
|
38
|
+
// `secondaryLeading` token; null when absent.
|
|
39
|
+
private var networkIndex: Int? = null
|
|
40
|
+
private var pairIndex: Int? = null
|
|
35
41
|
val fallbackTextView: TextView
|
|
36
42
|
get() = fallback
|
|
37
43
|
|
|
@@ -90,7 +96,9 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
init {
|
|
93
|
-
listOf(fallback, icon, networkBackdrop, cornerBackdrop, corner, unread).forEach {
|
|
99
|
+
listOf(fallback, icon, pairBackdrop, networkBackdrop, cornerBackdrop, corner, unread).forEach {
|
|
100
|
+
addView(it)
|
|
101
|
+
}
|
|
94
102
|
clipChildren = false
|
|
95
103
|
clipToPadding = false
|
|
96
104
|
}
|
|
@@ -119,6 +127,8 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
119
127
|
"wallet" -> "avatar"
|
|
120
128
|
else -> "generic"
|
|
121
129
|
}
|
|
130
|
+
networkIndex = null
|
|
131
|
+
pairIndex = null
|
|
122
132
|
sources =
|
|
123
133
|
if (kind == "stackedImages")
|
|
124
134
|
visual
|
|
@@ -132,7 +142,10 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
132
142
|
else if (kind == "icon") emptyList()
|
|
133
143
|
else visual.optJSONObject("image")?.let { listOf(it to variant) }.orEmpty()
|
|
134
144
|
if (kind == "token" && sources.isNotEmpty())
|
|
135
|
-
visual.optJSONObject("networkImage")?.let {
|
|
145
|
+
visual.optJSONObject("networkImage")?.let {
|
|
146
|
+
networkIndex = sources.size
|
|
147
|
+
sources = sources + (it to "network")
|
|
148
|
+
}
|
|
136
149
|
secondaryVisual?.let { second ->
|
|
137
150
|
val kind = second.optString("kind")
|
|
138
151
|
val source =
|
|
@@ -141,7 +154,10 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
141
154
|
"icon" -> null
|
|
142
155
|
else -> second.optJSONObject("image")
|
|
143
156
|
}
|
|
144
|
-
if (source != null)
|
|
157
|
+
if (source != null) {
|
|
158
|
+
// A second token forms a pair only next to a primary image; without one
|
|
159
|
+
// it simply takes the primary's place (OK-66071).
|
|
160
|
+
if (this.visual.optString("kind") == "token" && sources.isNotEmpty()) pairIndex = sources.size
|
|
145
161
|
sources =
|
|
146
162
|
sources +
|
|
147
163
|
(source to
|
|
@@ -152,6 +168,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
152
168
|
"wallet" -> "avatar"
|
|
153
169
|
else -> "generic"
|
|
154
170
|
})
|
|
171
|
+
}
|
|
155
172
|
}
|
|
156
173
|
while (slots.size < sources.size) {
|
|
157
174
|
val slot = NativeListImageSlot(reactContext)
|
|
@@ -228,6 +245,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
228
245
|
) { loaded ->
|
|
229
246
|
slot.view.visibility =
|
|
230
247
|
if (!loaded && (index > 0 || sourceFallback)) INVISIBLE else VISIBLE
|
|
248
|
+
if (index == pairIndex) syncPairBackdrop()
|
|
231
249
|
if (index == 0 && sourceFallback) {
|
|
232
250
|
fallbackKey?.let {
|
|
233
251
|
if (loaded) NativeListSourceFallbackState.forget(it)
|
|
@@ -242,9 +260,21 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
242
260
|
}
|
|
243
261
|
}
|
|
244
262
|
}
|
|
245
|
-
networkBackdrop.visibility = if (
|
|
263
|
+
networkBackdrop.visibility = if (networkIndex != null) VISIBLE else GONE
|
|
246
264
|
networkBackdrop.background =
|
|
247
265
|
fill(color(theme?.optString("rowBackground", "#FFFFFF") ?: "#FFFFFF"), dp(10).toFloat())
|
|
266
|
+
pairBackdrop.background =
|
|
267
|
+
fill(color(theme?.optString("rowBackground", "#FFFFFF") ?: "#FFFFFF"), 0f)
|
|
268
|
+
syncPairBackdrop()
|
|
269
|
+
// Stacking order by role: primary, pair ring, secondary token, badge
|
|
270
|
+
// backdrop, badge, corner icon. Slots are reused across binds with
|
|
271
|
+
// different roles, so the order is restored on every bind.
|
|
272
|
+
pairBackdrop.bringToFront()
|
|
273
|
+
pairIndex?.let { slots[it].view.bringToFront() }
|
|
274
|
+
networkBackdrop.bringToFront()
|
|
275
|
+
networkIndex?.let { slots[it].view.bringToFront() }
|
|
276
|
+
cornerBackdrop.bringToFront()
|
|
277
|
+
corner.bringToFront()
|
|
248
278
|
val badge = visual.optJSONObject("cornerIcon")
|
|
249
279
|
corner.visibility = if (badge == null) GONE else VISIBLE
|
|
250
280
|
cornerBackdrop.visibility = corner.visibility
|
|
@@ -353,7 +383,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
353
383
|
(fallbackGlyph.parent as? android.view.ViewGroup)?.removeView(fallbackGlyph)
|
|
354
384
|
frame.addView(fallbackGlyph, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
355
385
|
}
|
|
356
|
-
if (frame.parent == null) addView(frame)
|
|
386
|
+
if (frame.parent == null) addView(frame) else frame.bringToFront()
|
|
357
387
|
overlays.add(frame to data)
|
|
358
388
|
}
|
|
359
389
|
previousOverlays.drop(descriptors.size).forEach { (frame, _) ->
|
|
@@ -426,28 +456,46 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
426
456
|
iconSize,
|
|
427
457
|
iconSize,
|
|
428
458
|
)
|
|
429
|
-
val
|
|
459
|
+
val ring = dp(2)
|
|
430
460
|
slots.take(sources.size).forEachIndexed { index, slot ->
|
|
431
|
-
val
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
461
|
+
val frame =
|
|
462
|
+
nativeListLeadingSlotFrame(
|
|
463
|
+
index = index,
|
|
464
|
+
sourceCount = sources.size,
|
|
465
|
+
networkIndex = networkIndex,
|
|
466
|
+
pairIndex = pairIndex,
|
|
467
|
+
width = w,
|
|
468
|
+
height = h,
|
|
469
|
+
badgeSize = dp(16),
|
|
470
|
+
badgeOverhang = dp(2),
|
|
471
|
+
pairRing = ring,
|
|
472
|
+
)
|
|
440
473
|
slot.view.measure(
|
|
441
|
-
MeasureSpec.makeMeasureSpec(
|
|
442
|
-
MeasureSpec.makeMeasureSpec(
|
|
474
|
+
MeasureSpec.makeMeasureSpec(frame.width, MeasureSpec.EXACTLY),
|
|
475
|
+
MeasureSpec.makeMeasureSpec(frame.height, MeasureSpec.EXACTLY),
|
|
443
476
|
)
|
|
444
477
|
val inset = if (index == 0) bitmapBorderWidth else 0
|
|
445
|
-
place(
|
|
478
|
+
place(
|
|
479
|
+
slot.view,
|
|
480
|
+
frame.x + inset,
|
|
481
|
+
frame.y + inset,
|
|
482
|
+
frame.width - 2 * inset,
|
|
483
|
+
frame.height - 2 * inset,
|
|
484
|
+
)
|
|
446
485
|
val imageRadius =
|
|
447
486
|
if (shape == "rounded" && !style.has("shape") && !style.has("cornerRadius"))
|
|
448
487
|
dp(roundedImageRadius).toFloat()
|
|
449
488
|
else radius
|
|
450
|
-
val
|
|
489
|
+
val fillsFrame = index == 0 && pairIndex == null && (sources.size == 1 || networkIndex != null)
|
|
490
|
+
val r =
|
|
491
|
+
when {
|
|
492
|
+
fillsFrame -> imageRadius
|
|
493
|
+
// Pair members keep the token shape at their own size (rounded NFTs
|
|
494
|
+
// use the box's 10dp rule scaled to the member).
|
|
495
|
+
pairIndex != null && index != networkIndex && shape == "rounded" ->
|
|
496
|
+
minOf(dp(roundedImageRadius).toFloat(), frame.height / 4f)
|
|
497
|
+
else -> frame.height / 2f
|
|
498
|
+
}
|
|
451
499
|
slot.view.outlineProvider =
|
|
452
500
|
object : ViewOutlineProvider() {
|
|
453
501
|
override fun getOutline(view: View, outline: Outline) {
|
|
@@ -458,6 +506,14 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
458
506
|
}
|
|
459
507
|
slot.view.clipToOutline = true
|
|
460
508
|
}
|
|
509
|
+
if (pairIndex != null) {
|
|
510
|
+
val member = nativeListLeadingPairMemberSize(h)
|
|
511
|
+
val box = member + 2 * ring
|
|
512
|
+
(pairBackdrop.background as? GradientDrawable)?.cornerRadius =
|
|
513
|
+
if (shape == "rounded") minOf(dp(roundedImageRadius).toFloat(), member / 4f) + ring
|
|
514
|
+
else box / 2f
|
|
515
|
+
place(pairBackdrop, w - box, h - box, box, box)
|
|
516
|
+
}
|
|
461
517
|
place(networkBackdrop, w - dp(16), h - dp(16), dp(20), dp(20))
|
|
462
518
|
place(cornerBackdrop, w - dp(16), h - dp(16), dp(20), dp(20))
|
|
463
519
|
place(corner, w - dp(15), h - dp(15), dp(18), dp(18))
|
|
@@ -487,9 +543,22 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
|
|
|
487
543
|
}
|
|
488
544
|
}
|
|
489
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The pair ring sits above the primary token, so it is shown only while the
|
|
548
|
+
* secondary token image is actually displayed: hidden while that image loads,
|
|
549
|
+
* after a failed load and once the slot is recycled, or it would cover the
|
|
550
|
+
* primary's lower-right corner with a blank row-background disc.
|
|
551
|
+
*/
|
|
552
|
+
private fun syncPairBackdrop() {
|
|
553
|
+
val secondary = pairIndex?.let { slots.getOrNull(it) }
|
|
554
|
+
pairBackdrop.visibility =
|
|
555
|
+
if (secondary != null && secondary.view.visibility == VISIBLE) VISIBLE else GONE
|
|
556
|
+
}
|
|
557
|
+
|
|
490
558
|
fun recycle() {
|
|
491
559
|
slots.forEach { it.recycle() }
|
|
492
560
|
overlaySlots.forEach { it.recycle() }
|
|
561
|
+
pairBackdrop.visibility = GONE
|
|
493
562
|
}
|
|
494
563
|
|
|
495
564
|
fun dispose() {
|
|
@@ -4,6 +4,7 @@ import org.json.JSONArray
|
|
|
4
4
|
import org.json.JSONObject
|
|
5
5
|
import java.nio.ByteBuffer
|
|
6
6
|
import java.security.MessageDigest
|
|
7
|
+
import kotlin.math.roundToInt
|
|
7
8
|
import java.util.Locale
|
|
8
9
|
|
|
9
10
|
internal data class NativeSelectionTarget(val scope: String, val key: String?)
|
|
@@ -48,6 +49,96 @@ internal fun nativeListLeadingVisualOverhangsFrame(
|
|
|
48
49
|
overlayCount: Int,
|
|
49
50
|
): Boolean = kind == "token" && sourceCount > 1 || hasCornerIcon || overlayCount > 0
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Whether an Activity row host clips its children and padding for this bind.
|
|
54
|
+
* One row view serves both the legacy row and the rich (`amounts`) row and is
|
|
55
|
+
* reused across binds, so clipping is derived from the bound item alone and
|
|
56
|
+
* applied on every bind: only a legacy row whose leading visual lays a badge
|
|
57
|
+
* past its frame ([nativeListLeadingVisualOverhangsFrame], OK-66071) opts out;
|
|
58
|
+
* a rich row keeps Android's default clipping even when the view it reuses
|
|
59
|
+
* last showed such a badge.
|
|
60
|
+
*/
|
|
61
|
+
internal fun nativeListActivityHostClips(rich: Boolean, leadingOverhangs: Boolean): Boolean =
|
|
62
|
+
rich || !leadingOverhangs
|
|
63
|
+
|
|
64
|
+
internal data class NativeListSlotFrame(val x: Int, val y: Int, val width: Int, val height: Int)
|
|
65
|
+
|
|
66
|
+
/** Side of one member of an Activity token pair inside a [frame]-unit box (24 in 40). */
|
|
67
|
+
internal fun nativeListLeadingPairMemberSize(frame: Int): Int = (frame * 0.6f).roundToInt()
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Frame of image source [index] of a leading visual inside its [width]×[height]
|
|
71
|
+
* box (SPEC "Activity leading pair", OK-66071). [networkIndex] is the token
|
|
72
|
+
* `networkImage` badge and [pairIndex] the Activity `secondaryLeading` token;
|
|
73
|
+
* either is null when absent. A pair draws the primary at the top-left and the
|
|
74
|
+
* secondary at the bottom-right, both [nativeListLeadingPairMemberSize] wide and
|
|
75
|
+
* the secondary inset by its [pairRing]; the badge keeps its corner geometry
|
|
76
|
+
* above them. A lone token, or a token with only a badge, fills the box. Any
|
|
77
|
+
* other multi-source visual (stackedImages) steps 0.72-sized images diagonally.
|
|
78
|
+
*/
|
|
79
|
+
internal fun nativeListLeadingSlotFrame(
|
|
80
|
+
index: Int,
|
|
81
|
+
sourceCount: Int,
|
|
82
|
+
networkIndex: Int?,
|
|
83
|
+
pairIndex: Int?,
|
|
84
|
+
width: Int,
|
|
85
|
+
height: Int,
|
|
86
|
+
badgeSize: Int,
|
|
87
|
+
badgeOverhang: Int,
|
|
88
|
+
pairRing: Int,
|
|
89
|
+
): NativeListSlotFrame {
|
|
90
|
+
if (index == networkIndex)
|
|
91
|
+
return NativeListSlotFrame(
|
|
92
|
+
width - badgeSize + badgeOverhang,
|
|
93
|
+
height - badgeSize + badgeOverhang,
|
|
94
|
+
badgeSize,
|
|
95
|
+
badgeSize,
|
|
96
|
+
)
|
|
97
|
+
if (pairIndex != null) {
|
|
98
|
+
val member = nativeListLeadingPairMemberSize(height)
|
|
99
|
+
return if (index == pairIndex)
|
|
100
|
+
NativeListSlotFrame(width - member - pairRing, height - member - pairRing, member, member)
|
|
101
|
+
else NativeListSlotFrame(0, 0, member, member)
|
|
102
|
+
}
|
|
103
|
+
if (sourceCount == 1 || networkIndex != null) return NativeListSlotFrame(0, 0, width, height)
|
|
104
|
+
val side = (height * 0.72f).roundToInt()
|
|
105
|
+
val x = (index * (width - side).toFloat() / maxOf(1, sourceCount - 1)).roundToInt()
|
|
106
|
+
return NativeListSlotFrame(x, (height - side) / 2, side, side)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
internal data class NativeListTrailingDescriptor(
|
|
110
|
+
val kind: String,
|
|
111
|
+
val name: String = "",
|
|
112
|
+
val actionKey: String = "",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Identity trailing accessories stack vertically unless they form one of the
|
|
117
|
+
* pairs every platform lays out side by side. The `valuePair` + `DotVerSolid`
|
|
118
|
+
* menu icon pair (the Swap token selector's balance and row menu) matches
|
|
119
|
+
* iOS `configureTrailingStack`; stacking it put the menu under the balance and
|
|
120
|
+
* clipped both inside the fixed row height on Android only.
|
|
121
|
+
*/
|
|
122
|
+
internal fun nativeListIdentityTrailingIsHorizontal(
|
|
123
|
+
accessories: List<NativeListTrailingDescriptor>,
|
|
124
|
+
): Boolean {
|
|
125
|
+
val kinds = accessories.map { it.kind }
|
|
126
|
+
if (kinds.containsAll(listOf("value", "checkbox"))) return true
|
|
127
|
+
if (accessories.size != 2) return false
|
|
128
|
+
val (first, second) = accessories
|
|
129
|
+
return kinds == listOf("valuePair", "menu") ||
|
|
130
|
+
kinds == listOf("valuePair", "chevron") ||
|
|
131
|
+
(kinds == listOf("valuePair", "icon") &&
|
|
132
|
+
second.name == "DotVerSolid" &&
|
|
133
|
+
second.actionKey == "menu") ||
|
|
134
|
+
(kinds == listOf("icon", "chevron") &&
|
|
135
|
+
first.name == "RefreshCcwOutline" &&
|
|
136
|
+
first.actionKey.isEmpty()) ||
|
|
137
|
+
(kinds == listOf("icon", "icon") &&
|
|
138
|
+
first.name == "PencilOutline" &&
|
|
139
|
+
second.name == "DragOutline")
|
|
140
|
+
}
|
|
141
|
+
|
|
51
142
|
internal fun resolveNativeListSelectorBackgroundColor(
|
|
52
143
|
rowType: String,
|
|
53
144
|
styleBackgroundColor: String?,
|
|
@@ -287,7 +378,9 @@ internal fun mergeRow(original: JSONObject, changes: JSONObject): JSONObject {
|
|
|
287
378
|
val keys = changes.keys()
|
|
288
379
|
while (keys.hasNext()) {
|
|
289
380
|
val key = keys.next()
|
|
290
|
-
if (key != "key" && key != "type")
|
|
381
|
+
if (key != "key" && key != "type") {
|
|
382
|
+
if (changes.isNull(key)) result.remove(key) else result.put(key, changes.get(key))
|
|
383
|
+
}
|
|
291
384
|
}
|
|
292
385
|
return result
|
|
293
386
|
}
|
|
@@ -287,6 +287,12 @@ internal class OneKeyIconView(context: android.content.Context) : View(context)
|
|
|
287
287
|
// use EVEN_ODD. Other existing icons retain their prior rendering behavior.
|
|
288
288
|
private val actionIconFillTypes =
|
|
289
289
|
mapOf(
|
|
290
|
+
// Asset-list icons the app passes by name (OK-66072 audit): absent entries drew nothing.
|
|
291
|
+
"DotVerSolid" to listOf(Path.FillType.WINDING),
|
|
292
|
+
"Document2Outline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
|
|
293
|
+
"WalletOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
|
|
294
|
+
"PlusLargeOutline" to listOf(Path.FillType.WINDING),
|
|
295
|
+
"TicketOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
|
|
290
296
|
"GlobusOutline" to listOf(Path.FillType.EVEN_ODD),
|
|
291
297
|
"GasSolid" to listOf(Path.FillType.EVEN_ODD),
|
|
292
298
|
"RefreshCcwOutline" to listOf(Path.FillType.WINDING),
|
|
@@ -333,6 +339,29 @@ internal class OneKeyIconView(context: android.content.Context) : View(context)
|
|
|
333
339
|
// Exact 24x24 paths from app-monorepo packages/components Icon sources.
|
|
334
340
|
private val iconPaths =
|
|
335
341
|
mapOf(
|
|
342
|
+
"DotVerSolid" to
|
|
343
|
+
listOf(
|
|
344
|
+
"M14 18v4h-4v-4zm0-4h-4v-4h4zm0-8h-4V2h4z"
|
|
345
|
+
),
|
|
346
|
+
"Document2Outline" to
|
|
347
|
+
listOf(
|
|
348
|
+
"M12 16H8v-2h4zm4-4H8v-2h8zm0-4H8V6h8z",
|
|
349
|
+
"M20 22H4V2h16zM6 20h12V4H6z"
|
|
350
|
+
),
|
|
351
|
+
"WalletOutline" to
|
|
352
|
+
listOf(
|
|
353
|
+
"M15.5 13.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5",
|
|
354
|
+
"M17 8h4v13H7a4 4 0 0 1-4-4V6.5A3.5 3.5 0 0 1 6.5 3H17zM5 17a2 2 0 0 0 2 2h12v-9H6.5c-.537 0-1.045-.12-1.5-.337zM6.5 5a1.5 1.5 0 1 0 0 3H15V5z"
|
|
355
|
+
),
|
|
356
|
+
"PlusLargeOutline" to
|
|
357
|
+
listOf(
|
|
358
|
+
"M13 11h8v2h-8v8h-2v-8H3v-2h8V3h2z"
|
|
359
|
+
),
|
|
360
|
+
"TicketOutline" to
|
|
361
|
+
listOf(
|
|
362
|
+
"M16 17.01h-2V15h2zm0-4h-2V11h2zm0-4h-2V7h2z",
|
|
363
|
+
"m22 9.927-.629.252c-1.661.665-1.661 2.977 0 3.642l.629.252V20H2v-5.927l.629-.252c1.661-.665 1.661-2.977 0-3.642L2 9.927V4h20zM4 8.636c2.5 1.52 2.499 5.207 0 6.727V18h16v-2.637c-2.499-1.52-2.5-5.208 0-6.727V6H4z"
|
|
364
|
+
),
|
|
336
365
|
"RefreshCcwOutline" to
|
|
337
366
|
listOf(
|
|
338
367
|
"M5 12a7 7 0 0 0 7 7 7.15 7.15 0 0 0 4.955-2h-2.943v-2h6v6h-2v-2.257A9.148 9.148 0 0 1 12 21a9 9 0 0 1-9-9v-1h2v1Zm1-6.754A9.148 9.148 0 0 1 12 3a9 9 0 0 1 9 9v1h-2v-1a7 7 0 0 0-7-7 7.15 7.15 0 0 0-4.955 2H10v2H4V3h2v2.246Z"
|
package/android/src/test/java/com/margelo/nitro/nativelist/NativeListActivityHostClipPolicyTest.kt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListActivityHostClipPolicyTest {
|
|
7
|
+
// One Activity row view serves legacy and rich rows and is reused across
|
|
8
|
+
// binds. A legacy row with an overhanging network badge switches the host's
|
|
9
|
+
// clipping off (OK-66071); the next bind must derive clipping afresh, so a
|
|
10
|
+
// rich row reusing that view clips again instead of drawing its details over
|
|
11
|
+
// a neighbouring row (PR #185 review).
|
|
12
|
+
@Test
|
|
13
|
+
fun rebindSequenceDerivesClippingFromEachBoundItem() {
|
|
14
|
+
data class Bind(val name: String, val rich: Boolean, val leadingOverhangs: Boolean, val clips: Boolean)
|
|
15
|
+
|
|
16
|
+
val binds = listOf(
|
|
17
|
+
Bind("legacy row, plain token", rich = false, leadingOverhangs = false, clips = true),
|
|
18
|
+
Bind("legacy row, network badge", rich = false, leadingOverhangs = true, clips = false),
|
|
19
|
+
Bind("rich row reusing the badge row's view", rich = true, leadingOverhangs = false, clips = true),
|
|
20
|
+
Bind("legacy row, badge again", rich = false, leadingOverhangs = true, clips = false),
|
|
21
|
+
Bind("legacy row, hidden leading", rich = false, leadingOverhangs = false, clips = true),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assertEquals(
|
|
25
|
+
binds.map(Bind::clips),
|
|
26
|
+
binds.map { nativeListActivityHostClips(rich = it.rich, leadingOverhangs = it.leadingOverhangs) },
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
package/android/src/test/java/com/margelo/nitro/nativelist/NativeListIdentityTrailingAxisTest.kt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListIdentityTrailingAxisTest {
|
|
7
|
+
// Identity trailing accessories stack vertically except for the pairs iOS
|
|
8
|
+
// lays out side by side. The Swap token selector's `valuePair` + DotVerSolid
|
|
9
|
+
// menu icon was missing from the Android list, so the balance was pushed
|
|
10
|
+
// above the menu and clipped inside the 60dp row.
|
|
11
|
+
@Test
|
|
12
|
+
fun matchesTheSideBySidePairsOfIos() {
|
|
13
|
+
fun d(kind: String, name: String = "", actionKey: String = "") =
|
|
14
|
+
NativeListTrailingDescriptor(kind, name, actionKey)
|
|
15
|
+
|
|
16
|
+
val cases: List<Pair<List<NativeListTrailingDescriptor>, Boolean>> = listOf(
|
|
17
|
+
listOf(d("valuePair")) to false,
|
|
18
|
+
listOf(d("valuePair"), d("menu", actionKey = "menu")) to true,
|
|
19
|
+
listOf(d("valuePair"), d("icon", "DotVerSolid", "menu")) to true,
|
|
20
|
+
listOf(d("valuePair"), d("icon", "DotVerSolid")) to false,
|
|
21
|
+
listOf(d("valuePair"), d("icon", "InfoCircleOutline", "menu")) to false,
|
|
22
|
+
listOf(d("icon", "DotVerSolid", "menu"), d("valuePair")) to false,
|
|
23
|
+
listOf(d("valuePair"), d("chevron")) to true,
|
|
24
|
+
listOf(d("icon", "RefreshCcwOutline"), d("chevron")) to true,
|
|
25
|
+
listOf(d("icon", "RefreshCcwOutline", "refresh"), d("chevron")) to false,
|
|
26
|
+
listOf(d("icon", "PencilOutline", "edit"), d("icon", "DragOutline")) to true,
|
|
27
|
+
listOf(d("value"), d("checkbox")) to true,
|
|
28
|
+
listOf(d("valuePair"), d("icon", "DotVerSolid", "menu"), d("chevron")) to false,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assertEquals(
|
|
32
|
+
cases.map { it.second },
|
|
33
|
+
cases.map { nativeListIdentityTrailingIsHorizontal(it.first) },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListLeadingSlotFrameTest {
|
|
7
|
+
// 40dp box, 16dp badge hanging 2dp past the frame, 2dp pair ring.
|
|
8
|
+
private fun frame(index: Int, count: Int, network: Int? = null, pair: Int? = null) =
|
|
9
|
+
nativeListLeadingSlotFrame(
|
|
10
|
+
index = index,
|
|
11
|
+
sourceCount = count,
|
|
12
|
+
networkIndex = network,
|
|
13
|
+
pairIndex = pair,
|
|
14
|
+
width = 40,
|
|
15
|
+
height = 40,
|
|
16
|
+
badgeSize = 16,
|
|
17
|
+
badgeOverhang = 2,
|
|
18
|
+
pairRing = 2,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
private val full = NativeListSlotFrame(0, 0, 40, 40)
|
|
22
|
+
private val badge = NativeListSlotFrame(26, 26, 16, 16)
|
|
23
|
+
private val primary = NativeListSlotFrame(0, 0, 24, 24)
|
|
24
|
+
private val secondary = NativeListSlotFrame(14, 14, 24, 24)
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun loneTokenFillsTheBox() {
|
|
28
|
+
assertEquals(full, frame(0, 1))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun tokenWithNetworkBadgeKeepsTheCornerBadge() {
|
|
33
|
+
assertEquals(listOf(full, badge), listOf(frame(0, 2, network = 1), frame(1, 2, network = 1)))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// OK-66071: the history swap rows bind [send, network, receive]. Android laid
|
|
37
|
+
// the receive token over the whole box, hiding the send token and the network
|
|
38
|
+
// badge; iOS drew it in the badge's corner slot over the badge.
|
|
39
|
+
@Test
|
|
40
|
+
fun activityPairStepsTheSecondaryToTheBottomRightUnderTheBadge() {
|
|
41
|
+
assertEquals(
|
|
42
|
+
listOf(primary, badge, secondary),
|
|
43
|
+
(0 until 3).map { frame(it, 3, network = 1, pair = 2) },
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun activityPairWithoutNetworkBadge() {
|
|
49
|
+
assertEquals(listOf(primary, secondary), listOf(frame(0, 2, pair = 1), frame(1, 2, pair = 1)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun pairMemberIsSixTenthsOfTheBox() {
|
|
54
|
+
assertEquals(listOf(24, 19, 29), listOf(40, 32, 48).map(::nativeListLeadingPairMemberSize))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun stackedImagesStepDiagonally() {
|
|
59
|
+
assertEquals(
|
|
60
|
+
listOf(
|
|
61
|
+
NativeListSlotFrame(0, 5, 29, 29),
|
|
62
|
+
NativeListSlotFrame(6, 5, 29, 29),
|
|
63
|
+
NativeListSlotFrame(11, 5, 29, 29),
|
|
64
|
+
),
|
|
65
|
+
(0 until 3).map { frame(it, 3) },
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
package/docs/ROW_TEMPLATES.md
CHANGED
|
@@ -59,6 +59,10 @@ separately from source implementation in STYLE_SPEC §9.
|
|
|
59
59
|
a 4-point gap. It has no click action; an optional Web `hoverActionKey`
|
|
60
60
|
emits an icon-anchored tooltip request on hover or touch. A trailing `button` is the fixed small
|
|
61
61
|
secondary text action and uses the normal accessory event slot.
|
|
62
|
+
- **Title image:** optional `titleImage` renders a circular image after the title
|
|
63
|
+
icons and before suffixes/badges. Its `ImageSource` dimensions must be square,
|
|
64
|
+
1...40 logical units. Optional Web `hoverActionKey` uses the existing anchored
|
|
65
|
+
help event; native keeps the image decorative for touch. See SPEC.md.
|
|
62
66
|
- **Second Web help icon:** `secondaryTitleIcon` uses the same descriptor after
|
|
63
67
|
`titleIcon`; each glyph has its own hover/touch anchor. Native ignores it.
|
|
64
68
|
- **Decorative refresh:** a trailing `RefreshCcwOutline` without `actionKey`
|