@onekeyfe/react-native-native-list 6.7.0-alpha.396 → 6.7.0-alpha.398
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 +10 -13
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +88 -19
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +91 -0
- 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/SPEC.md +59 -2
- package/ios/NativeListDesignAssets.swift +6 -0
- package/ios/NativeListIdentityRenderer.swift +26 -1
- package/ios/NativeListLeadingVisual.swift +70 -8
- 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/lib/module/web/NativeListWebEngine.js +46 -0
- package/package.json +3 -3
- package/src/web/NativeListWebEngine.ts +56 -0
|
@@ -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"),
|
|
@@ -878,19 +878,16 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
878
878
|
sourceScale,
|
|
879
879
|
checkboxState,
|
|
880
880
|
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
|
-
},
|
|
881
|
+
nativeListIdentityTrailingIsHorizontal(
|
|
882
|
+
(0 until accessories.length()).map { index ->
|
|
883
|
+
val accessory = accessories.getJSONObject(index)
|
|
884
|
+
NativeListTrailingDescriptor(
|
|
885
|
+
kind = accessory.optString("kind"),
|
|
886
|
+
name = accessory.optString("name"),
|
|
887
|
+
actionKey = accessory.optString("actionKey"),
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
),
|
|
894
891
|
)
|
|
895
892
|
trailingColumn.endInset?.let { setPadding(paddingLeft, paddingTop, it, paddingBottom) }
|
|
896
893
|
}
|
|
@@ -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,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/SPEC.md
CHANGED
|
@@ -51,6 +51,24 @@ ordinary press for that gesture. Motion cancellation, row rebind/recycle,
|
|
|
51
51
|
disabled state and disposal cancel pending recognition. Trailing accessory
|
|
52
52
|
actions remain independent. Omission preserves existing tap behavior.
|
|
53
53
|
|
|
54
|
+
Identity trailing accessories stack vertically, except for the pairs every
|
|
55
|
+
platform lays out side by side: `value` + `checkbox`, `valuePair` + `menu`,
|
|
56
|
+
`valuePair` + an `icon` named `DotVerSolid` whose `actionKey` is `menu`,
|
|
57
|
+
`valuePair` + `chevron`, a `RefreshCcwOutline` icon without an action key +
|
|
58
|
+
`chevron`, and `PencilOutline` + `DragOutline` icons. iOS decides this in
|
|
59
|
+
`configureTrailingStack`; Android in `nativeListIdentityTrailingIsHorizontal`
|
|
60
|
+
(JVM test `NativeListIdentityTrailingAxisTest`). The `valuePair` + menu-icon
|
|
61
|
+
pair is the Swap token selector's balance column and row menu; stacking it
|
|
62
|
+
clipped both inside the fixed 60dp row on Android.
|
|
63
|
+
|
|
64
|
+
Icon names are a closed set drawn from the app's 24-unit Icon sources on every
|
|
65
|
+
platform (`iconPaths` on Android, `NativeListIcons.xcassets` on iOS,
|
|
66
|
+
`selectorIcons` on Web); an unmapped name renders an empty slot rather than a
|
|
67
|
+
placeholder. The set includes every name the app's NativeList builders pass,
|
|
68
|
+
including `DotVerSolid`, `Document2Outline`, `WalletOutline`, `PlusLargeOutline`
|
|
69
|
+
and `TicketOutline` (added 2026-10-30 after the OK-66072 audit found them missing
|
|
70
|
+
on native).
|
|
71
|
+
|
|
54
72
|
A `message` row may opt into `bodyLines: 0`; its complete body wraps within its
|
|
55
73
|
existing text area and determines natural height unless an explicit row or
|
|
56
74
|
container height overrides it. Existing omitted and one-to-three-line behavior
|
|
@@ -164,7 +182,11 @@ Status: implemented in source for local consumers, unreleased. Focused validatio
|
|
|
164
182
|
Web DOM tests, native builds and device acceptance are tracked separately below.
|
|
165
183
|
The optional `identity.titleIcon` is a static, non-actionable icon after the
|
|
166
184
|
title: a 20-point glyph with a 4-point gap, optional tint and accessibility
|
|
167
|
-
label.
|
|
185
|
+
label. "After the title" means directly after the title text: the title row
|
|
186
|
+
packs at its start on every platform (Android `PackedTitleLineLayout`, Web flex
|
|
187
|
+
`0 1 auto`, iOS a slack-absorbing filler at the end of the title row), so the
|
|
188
|
+
glyph and any badge follow the text and never drift toward the trailing column
|
|
189
|
+
(OK-66041). Its optional Web-only `hoverActionKey` emits an anchored help action
|
|
168
190
|
from the icon alone on mouse/pen hover or touch press; the consumer owns the
|
|
169
191
|
tooltip. Mouse/pen press closes the anchor until pointer leave. Touch press
|
|
170
192
|
toggles the icon anchor. The icon adds no tab stop or separate click action;
|
|
@@ -571,7 +593,8 @@ an action-less accessory as a `span`, so its click reaches the row.
|
|
|
571
593
|
### Leading badge overhang (OK-65694)
|
|
572
594
|
|
|
573
595
|
Status: implemented on Android, the only platform that clipped. Android JVM
|
|
574
|
-
policy
|
|
596
|
+
policy tests (`NativeListLeadingVisualClipPolicyTest`,
|
|
597
|
+
`NativeListActivityHostClipPolicyTest`); Android emulator
|
|
575
598
|
acceptance on the example account selector (API 36, 2026-10-27) is recorded in
|
|
576
599
|
the changelog entry. iOS and Web are unchanged.
|
|
577
600
|
|
|
@@ -590,6 +613,12 @@ overhanging badge (`requestsUnclippedHost`, decided by
|
|
|
590
613
|
already do for their network badges; a visual without such a badge keeps the
|
|
591
614
|
row's default clipping.
|
|
592
615
|
|
|
616
|
+
Host clipping is per-bind state. A row view that serves several row shapes
|
|
617
|
+
(the Android Activity view serves the legacy row and the rich `amounts` row) is
|
|
618
|
+
reused across binds, so each bind MUST derive clipping from the item it binds
|
|
619
|
+
(`nativeListActivityHostClips`): a view that last showed an overhanging badge
|
|
620
|
+
clips again when it is rebound to a row without one.
|
|
621
|
+
|
|
593
622
|
The row's own shape still clips on every platform: iOS masks the cell to its
|
|
594
623
|
corner radius (`masksToBounds`), Web keeps the row `overflow: hidden`, and
|
|
595
624
|
Android keeps `clipToOutline` for a `style.container.cornerRadius` row (its
|
|
@@ -601,6 +630,34 @@ padding. The example `NativeListRowStylePage` row `identity-badge-rounded`
|
|
|
601
630
|
combines both badges with `container.cornerRadius` as the rendering check for
|
|
602
631
|
that combination.
|
|
603
632
|
|
|
633
|
+
### Activity leading pair (OK-66071)
|
|
634
|
+
|
|
635
|
+
Status: implemented on iOS and Android; Web already drew the pair. Android JVM
|
|
636
|
+
geometry test (`NativeListLeadingSlotFrameTest`); Android emulator acceptance on
|
|
637
|
+
the example row-style page is recorded in the changelog entry.
|
|
638
|
+
|
|
639
|
+
An Activity row whose `leading` and `secondaryLeading` are both tokens with an
|
|
640
|
+
image (the app's swap and multi-asset history rows) draws them as a pair inside
|
|
641
|
+
the 40-unit visual box, as the legacy avatar and the Web renderer do: the
|
|
642
|
+
primary token at the top-left at 0.6 of the box (24 units), the secondary at the
|
|
643
|
+
same size at the bottom-right inside a 2-unit ring in the row background, above
|
|
644
|
+
the primary. The ring is shown only while the secondary image is displayed:
|
|
645
|
+
while that image loads, after a failed load and after recycling it stays hidden,
|
|
646
|
+
so it never covers the primary's corner with a blank disc. The token `networkImage` badge, when present, keeps its single-token
|
|
647
|
+
geometry at the box's bottom-right corner above the secondary; a pair shows one
|
|
648
|
+
badge, never one per member. A secondary token without an image leaves the
|
|
649
|
+
primary alone at full size, and a primary without an image lets the secondary
|
|
650
|
+
take its place. Before this rule Android laid the secondary over the whole box
|
|
651
|
+
(hiding the primary and the badge) and iOS drew it in the badge's corner slot
|
|
652
|
+
over the badge. The per-source geometry is one pure function on Android
|
|
653
|
+
(`nativeListLeadingSlotFrame`) and the same arithmetic in
|
|
654
|
+
`NativeListLeadingVisual.layoutSubviews` on iOS; both restore the stacking
|
|
655
|
+
order by role on every bind because slots are reused across roles.
|
|
656
|
+
|
|
657
|
+
The Android Activity renderer also applies the badge-overhang rule above
|
|
658
|
+
(`requestsUnclippedHost` stops the content row and the row padding clipping),
|
|
659
|
+
so the history rows' network badges render whole instead of cut flat.
|
|
660
|
+
|
|
604
661
|
## Failures, fallback and limits
|
|
605
662
|
|
|
606
663
|
Snapshot validation rejects malformed/duplicate-key input without partial
|
|
@@ -139,6 +139,12 @@ func nativeListIcon(named name: String) -> UIImage? {
|
|
|
139
139
|
case "ErrorSolid": assetName = "onekey_error_solid"
|
|
140
140
|
case "QuestionmarkSolid": assetName = "onekey_questionmark_solid"
|
|
141
141
|
case "ImageSquareWavesOutline": assetName = "onekey_image_square_waves"
|
|
142
|
+
// Asset-list icons the app passes by name (OK-66072 audit): unmapped names rendered nothing.
|
|
143
|
+
case "DotVerSolid": assetName = "onekey_dot_ver_solid"
|
|
144
|
+
case "Document2Outline": assetName = "onekey_document2"
|
|
145
|
+
case "WalletOutline": assetName = "onekey_wallet"
|
|
146
|
+
case "PlusLargeOutline": assetName = "onekey_plus_large"
|
|
147
|
+
case "TicketOutline": assetName = "onekey_ticket"
|
|
142
148
|
default: return nil
|
|
143
149
|
}
|
|
144
150
|
return UIImage(named: assetName, in: NativeListResources.bundle, compatibleWith: nil)?
|
|
@@ -107,6 +107,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
107
107
|
private let titleRowStack = UIStackView()
|
|
108
108
|
private let titleLabel = NativeListTextLabel()
|
|
109
109
|
private let titleIconView = UIImageView()
|
|
110
|
+
// Absorbs the title row's slack so the title keeps its intrinsic width and the
|
|
111
|
+
// title icon / badge follow the text (OK-66041); Android's PackedTitleLineLayout
|
|
112
|
+
// and the Web flex row already pack the title row at its start.
|
|
113
|
+
private let titleRowFiller = UIView()
|
|
110
114
|
private let subtitleLabel = NativeListTextLabel()
|
|
111
115
|
private let tertiaryLabel = NativeListTextLabel()
|
|
112
116
|
private let badgeLabel = NativeListInsetLabel()
|
|
@@ -164,6 +168,9 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
164
168
|
titleRowStack.addArrangedSubview(titleLabel)
|
|
165
169
|
titleRowStack.addArrangedSubview(titleIconView)
|
|
166
170
|
titleRowStack.addArrangedSubview(badgeLabel)
|
|
171
|
+
titleRowStack.addArrangedSubview(titleRowFiller)
|
|
172
|
+
titleRowFiller.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
|
|
173
|
+
titleRowFiller.setContentCompressionResistancePriority(UILayoutPriority(1), for: .horizontal)
|
|
167
174
|
titleIconView.translatesAutoresizingMaskIntoConstraints = false
|
|
168
175
|
let titleIconWidth = titleIconView.widthAnchor.constraint(equalToConstant: 20)
|
|
169
176
|
titleIconWidth.priority = UILayoutPriority(999)
|
|
@@ -291,6 +298,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
291
298
|
titleIconView.accessibilityLabel = label.isEmpty ? nil : label
|
|
292
299
|
titleIconView.isAccessibilityElement = !label.isEmpty
|
|
293
300
|
titleRowStack.setCustomSpacing(4, after: titleLabel)
|
|
301
|
+
// A long title truncates ahead of the 20pt glyph (Android caps the title
|
|
302
|
+
// at row width minus accessories, Web shrinks the title first); with the
|
|
303
|
+
// title at `.required` the icon's 999 width constraint collapsed to 0.
|
|
304
|
+
titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
294
305
|
}
|
|
295
306
|
if let ratio = style["leadingContentMaxWidthRatio"] as? NSNumber,
|
|
296
307
|
root.arrangedSubviews.contains(visual)
|
|
@@ -351,8 +362,19 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
351
362
|
: style.dictionary("container")?.string("contentVerticalAlignment") == "bottom"
|
|
352
363
|
? .bottom : .center
|
|
353
364
|
}
|
|
365
|
+
settleTitleRowFillerSpacing()
|
|
354
366
|
applySelectorTypography(item)
|
|
355
367
|
}
|
|
368
|
+
|
|
369
|
+
/// The filler is the title row's last arranged view; the stack's default
|
|
370
|
+
/// spacing before it would shave 8pt off the title's available width (visible
|
|
371
|
+
/// under `leadingContentMaxWidthRatio`), so the gap before the filler is zero.
|
|
372
|
+
private func settleTitleRowFillerSpacing() {
|
|
373
|
+
let visible = titleRowStack.arrangedSubviews.filter { $0 !== titleRowFiller && !$0.isHidden }
|
|
374
|
+
if let last = visible.last {
|
|
375
|
+
titleRowStack.setCustomSpacing(0, after: last)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
356
378
|
override func bindSelectionContent(
|
|
357
379
|
_ item: NativeListItem,
|
|
358
380
|
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
@@ -593,7 +615,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
593
615
|
limit.isActive = true
|
|
594
616
|
selectorConstraints.append(limit)
|
|
595
617
|
}
|
|
596
|
-
titleRowStack.insertArrangedSubview(
|
|
618
|
+
titleRowStack.insertArrangedSubview(
|
|
619
|
+
label,
|
|
620
|
+
at: titleRowStack.arrangedSubviews.firstIndex(of: badgeLabel)
|
|
621
|
+
?? titleRowStack.arrangedSubviews.count)
|
|
597
622
|
selectorViews.append(label)
|
|
598
623
|
}
|
|
599
624
|
}
|
|
@@ -17,6 +17,8 @@ final class NativeListLeadingVisual: UIView {
|
|
|
17
17
|
private let icon = UIImageView()
|
|
18
18
|
private let unread = UIView()
|
|
19
19
|
private let networkBackdrop = UIView()
|
|
20
|
+
// Ring under the secondary token of an Activity pair (SPEC "Activity leading pair").
|
|
21
|
+
private let pairBackdrop = UIView()
|
|
20
22
|
private let corner = UIImageView()
|
|
21
23
|
private let cornerBackdrop = UIView()
|
|
22
24
|
private var slots: [NativeListImageSlot] = []
|
|
@@ -27,10 +29,14 @@ final class NativeListLeadingVisual: UIView {
|
|
|
27
29
|
private var visual: [String: Any] = [:]
|
|
28
30
|
private var imageStyle: [String: Any] = [:]
|
|
29
31
|
private var sources: [([String: Any], String)] = []
|
|
32
|
+
// Roles inside `sources`: the token `networkImage` badge and the Activity
|
|
33
|
+
// `secondaryLeading` token; nil when absent.
|
|
34
|
+
private var networkIndex: Int?
|
|
35
|
+
private var pairIndex: Int?
|
|
30
36
|
|
|
31
37
|
override init(frame: CGRect) {
|
|
32
38
|
super.init(frame: frame)
|
|
33
|
-
[fallback, icon, networkBackdrop, cornerBackdrop, corner, unread].forEach(addSubview)
|
|
39
|
+
[fallback, icon, pairBackdrop, networkBackdrop, cornerBackdrop, corner, unread].forEach(addSubview)
|
|
34
40
|
fallback.textAlignment = .center
|
|
35
41
|
fallback.font = nativeListFont(ofSize: 13, weight: .bold, fonts: fonts)
|
|
36
42
|
icon.contentMode = .scaleAspectFit
|
|
@@ -57,11 +63,14 @@ final class NativeListLeadingVisual: UIView {
|
|
|
57
63
|
kind == "token"
|
|
58
64
|
? "token"
|
|
59
65
|
: kind == "network" ? "network" : ["account", "wallet"].contains(kind) ? "avatar" : "generic"
|
|
66
|
+
networkIndex = nil
|
|
67
|
+
pairIndex = nil
|
|
60
68
|
sources =
|
|
61
69
|
kind == "stackedImages"
|
|
62
70
|
? visual.dictionaries("images").prefix(3).map { ($0, "generic") }
|
|
63
71
|
: kind == "icon" ? [] : visual.dictionary("image").map { [($0, variant)] } ?? []
|
|
64
72
|
if kind == "token", !sources.isEmpty, let network = visual.dictionary("networkImage") {
|
|
73
|
+
networkIndex = sources.count
|
|
65
74
|
sources.append((network, "network"))
|
|
66
75
|
}
|
|
67
76
|
if let secondaryVisual {
|
|
@@ -71,6 +80,9 @@ final class NativeListLeadingVisual: UIView {
|
|
|
71
80
|
? secondaryVisual.dictionaries("images").first
|
|
72
81
|
: secondaryKind == "icon" ? nil : secondaryVisual.dictionary("image")
|
|
73
82
|
if let source {
|
|
83
|
+
// A second token forms a pair only next to a primary image; without one
|
|
84
|
+
// it simply takes the primary's place (OK-66071).
|
|
85
|
+
if kind == "token", !sources.isEmpty { pairIndex = sources.count }
|
|
74
86
|
sources.append(
|
|
75
87
|
(
|
|
76
88
|
source,
|
|
@@ -132,6 +144,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
132
144
|
fit: index == 0 ? style["contentFit"] as? String : nil, placeholder: placeholder
|
|
133
145
|
) { [weak self, weak slot] loaded in
|
|
134
146
|
slot?.view.isHidden = !loaded && (index > 0 || sourceFallback)
|
|
147
|
+
if let self, index == self.pairIndex { self.syncPairBackdrop() }
|
|
135
148
|
guard let self, index == 0, sourceFallback else { return }
|
|
136
149
|
if let fallbackKey {
|
|
137
150
|
if loaded {
|
|
@@ -146,8 +159,19 @@ final class NativeListLeadingVisual: UIView {
|
|
|
146
159
|
self.icon.isHidden = loaded || fallbackIcon == nil
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
|
-
networkBackdrop.isHidden =
|
|
162
|
+
networkBackdrop.isHidden = networkIndex == nil
|
|
150
163
|
networkBackdrop.backgroundColor = nativeListColor(theme, "rowBackground", "#FFFFFF")
|
|
164
|
+
pairBackdrop.backgroundColor = nativeListColor(theme, "rowBackground", "#FFFFFF")
|
|
165
|
+
syncPairBackdrop()
|
|
166
|
+
// Stacking order by role: primary, pair ring, secondary token, badge
|
|
167
|
+
// backdrop, badge, corner icon. Slots are reused across binds with
|
|
168
|
+
// different roles, so the order is restored on every bind.
|
|
169
|
+
bringSubviewToFront(pairBackdrop)
|
|
170
|
+
if let pairIndex { bringSubviewToFront(slots[pairIndex].view) }
|
|
171
|
+
bringSubviewToFront(networkBackdrop)
|
|
172
|
+
if let networkIndex { bringSubviewToFront(slots[networkIndex].view) }
|
|
173
|
+
bringSubviewToFront(cornerBackdrop)
|
|
174
|
+
bringSubviewToFront(corner)
|
|
151
175
|
let badge = visual.dictionary("cornerIcon")
|
|
152
176
|
corner.isHidden = badge == nil
|
|
153
177
|
cornerBackdrop.isHidden = badge == nil
|
|
@@ -229,7 +253,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
229
253
|
frame.subviews.forEach { $0.removeFromSuperview() }
|
|
230
254
|
frame.addSubview(child)
|
|
231
255
|
if data.dictionary("image") != nil && fallbackData != nil { frame.addSubview(fallbackGlyph) }
|
|
232
|
-
if frame.superview == nil { addSubview(frame) }
|
|
256
|
+
if frame.superview == nil { addSubview(frame) } else { bringSubviewToFront(frame) }
|
|
233
257
|
overlays.append((frame, data))
|
|
234
258
|
}
|
|
235
259
|
previousOverlays.dropFirst(descriptors.count).forEach { $0.0.removeFromSuperview() }
|
|
@@ -284,13 +308,22 @@ final class NativeListLeadingVisual: UIView {
|
|
|
284
308
|
icon.frame = CGRect(
|
|
285
309
|
x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize,
|
|
286
310
|
height: iconSize)
|
|
287
|
-
|
|
311
|
+
// Same geometry as Android's `nativeListLeadingSlotFrame` (SPEC "Activity leading pair").
|
|
312
|
+
let pairMember = (bounds.height * 0.6).rounded()
|
|
313
|
+
let pairRing: CGFloat = 2
|
|
288
314
|
for (index, slot) in slots.prefix(sources.count).enumerated() {
|
|
289
315
|
let frame: CGRect
|
|
290
|
-
if
|
|
291
|
-
frame = bounds
|
|
292
|
-
} else if tokenPair {
|
|
316
|
+
if index == networkIndex {
|
|
293
317
|
frame = CGRect(x: bounds.width - 14, y: bounds.height - 14, width: 16, height: 16)
|
|
318
|
+
} else if pairIndex != nil {
|
|
319
|
+
frame =
|
|
320
|
+
index == pairIndex
|
|
321
|
+
? CGRect(
|
|
322
|
+
x: bounds.width - pairMember - pairRing, y: bounds.height - pairMember - pairRing,
|
|
323
|
+
width: pairMember, height: pairMember)
|
|
324
|
+
: CGRect(x: 0, y: 0, width: pairMember, height: pairMember)
|
|
325
|
+
} else if sources.count == 1 || networkIndex != nil {
|
|
326
|
+
frame = bounds
|
|
294
327
|
} else {
|
|
295
328
|
let side = bounds.height * 0.72
|
|
296
329
|
frame = CGRect(
|
|
@@ -302,10 +335,23 @@ final class NativeListLeadingVisual: UIView {
|
|
|
302
335
|
slot.view.clipsToBounds =
|
|
303
336
|
!(singleOuterMask && sources.count == 1 && overlays.isEmpty
|
|
304
337
|
&& visual.dictionary("cornerIcon") == nil)
|
|
338
|
+
let fillsFrame = index == 0 && pairIndex == nil && (sources.count == 1 || networkIndex != nil)
|
|
339
|
+
// Pair members keep the token shape at their own size (rounded NFTs use
|
|
340
|
+
// the box's 10pt rule scaled to the member).
|
|
341
|
+
let pairRadius: CGFloat = shape == "rounded" ? min(10, frame.height / 4) : frame.height / 2
|
|
305
342
|
slot.view.layer.cornerRadius =
|
|
306
343
|
index == 0 && singleOuterMask && sources.count == 1 && overlays.isEmpty
|
|
307
344
|
&& visual.dictionary("cornerIcon") == nil
|
|
308
|
-
? 0
|
|
345
|
+
? 0
|
|
346
|
+
: fillsFrame
|
|
347
|
+
? radius : pairIndex != nil && index != networkIndex ? pairRadius : frame.height / 2
|
|
348
|
+
}
|
|
349
|
+
if pairIndex != nil {
|
|
350
|
+
let box = pairMember + 2 * pairRing
|
|
351
|
+
pairBackdrop.frame = logical(
|
|
352
|
+
CGRect(x: bounds.width - box, y: bounds.height - box, width: box, height: box))
|
|
353
|
+
pairBackdrop.layer.cornerRadius =
|
|
354
|
+
shape == "rounded" ? min(10, pairMember / 4) + pairRing : box / 2
|
|
309
355
|
}
|
|
310
356
|
for (index, slot) in slots.enumerated() {
|
|
311
357
|
if index == 0 && bitmapBorderWidth > 0 {
|
|
@@ -381,9 +427,22 @@ final class NativeListLeadingVisual: UIView {
|
|
|
381
427
|
UIBezierPath(ovalIn: bounds.insetBy(dx: dashedBorderWidth / 2, dy: dashedBorderWidth / 2))
|
|
382
428
|
.cgPath
|
|
383
429
|
}
|
|
430
|
+
/// The pair ring sits above the primary token, so it is shown only while the
|
|
431
|
+
/// secondary token image is actually displayed: hidden while that image loads,
|
|
432
|
+
/// after a failed load and once the slot is recycled, or it would cover the
|
|
433
|
+
/// primary's lower-right corner with a blank row-background disc.
|
|
434
|
+
private func syncPairBackdrop() {
|
|
435
|
+
guard let pairIndex, slots.indices.contains(pairIndex) else {
|
|
436
|
+
pairBackdrop.isHidden = true
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
pairBackdrop.isHidden = slots[pairIndex].view.isHidden
|
|
440
|
+
}
|
|
441
|
+
|
|
384
442
|
func recycle() {
|
|
385
443
|
slots.forEach { $0.recycle() }
|
|
386
444
|
overlaySlots.forEach { $0.recycle() }
|
|
445
|
+
pairBackdrop.isHidden = true
|
|
387
446
|
}
|
|
388
447
|
/// Empty slot (legacy leading container without a visual): no image, fallback, fill,
|
|
389
448
|
/// border or overlays from a previous binding.
|
|
@@ -399,6 +458,9 @@ final class NativeListLeadingVisual: UIView {
|
|
|
399
458
|
icon.image = nil
|
|
400
459
|
icon.isHidden = true
|
|
401
460
|
networkBackdrop.isHidden = true
|
|
461
|
+
pairBackdrop.isHidden = true
|
|
462
|
+
networkIndex = nil
|
|
463
|
+
pairIndex = nil
|
|
402
464
|
corner.isHidden = true
|
|
403
465
|
cornerBackdrop.isHidden = true
|
|
404
466
|
unread.isHidden = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M12 16H8v-2h4zm4-4H8v-2h8zm0-4H8V6h8z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M20 22H4V2h16zM6 20h12V4H6z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M14 18v4h-4v-4zm0-4h-4v-4h4zm0-8h-4V2h4z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M13 11h8v2h-8v8h-2v-8H3v-2h8V3h2z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M16 17.01h-2V15h2zm0-4h-2V11h2zm0-4h-2V7h2z"/><path fill-rule="evenodd" clip-rule="evenodd" d="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"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M15.5 13.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="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"/></svg>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import NativeListModule
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class NativeListIdentityTitleIconLayoutTests: XCTestCase {
|
|
7
|
+
private func child<T>(_ cell: NativeListIdentityCell, _ label: String, as type: T.Type) throws -> T {
|
|
8
|
+
try XCTUnwrap(Mirror(reflecting: cell).children.first { $0.label == label }?.value as? T)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// OK-66041: the Home native-token row carries a GasSolid `titleIcon`. The
|
|
12
|
+
// glyph must sit right after the title text (4pt gap), as Android's
|
|
13
|
+
// PackedTitleLineLayout and the Web flex row already do. iOS let the title
|
|
14
|
+
// label stretch across the whole title row, so the icon landed next to the
|
|
15
|
+
// trailing value column instead of beside "ETH".
|
|
16
|
+
func testTitleIconFollowsTheTitleTextInsteadOfTheRowEnd() throws {
|
|
17
|
+
let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 390, height: 60))
|
|
18
|
+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 200))
|
|
19
|
+
window.addSubview(cell)
|
|
20
|
+
let titleIcon = try child(cell, "titleIconView", as: UIImageView.self)
|
|
21
|
+
let titleLabel = try child(cell, "titleLabel", as: UILabel.self)
|
|
22
|
+
let titleStack = try child(cell, "titleRowStack", as: UIStackView.self)
|
|
23
|
+
let item = try NativeListItem(data: [
|
|
24
|
+
"key": "eth", "type": "identity", "title": "ETH", "height": 60,
|
|
25
|
+
"leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]],
|
|
26
|
+
"titleIcon": ["name": "GasSolid"],
|
|
27
|
+
"subtitleSegments": [
|
|
28
|
+
["text": "$2,330.23", "tone": "secondary"],
|
|
29
|
+
["text": "+0.23%", "tone": "positive", "separatorBefore": true],
|
|
30
|
+
],
|
|
31
|
+
"trailing": [["kind": "valuePair", "primary": "0.00001417", "secondary": "$0.03"]],
|
|
32
|
+
])
|
|
33
|
+
cell.bind(item: item, theme: nil, layout: "linear", selected: false,
|
|
34
|
+
checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
|
|
35
|
+
cell.layoutIfNeeded()
|
|
36
|
+
|
|
37
|
+
XCTAssertFalse(titleIcon.isHidden)
|
|
38
|
+
// The title keeps its intrinsic width; the slack goes to the end of the row.
|
|
39
|
+
XCTAssertEqual(
|
|
40
|
+
titleLabel.bounds.width, titleLabel.intrinsicContentSize.width, accuracy: 1,
|
|
41
|
+
"title label must not stretch across the title row")
|
|
42
|
+
let labelFrame = titleLabel.convert(titleLabel.bounds, to: cell)
|
|
43
|
+
let iconFrame = titleIcon.convert(titleIcon.bounds, to: cell)
|
|
44
|
+
XCTAssertEqual(iconFrame.minX, labelFrame.maxX + 4, accuracy: 1,
|
|
45
|
+
"gas icon must follow the title text with the 4pt gap")
|
|
46
|
+
let iconInRow = titleIcon.convert(titleIcon.bounds, to: titleStack)
|
|
47
|
+
XCTAssertLessThan(iconInRow.maxX, titleStack.bounds.width / 2,
|
|
48
|
+
"gas icon must stay in the leading half of the title row, not at the value column")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A long title still truncates in front of the icon instead of pushing it
|
|
52
|
+
// out of the row, and the badge keeps following the icon.
|
|
53
|
+
func testLongTitleTruncatesAndKeepsIconAndBadgeAdjacent() throws {
|
|
54
|
+
let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
|
|
55
|
+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
56
|
+
window.addSubview(cell)
|
|
57
|
+
let titleIcon = try child(cell, "titleIconView", as: UIImageView.self)
|
|
58
|
+
let titleLabel = try child(cell, "titleLabel", as: UILabel.self)
|
|
59
|
+
let badge = try child(cell, "badgeLabel", as: UILabel.self)
|
|
60
|
+
let item = try NativeListItem(data: [
|
|
61
|
+
"key": "long", "type": "identity",
|
|
62
|
+
"title": "A very long token name that cannot possibly fit on one line", "height": 60,
|
|
63
|
+
"leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]],
|
|
64
|
+
"titleIcon": ["name": "GasSolid"],
|
|
65
|
+
"badges": [["key": "native", "text": "Native"]],
|
|
66
|
+
"trailing": [["kind": "valuePair", "primary": "1.00", "secondary": "$1.00"]],
|
|
67
|
+
])
|
|
68
|
+
cell.bind(item: item, theme: nil, layout: "linear", selected: false,
|
|
69
|
+
checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
|
|
70
|
+
cell.layoutIfNeeded()
|
|
71
|
+
|
|
72
|
+
let labelFrame = titleLabel.convert(titleLabel.bounds, to: cell)
|
|
73
|
+
let iconFrame = titleIcon.convert(titleIcon.bounds, to: cell)
|
|
74
|
+
let badgeFrame = badge.convert(badge.bounds, to: cell)
|
|
75
|
+
XCTAssertEqual(titleIcon.bounds.width, 20, accuracy: 0.5)
|
|
76
|
+
XCTAssertEqual(iconFrame.minX, labelFrame.maxX + 4, accuracy: 1)
|
|
77
|
+
XCTAssertFalse(badge.isHidden)
|
|
78
|
+
XCTAssertGreaterThan(badgeFrame.minX, iconFrame.maxX)
|
|
79
|
+
XCTAssertLessThanOrEqual(badgeFrame.maxX, cell.bounds.width)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import NativeListModule
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class NativeListLeadingVisualPairBackdropTests: XCTestCase {
|
|
7
|
+
private func child<T>(_ visual: NativeListLeadingVisual, _ label: String, as type: T.Type) throws -> T {
|
|
8
|
+
try XCTUnwrap(Mirror(reflecting: visual).children.first { $0.label == label }?.value as? T)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private func token(_ uri: String) -> [String: Any] {
|
|
12
|
+
[
|
|
13
|
+
"kind": "token", "shape": "circle",
|
|
14
|
+
"image": ["uri": uri, "width": 40, "height": 40],
|
|
15
|
+
"fallbackIcon": ["name": "CryptoCoinOutline"],
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// PR #185 review: the pair ring sits above the primary token. While the
|
|
20
|
+
// secondary image has not loaded (or failed), its slot is hidden, so the ring
|
|
21
|
+
// must be hidden too; otherwise a blank row-background disc covers the
|
|
22
|
+
// primary's lower-right corner.
|
|
23
|
+
func testPairRingStaysHiddenWhileTheSecondaryImageIsNotDisplayed() throws {
|
|
24
|
+
let visual = NativeListLeadingVisual(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
|
|
25
|
+
let ring = try child(visual, "pairBackdrop", as: UIView.self)
|
|
26
|
+
visual.bind(
|
|
27
|
+
token("https://example.invalid/primary.png"), style: [:], key: "pair", theme: nil,
|
|
28
|
+
isUnread: false, secondaryVisual: token("https://example.invalid/secondary.png"),
|
|
29
|
+
fonts: NativeListFontFamilies())
|
|
30
|
+
visual.layoutIfNeeded()
|
|
31
|
+
// `slots` is a value-type array: read it after bind has created the slots.
|
|
32
|
+
let slots = try child(visual, "slots", as: [NativeListImageSlot].self)
|
|
33
|
+
XCTAssertEqual(slots.count, 2)
|
|
34
|
+
XCTAssertTrue(slots[1].view.isHidden, "secondary slot is hidden until its image loads")
|
|
35
|
+
XCTAssertTrue(ring.isHidden, "ring must follow the hidden secondary slot")
|
|
36
|
+
|
|
37
|
+
visual.recycle()
|
|
38
|
+
XCTAssertTrue(ring.isHidden)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testNoPairMeansNoRing() throws {
|
|
42
|
+
let visual = NativeListLeadingVisual(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
|
|
43
|
+
let ring = try child(visual, "pairBackdrop", as: UIView.self)
|
|
44
|
+
visual.bind(
|
|
45
|
+
token("https://example.invalid/primary.png"), style: [:], key: "single", theme: nil,
|
|
46
|
+
isUnread: false, fonts: NativeListFontFamilies())
|
|
47
|
+
visual.layoutIfNeeded()
|
|
48
|
+
XCTAssertTrue(ring.isHidden)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -735,6 +735,52 @@ const selectorIcons = {
|
|
|
735
735
|
opacity: 1
|
|
736
736
|
}]
|
|
737
737
|
},
|
|
738
|
+
DotVerSolid: {
|
|
739
|
+
viewBox: '0 0 24 24',
|
|
740
|
+
paths: [{
|
|
741
|
+
d: 'M14 18v4h-4v-4zm0-4h-4v-4h4zm0-8h-4V2h4z',
|
|
742
|
+
fill: 'currentColor',
|
|
743
|
+
fillRule: 'nonzero',
|
|
744
|
+
opacity: 1
|
|
745
|
+
}]
|
|
746
|
+
},
|
|
747
|
+
Document2Outline: {
|
|
748
|
+
viewBox: '0 0 24 24',
|
|
749
|
+
paths: [{
|
|
750
|
+
d: 'M12 16H8v-2h4zm4-4H8v-2h8zm0-4H8V6h8z',
|
|
751
|
+
fill: 'currentColor',
|
|
752
|
+
fillRule: 'nonzero',
|
|
753
|
+
opacity: 1
|
|
754
|
+
}, {
|
|
755
|
+
d: 'M20 22H4V2h16zM6 20h12V4H6z',
|
|
756
|
+
fill: 'currentColor',
|
|
757
|
+
fillRule: 'evenodd',
|
|
758
|
+
opacity: 1
|
|
759
|
+
}]
|
|
760
|
+
},
|
|
761
|
+
WalletOutline: {
|
|
762
|
+
viewBox: '0 0 24 24',
|
|
763
|
+
paths: [{
|
|
764
|
+
d: 'M15.5 13.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5',
|
|
765
|
+
fill: 'currentColor',
|
|
766
|
+
fillRule: 'nonzero',
|
|
767
|
+
opacity: 1
|
|
768
|
+
}, {
|
|
769
|
+
d: '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',
|
|
770
|
+
fill: 'currentColor',
|
|
771
|
+
fillRule: 'evenodd',
|
|
772
|
+
opacity: 1
|
|
773
|
+
}]
|
|
774
|
+
},
|
|
775
|
+
PlusLargeOutline: {
|
|
776
|
+
viewBox: '0 0 24 24',
|
|
777
|
+
paths: [{
|
|
778
|
+
d: 'M13 11h8v2h-8v8h-2v-8H3v-2h8V3h2z',
|
|
779
|
+
fill: 'currentColor',
|
|
780
|
+
fillRule: 'nonzero',
|
|
781
|
+
opacity: 1
|
|
782
|
+
}]
|
|
783
|
+
},
|
|
738
784
|
TicketOutline: {
|
|
739
785
|
viewBox: '0 0 24 24',
|
|
740
786
|
paths: [{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-native-list",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.398",
|
|
4
4
|
"description": "Template-driven native RecyclerView and UICollectionView for React Native",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"typescript": "^5.9.2"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@onekeyfe/react-native-image": "6.7.0-alpha.
|
|
90
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
89
|
+
"@onekeyfe/react-native-image": "6.7.0-alpha.398",
|
|
90
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.398",
|
|
91
91
|
"react": "*",
|
|
92
92
|
"react-native": "*",
|
|
93
93
|
"react-native-nitro-modules": "0.37.0"
|
|
@@ -1206,6 +1206,62 @@ const selectorIcons: Readonly<
|
|
|
1206
1206
|
},
|
|
1207
1207
|
],
|
|
1208
1208
|
},
|
|
1209
|
+
DotVerSolid: {
|
|
1210
|
+
viewBox: '0 0 24 24',
|
|
1211
|
+
paths: [
|
|
1212
|
+
{
|
|
1213
|
+
d: 'M14 18v4h-4v-4zm0-4h-4v-4h4zm0-8h-4V2h4z',
|
|
1214
|
+
fill: 'currentColor',
|
|
1215
|
+
fillRule: 'nonzero',
|
|
1216
|
+
opacity: 1,
|
|
1217
|
+
},
|
|
1218
|
+
],
|
|
1219
|
+
},
|
|
1220
|
+
Document2Outline: {
|
|
1221
|
+
viewBox: '0 0 24 24',
|
|
1222
|
+
paths: [
|
|
1223
|
+
{
|
|
1224
|
+
d: 'M12 16H8v-2h4zm4-4H8v-2h8zm0-4H8V6h8z',
|
|
1225
|
+
fill: 'currentColor',
|
|
1226
|
+
fillRule: 'nonzero',
|
|
1227
|
+
opacity: 1,
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
d: 'M20 22H4V2h16zM6 20h12V4H6z',
|
|
1231
|
+
fill: 'currentColor',
|
|
1232
|
+
fillRule: 'evenodd',
|
|
1233
|
+
opacity: 1,
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
},
|
|
1237
|
+
WalletOutline: {
|
|
1238
|
+
viewBox: '0 0 24 24',
|
|
1239
|
+
paths: [
|
|
1240
|
+
{
|
|
1241
|
+
d: 'M15.5 13.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5',
|
|
1242
|
+
fill: 'currentColor',
|
|
1243
|
+
fillRule: 'nonzero',
|
|
1244
|
+
opacity: 1,
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
d: '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',
|
|
1248
|
+
fill: 'currentColor',
|
|
1249
|
+
fillRule: 'evenodd',
|
|
1250
|
+
opacity: 1,
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
},
|
|
1254
|
+
PlusLargeOutline: {
|
|
1255
|
+
viewBox: '0 0 24 24',
|
|
1256
|
+
paths: [
|
|
1257
|
+
{
|
|
1258
|
+
d: 'M13 11h8v2h-8v8h-2v-8H3v-2h8V3h2z',
|
|
1259
|
+
fill: 'currentColor',
|
|
1260
|
+
fillRule: 'nonzero',
|
|
1261
|
+
opacity: 1,
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
},
|
|
1209
1265
|
TicketOutline: {
|
|
1210
1266
|
viewBox: '0 0 24 24',
|
|
1211
1267
|
paths: [
|