@onekeyfe/react-native-native-list 3.0.105 → 3.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
  2. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
  4. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
  5. package/ios/HybridNativeList.swift +8 -2
  6. package/ios/NativeListCell.swift +630 -31
  7. package/ios/NativeListDesignAssets.swift +24 -1
  8. package/ios/RNCNativeListView.swift +216 -42
  9. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
  10. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
  11. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
  12. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
  13. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
  14. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
  25. package/lib/module/NativeList.js +102 -8
  26. package/lib/module/NativeList.web.js +20 -2
  27. package/lib/module/avatarPrefetch.js +174 -0
  28. package/lib/module/validation.js +45 -2
  29. package/lib/module/web/NativeListAvatarWorker.js +542 -0
  30. package/lib/module/web/NativeListWebAvatarCache.js +267 -0
  31. package/lib/module/web/NativeListWebEngine.js +958 -47
  32. package/lib/typescript/src/NativeList.web.d.ts +2 -2
  33. package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
  34. package/lib/typescript/src/models.d.ts +72 -4
  35. package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
  36. package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
  37. package/package.json +2 -2
  38. package/src/NativeList.tsx +149 -13
  39. package/src/NativeList.web.tsx +30 -3
  40. package/src/avatarPrefetch.ts +236 -0
  41. package/src/models.ts +98 -7
  42. package/src/validation.ts +131 -2
  43. package/src/web/NativeListAvatarWorker.js +567 -0
  44. package/src/web/NativeListWebAvatarCache.ts +314 -0
  45. package/src/web/NativeListWebEngine.ts +1409 -60
@@ -29,6 +29,12 @@ import android.widget.LinearLayout
29
29
  import android.widget.ProgressBar
30
30
  import android.widget.TextView
31
31
  import com.facebook.react.uimanager.ThemedReactContext
32
+ // OneKey patch: selector checkboxes share the source React Native border/background renderer.
33
+ import com.facebook.react.uimanager.BackgroundStyleApplicator
34
+ import com.facebook.react.uimanager.LengthPercentage
35
+ import com.facebook.react.uimanager.LengthPercentageType
36
+ import com.facebook.react.uimanager.style.BorderRadiusProp
37
+ import com.facebook.react.uimanager.style.LogicalEdge
32
38
  import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
33
39
  import androidx.core.graphics.PathParser
34
40
  import androidx.core.widget.TextViewCompat
@@ -42,6 +48,7 @@ internal data class NativeListActionOrigin(
42
48
  val bindingEpoch: Long,
43
49
  val source: String,
44
50
  val slot: Int? = null,
51
+ val anchorInsetPixels: Int = 0,
45
52
  )
46
53
 
47
54
  /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
@@ -96,7 +103,20 @@ internal object NativeListFonts {
96
103
 
97
104
  internal data class NativeSelectionTarget(val scope: String, val key: String?)
98
105
 
106
+ // OneKey patch: match React Native's CustomLineHeightSpan for first/last line bounds.
107
+ private class SelectorLineHeightSpan(private val lineHeight: Int) : android.text.style.LineHeightSpan {
108
+ override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, fm: Paint.FontMetricsInt) {
109
+ val leading = lineHeight - (fm.descent - fm.ascent)
110
+ fm.ascent -= kotlin.math.ceil(leading / 2.0).toInt()
111
+ fm.descent += kotlin.math.floor(leading / 2.0).toInt()
112
+ if (start == 0) fm.top = fm.ascent
113
+ if (end == text.length) fm.bottom = fm.descent
114
+ }
115
+ }
116
+
99
117
  private class DottedUnderlineTextView(context: android.content.Context) : TextView(context) {
118
+ var useSourceScale = false
119
+ private fun scaledDp(value: Float) = if (useSourceScale) value * resources.displayMetrics.density else NativeListScale.dp(resources, value)
100
120
  var showsDottedUnderline = false
101
121
  var dottedUnderlineColor = Color.TRANSPARENT
102
122
  private val dottedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
@@ -105,11 +125,11 @@ private class DottedUnderlineTextView(context: android.content.Context) : TextVi
105
125
  super.onDraw(canvas)
106
126
  if (!showsDottedUnderline || text.isEmpty()) return
107
127
  dottedPaint.color = dottedUnderlineColor
108
- val radius = NativeListScale.dp(resources, 0.75f)
109
- val spacing = NativeListScale.dp(resources, 4f)
128
+ val radius = scaledDp(0.75f)
129
+ val spacing = scaledDp(4f)
110
130
  val lineWidth = paint.measureText(text.toString()).coerceAtMost(width.toFloat())
111
131
  val y = height - radius
112
- var x = NativeListScale.dp(resources, 1f)
132
+ var x = scaledDp(1f)
113
133
  while (x <= lineWidth - radius) {
114
134
  canvas.drawCircle(x, y, radius, dottedPaint)
115
135
  x += spacing
@@ -160,6 +180,29 @@ private class PackedTitleLineLayout(context: android.content.Context) : LinearLa
160
180
  }
161
181
  }
162
182
 
183
+ // OneKey patch: fit subtitle segments at intrinsic width, shrinking text only when necessary.
184
+ private class SelectorSubtitleLayout(context: android.content.Context) : LinearLayout(context) {
185
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
186
+ val available = MeasureSpec.getSize(widthMeasureSpec)
187
+ val labels = mutableListOf<Pair<TextView, Int>>()
188
+ var fixedWidth = 0
189
+ for (index in 0 until childCount) {
190
+ val child = getChildAt(index)
191
+ val params = child.layoutParams as LayoutParams
192
+ if (child is TextView) {
193
+ child.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec)
194
+ labels.add(child to child.measuredWidth)
195
+ } else fixedWidth += params.width + params.leftMargin + params.rightMargin
196
+ }
197
+ val desired = labels.sumOf { it.second }
198
+ val textWidth = (available - fixedWidth).coerceAtLeast(0)
199
+ labels.forEach { (label, width) ->
200
+ (label.layoutParams as LayoutParams).width = if (desired <= textWidth) width else (width.toLong() * textWidth / desired.coerceAtLeast(1)).toInt()
201
+ }
202
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
203
+ }
204
+ }
205
+
163
206
  private class NativeListTableColumnView(context: android.content.Context) : LinearLayout(context) {
164
207
  private val primaryLine = LinearLayout(context)
165
208
  private val primary = TextView(context)
@@ -282,6 +325,21 @@ internal class NativeListRowView(
282
325
  private val reactContext: ThemedReactContext,
283
326
  ) : LinearLayout(reactContext) {
284
327
  private val leadingFrame = FrameLayout(context)
328
+ // OneKey patch: reset selector fragments and corner decorations on every bind.
329
+ private val selectorViews = mutableListOf<View>()
330
+ private var selectorUsesSourceScale = false
331
+ private val selectorAccessibilityDelegate = object : View.AccessibilityDelegate() {
332
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: android.view.accessibility.AccessibilityNodeInfo) {
333
+ super.onInitializeAccessibilityNodeInfo(host, info)
334
+ info.viewIdResourceName = host.getTag(com.facebook.react.R.id.react_test_id) as? String
335
+ }
336
+ }
337
+ private val selectorOriginalFontFeatures = mutableMapOf<TextView, String?>()
338
+ private val selectorOriginalPaintFlags = mutableMapOf<TextView, Int>()
339
+ private val selectorLineHeights = mutableMapOf<TextView, Int>()
340
+ private val selectorFontSizes = mutableMapOf<TextView, Float>()
341
+ private val selectorImages = mutableListOf<OneKeyImageReusableView>()
342
+ private var selectorHeight: Int? = null
285
343
  private val leadingImages = List(3) { OneKeyImageReusableView(reactContext) }
286
344
  private val leadingOverlayBackground = View(context)
287
345
  private val leadingCornerIconFrame = FrameLayout(context)
@@ -334,16 +392,22 @@ internal class NativeListRowView(
334
392
  private var walletGroupExpandAnimator: ValueAnimator? = null
335
393
  private var isMediaTile = false
336
394
  private var boundKey: String? = null
395
+ // OneKey patch: delayed retries cannot survive cell rebinding or recycling.
396
+ private val selectorImageRetries = mutableMapOf<OneKeyImageReusableView, Runnable>()
337
397
  var bindingEpoch: Long = 0
338
398
  private set
339
399
  private var boundCheckboxData: JSONObject? = null
340
400
  private var currentLayout = "linear"
341
401
  private var restingRowBackground: Drawable? = null
342
402
  private var pressedRowBackground: Drawable? = null
403
+ // OneKey patch: preserve a held row independently from RecyclerView snapshot rebinding.
404
+ private var touchPressed = false
343
405
  private var reorderActive = false
344
406
  private var checkboxCheckedColor = Color.rgb(32, 32, 32)
345
407
  private var checkboxUncheckedColor = Color.rgb(252, 252, 252)
346
408
  private var checkboxBorderColor = Color.rgb(206, 206, 206)
409
+ private var checkboxIconColor = Color.rgb(252, 252, 252)
410
+ private var checkboxUsesSelectorStyle = false
347
411
  private var iconSubduedColor = Color.rgb(141, 141, 141)
348
412
  private var visualBackdropColor = Color.WHITE
349
413
  private val circleOutlineProvider = object : ViewOutlineProvider() {
@@ -445,6 +509,7 @@ internal class NativeListRowView(
445
509
  setOnTouchListener { _, event ->
446
510
  when (event.actionMasked) {
447
511
  MotionEvent.ACTION_DOWN -> if (isEnabled) {
512
+ touchPressed = true
448
513
  if ((tag as? NativeListItem)?.type == "mediaTile") {
449
514
  leadingFrame.alpha = 0.8f
450
515
  } else {
@@ -454,15 +519,20 @@ internal class NativeListRowView(
454
519
  MotionEvent.ACTION_MOVE -> if (
455
520
  event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
456
521
  ) {
522
+ touchPressed = false
523
+ restoreRestingBackground()
524
+ }
525
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
526
+ touchPressed = false
457
527
  restoreRestingBackground()
458
528
  }
459
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> restoreRestingBackground()
460
529
  }
461
530
  false
462
531
  }
463
532
  setOnClickListener { view ->
464
533
  (view.tag as? NativeListItem)?.let { item ->
465
- onRowPress?.invoke(item, actionOrigin(view, "row"))
534
+ // OneKey patch: allow create-address accessories when whole-row press is gated.
535
+ if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
466
536
  }
467
537
  }
468
538
  setWillNotDraw(false)
@@ -509,13 +579,64 @@ internal class NativeListRowView(
509
579
  }
510
580
  }
511
581
 
582
+ // OneKey patch: RecyclerView may replace item delegates during a selection update.
583
+ override fun onInitializeAccessibilityNodeInfo(info: android.view.accessibility.AccessibilityNodeInfo) {
584
+ super.onInitializeAccessibilityNodeInfo(info)
585
+ info.viewIdResourceName = getTag(com.facebook.react.R.id.react_test_id) as? String
586
+ }
587
+
512
588
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
513
589
  if (isMediaTile) {
514
590
  val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
515
591
  .coerceAtLeast(0)
516
592
  leadingFrame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, availableWidth)
517
593
  }
518
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
594
+ // OneKey patch: RecyclerView must use the adapter's measured selector height.
595
+ super.onMeasure(widthMeasureSpec, selectorHeight?.let { MeasureSpec.makeMeasureSpec(it, MeasureSpec.EXACTLY) } ?: heightMeasureSpec)
596
+ val item = tag as? NativeListItem ?: return
597
+ if (item.json.has("height") && item.json.optString("presentation") == "networkSelector" && item.type in setOf("identity", "sectionHeader") && checkbox.visibility == VISIBLE && trailingColumn.parent === this) {
598
+ // OneKey patch: Yoga snaps the compound accessory before its children; their rounded edges can overflow it.
599
+ val density = resources.displayMetrics.density
600
+ val visibleValues = trailingViews.filter { it.visibility == VISIBLE }
601
+ val sourceTrailingWidth = visibleValues.sumOf { it.measuredWidth } + (20 + 12 * visibleValues.size) * density
602
+ val sourceTrailingLeft = (measuredWidth - 12 * density - sourceTrailingWidth).roundToInt()
603
+ val measuredTrailingLeft = measuredWidth - paddingRight - trailingColumn.measuredWidth
604
+ val mainWidth = (mainColumn.measuredWidth + sourceTrailingLeft - measuredTrailingLeft).coerceAtLeast(0)
605
+ mainColumn.measure(MeasureSpec.makeMeasureSpec(mainWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mainColumn.measuredHeight, MeasureSpec.EXACTLY))
606
+ }
607
+ }
608
+
609
+ // OneKey patch: Yoga rounds a half-pixel text center upward; LinearLayout truncates it.
610
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
611
+ super.onLayout(changed, left, top, right, bottom)
612
+ val item = tag as? NativeListItem ?: return
613
+ val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
614
+ if (item.type == "identity" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && accessory?.optString("kind") == "icon" && accessory.optString("name") == "PlusSmallOutline") {
615
+ // OneKey patch: the borderless Plus retains the source's fixed top18/negative7 slot.
616
+ val icon = trailingIcons[0]
617
+ trailingColumn.offsetTopAndBottom(dp(18) - dp(7) - trailingColumn.top - icon.top)
618
+ }
619
+ if (item.type == "action" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && leadingIcon.visibility == VISIBLE) {
620
+ // OneKey patch: Yoga rounds the 4dp padding inside the 32dp Add account icon upward.
621
+ leadingIcon.offsetLeftAndRight((leadingFrame.width - leadingIcon.width + 1) / 2 - leadingIcon.left)
622
+ leadingIcon.offsetTopAndBottom((leadingFrame.height - leadingIcon.height + 1) / 2 - leadingIcon.top)
623
+ }
624
+ if (!item.json.has("height")) return
625
+ val isNetworkIdentity = item.type == "identity" && item.json.optString("presentation") == "networkSelector"
626
+ val isAccountAction = item.type == "action" && item.json.optString("presentation") == "accountSelector"
627
+ val isNetworkSummary = item.type == "sectionHeader" && item.json.optString("presentation") == "networkSelector" && item.json.optString("variant") == "summary"
628
+ val centeredColumns = when {
629
+ isNetworkIdentity || isAccountAction -> listOf(mainColumn, trailingColumn)
630
+ isNetworkSummary -> listOf(trailingColumn)
631
+ else -> return
632
+ }
633
+ for (column in centeredColumns) {
634
+ if (column.parent !== this || column.visibility == GONE) continue
635
+ val margins = column.layoutParams as MarginLayoutParams
636
+ val available = height - paddingTop - paddingBottom - margins.topMargin - margins.bottomMargin
637
+ val desiredTop = paddingTop + margins.topMargin + (available - column.height + 1) / 2
638
+ column.offsetTopAndBottom(desiredTop - column.top)
639
+ }
519
640
  }
520
641
 
521
642
  fun bind(
@@ -526,12 +647,16 @@ internal class NativeListRowView(
526
647
  itemIndex: Int?,
527
648
  selected: Boolean,
528
649
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
650
+ useSourceScale: Boolean = false,
529
651
  ) {
652
+ val shouldRestorePressed = touchPressed && boundKey == item.key
530
653
  invalidateCurrentBinding()
531
654
  bindingEpoch += 1
532
655
  boundKey = item.key
656
+ touchPressed = shouldRestorePressed
533
657
  currentLayout = layout
534
658
  tag = item
659
+ selectorUsesSourceScale = item.usesSelectorSourceScale || useSourceScale
535
660
  reorderActive = false
536
661
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
537
662
  secondaryImage.prepareForReuse()
@@ -544,12 +669,21 @@ internal class NativeListRowView(
544
669
  val accent = color(theme, "accent", "#0D8200FC")
545
670
  checkboxCheckedColor = primary
546
671
  checkboxUncheckedColor = color(theme, "inverseText", "#FCFCFC")
672
+ checkboxIconColor = checkboxUncheckedColor
673
+ checkboxUsesSelectorStyle = item.json.optString("presentation") == "networkSelector"
547
674
  checkboxBorderColor = Color.argb(
548
675
  0x31,
549
676
  0,
550
677
  0,
551
678
  0,
552
679
  )
680
+ if (item.json.optString("presentation") == "networkSelector") {
681
+ checkboxCheckedColor = color(theme, "checkboxBackground", "#202020")
682
+ checkboxBorderColor = color(theme, "checkboxBorder", "#00000031")
683
+ checkboxIconColor = color(theme, "checkboxIcon", "#FFFFFF")
684
+ // OneKey patch: the V1 checkbox fills even its unchecked body with iconInverse.
685
+ checkboxUncheckedColor = checkboxIconColor
686
+ }
553
687
  iconSubduedColor = color(theme, "iconSubdued", "#00000072")
554
688
  visualBackdropColor = color(theme, "rowBackground", "#FFFFFF")
555
689
  unreadDot.background = roundedFill(
@@ -569,7 +703,7 @@ internal class NativeListRowView(
569
703
  if (item.type == "rail") "#0000000F" else "#00000017",
570
704
  ),
571
705
  )
572
- background = restingRowBackground
706
+ background = if (touchPressed || reorderActive) pressedRowBackground else restingRowBackground
573
707
  if (layout == "table") {
574
708
  if (item.type == "dataRow") {
575
709
  setPadding(dp(20), dp(10), dp(20), dp(10))
@@ -592,8 +726,14 @@ internal class NativeListRowView(
592
726
  invalidate()
593
727
  trailingViews.forEach { it.setTextColor(primary) }
594
728
  isEnabled = !item.json.optBoolean("disabled", false)
595
- alpha = if (isEnabled) 1f else 0.5f
729
+ if (!isEnabled) touchPressed = false
730
+ background = if (touchPressed || reorderActive) pressedRowBackground else restingRowBackground
731
+ // OneKey patch: deprecation dims the row without disabling menu controls.
732
+ // alpha = if (isEnabled) 1f else 0.5f
733
+ alpha = item.json.optDouble("opacity", 1.0).toFloat() * (if (isEnabled) 1f else 0.5f)
596
734
  contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
735
+ // OneKey patch: retain stable original selector test identifiers.
736
+ setTag(com.facebook.react.R.id.react_test_id, item.json.optString("testID").takeIf { it.isNotEmpty() })
597
737
 
598
738
  when (item.type) {
599
739
  "walletGroup" -> bindWalletGroup(item, theme, layout, listOrientation, checkboxState)
@@ -609,10 +749,34 @@ internal class NativeListRowView(
609
749
  "system" -> bindSystem(item, theme)
610
750
  }
611
751
  applySize(item)
752
+ if (item.type == "system" && item.json.optString("variant") == "warning") {
753
+ title.typeface = NativeListFonts.medium(context)
754
+ title.textSize = sp(14f)
755
+ subtitle.textSize = sp(14f)
756
+ TextViewCompat.setLineHeight(title, dp(20))
757
+ TextViewCompat.setLineHeight(subtitle, dp(20))
758
+ minimumHeight = 0
759
+ }
612
760
  applyListOrientation(item, listOrientation)
761
+ applySelectorTypography(item)
762
+ }
763
+
764
+ // OneKey patch: keep a small idle member pool after reuse or a direct rebind.
765
+ // The compact proxy/expansion keeps every member until it leaves that state.
766
+ private fun trimWalletGroupRows(required: Int) {
767
+ val retained = maxOf(8, required)
768
+ while (walletGroupRows.size > retained) {
769
+ val row = walletGroupRows.removeAt(walletGroupRows.lastIndex)
770
+ (row.parent as? ViewGroup)?.removeView(row)
771
+ row.dispose()
772
+ row.onRowPress = null
773
+ row.onAction = null
774
+ row.onBindingInvalidated = null
775
+ }
613
776
  }
614
777
 
615
778
  fun recycle() {
779
+ touchPressed = false
616
780
  restoreRestingBackground()
617
781
  invalidateCurrentBinding()
618
782
  boundKey = null
@@ -625,6 +789,7 @@ internal class NativeListRowView(
625
789
  row.alpha = 1f
626
790
  row.recycle()
627
791
  }
792
+ if (!reorderActive && walletGroupExpandAnimator?.isRunning != true) trimWalletGroupRows(0)
628
793
  }
629
794
 
630
795
  fun bindSelection(
@@ -636,6 +801,8 @@ internal class NativeListRowView(
636
801
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
637
802
  ) {
638
803
  if (boundKey != item.key) return
804
+ // OneKey patch: keep row callbacks and checkbox fallback state current without resetting images.
805
+ tag = item
639
806
  if (item.type == "walletGroup") {
640
807
  val members = buildList {
641
808
  add(item.json.getJSONObject("parent"))
@@ -669,7 +836,18 @@ internal class NativeListRowView(
669
836
  ),
670
837
  )
671
838
  }
672
- boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
839
+ // OneKey patch: the comparator permits state changes only on these existing checkbox slots.
840
+ // boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
841
+ if (boundCheckboxData != null) {
842
+ val latestCheckbox = if (item.type == "identity") {
843
+ item.json.optJSONArray("trailing")?.let { trailing ->
844
+ (0 until trailing.length()).mapNotNull { trailing.optJSONObject(it) }
845
+ .lastOrNull { it.optString("kind") == "checkbox" }
846
+ }
847
+ } else item.json.optJSONObject("checkbox")
848
+ boundCheckboxData = latestCheckbox ?: boundCheckboxData
849
+ boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
850
+ }
673
851
  }
674
852
 
675
853
  fun bindStableSummary(item: NativeListItem) {
@@ -684,10 +862,14 @@ internal class NativeListRowView(
684
862
  contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
685
863
  title.text = item.json.optString("title")
686
864
  trailingViews[0].text = item.json.optString("value")
865
+ applySelectorTypography(item)
687
866
  }
688
867
 
689
868
  fun dispose() {
869
+ invalidateCurrentBinding()
690
870
  restoreRestingBackground()
871
+ selectorImages.forEach(OneKeyImageReusableView::dispose)
872
+ selectorImages.clear()
691
873
  leadingImages.forEach(OneKeyImageReusableView::dispose)
692
874
  secondaryImage.dispose()
693
875
  mediaNetworkImage.dispose()
@@ -699,7 +881,8 @@ internal class NativeListRowView(
699
881
  sourceView: View,
700
882
  source: String,
701
883
  slot: Int? = null,
702
- ) = NativeListActionOrigin(sourceView, this, bindingEpoch, source, slot)
884
+ ) = NativeListActionOrigin(sourceView, this, bindingEpoch, source, slot,
885
+ if ((tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" && sourceView in trailingIcons && (sourceView.layoutParams as MarginLayoutParams).marginStart < 0) dp(7) else 0)
703
886
 
704
887
  private fun emitAction(
705
888
  item: NativeListItem,
@@ -712,13 +895,65 @@ internal class NativeListRowView(
712
895
  onAction?.invoke(item, actionKey, target, actionOrigin(sourceView, source, slot))
713
896
  }
714
897
 
898
+ // OneKey patch: match SizableText TABULAR_NUMS on dynamic labels without replacing their typeface.
899
+ private fun applySelectorTypography(item: NativeListItem) {
900
+ val usesSelectorTypography = item.json.optString("presentation") in setOf("accountSelector", "networkSelector", "walletSidebar") || item.type == "system" && item.json.optString("variant") == "warning"
901
+ fun visit(view: View) {
902
+ if (view is OneKeyIconView) view.useSourceScale = selectorUsesSourceScale
903
+ if (view is DottedUnderlineTextView) view.useSourceScale = selectorUsesSourceScale
904
+ if (view is TextView && usesSelectorTypography) {
905
+ val selectorLineHeight = selectorLineHeights.getOrPut(view) { view.lineHeight }
906
+ val original = view.fontFeatureSettings
907
+ if (!selectorOriginalFontFeatures.containsKey(view)) selectorOriginalFontFeatures[view] = original
908
+ view.fontFeatureSettings = if (original.isNullOrEmpty()) "tnum" else if (original.contains("tnum")) original else "$original, 'tnum' 1"
909
+ // OneKey patch: SizableText disables font scaling and rounds font sizes to whole pixels.
910
+ val sourceTypography = selectorUsesSourceScale || item.type == "system" && item.json.optString("variant") == "warning"
911
+ if (sourceTypography) {
912
+ // OneKey patch: React Native CustomStyleSpan disables hinting and preserves fractional advances.
913
+ selectorOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
914
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
915
+ val originalSize = selectorFontSizes.getOrPut(view) { view.textSize }
916
+ val sourceSize = originalSize * resources.displayMetrics.density / resources.displayMetrics.scaledDensity
917
+ view.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
918
+ view.letterSpacing = 0f
919
+ }
920
+ if (sourceTypography && view.text.isNotEmpty()) {
921
+ val text = SpannableStringBuilder(view.text)
922
+ text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
923
+ text.setSpan(SelectorLineHeightSpan(selectorLineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
924
+ view.setLineSpacing(0f, 1f)
925
+ view.text = text
926
+ }
927
+ }
928
+ if (view is ViewGroup) for (index in 0 until view.childCount) visit(view.getChildAt(index))
929
+ }
930
+ visit(this)
931
+ }
932
+
715
933
  private fun invalidateCurrentBinding() {
934
+ selectorImageRetries.forEach { (image, retry) -> image.removeCallbacks(retry) }
935
+ selectorImageRetries.clear()
716
936
  if (boundKey == null) return
717
937
  onBindingInvalidated?.invoke(this, bindingEpoch)
718
938
  bindingEpoch += 1
719
939
  }
720
940
 
721
941
  private fun resetViews() {
942
+ clipToPadding = true
943
+ selectorOriginalFontFeatures.forEach { (view, original) -> view.fontFeatureSettings = original }
944
+ selectorOriginalFontFeatures.clear()
945
+ selectorOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
946
+ selectorOriginalPaintFlags.clear()
947
+ selectorLineHeights.clear()
948
+ selectorFontSizes.clear()
949
+ // OneKey patch: selector-only views cannot survive a recycled binding.
950
+ selectorViews.forEach { (it.parent as? ViewGroup)?.removeView(it) }
951
+ selectorViews.clear()
952
+ selectorImages.forEach(OneKeyImageReusableView::dispose)
953
+ selectorImages.clear()
954
+ selectorHeight = null
955
+ title.setOnClickListener(null)
956
+ title.isClickable = false
722
957
  walletGroupRows.forEach { it.invalidateCurrentBinding() }
723
958
  walletGroupExpandAnimator?.removeAllListeners()
724
959
  walletGroupExpandAnimator?.cancel()
@@ -736,6 +971,11 @@ internal class NativeListRowView(
736
971
  activityContentRow.removeAllViews()
737
972
  (actionLine.parent as? ViewGroup)?.removeView(actionLine)
738
973
  removeAllViews()
974
+ // OneKey patch: the old animator is cancelled and old children are detached.
975
+ // Do not trim currently needed members when re-binding an expanded group.
976
+ val nextItem = tag as? NativeListItem
977
+ val required = if (nextItem?.type == "walletGroup") (nextItem.json.optJSONArray("children")?.length() ?: 0) + 1 else 0
978
+ trimWalletGroupRows(required)
739
979
  orientation = HORIZONTAL
740
980
  gravity = Gravity.CENTER_VERTICAL
741
981
  minimumHeight = 0
@@ -801,6 +1041,7 @@ internal class NativeListRowView(
801
1041
  trailingColumn.layoutParams = wrap()
802
1042
  trailingViews.forEach {
803
1043
  it.visibility = GONE
1044
+ it.setTag(com.facebook.react.R.id.react_test_id, null)
804
1045
  it.gravity = Gravity.END
805
1046
  it.maxLines = 1
806
1047
  it.layoutParams = wrap()
@@ -879,7 +1120,8 @@ internal class NativeListRowView(
879
1120
  mainColumn.removeView(skeletonSecondary)
880
1121
  setOnClickListener { view ->
881
1122
  (view.tag as? NativeListItem)?.let { item ->
882
- onRowPress?.invoke(item, actionOrigin(view, "row"))
1123
+ // OneKey patch: allow create-address accessories when whole-row press is gated.
1124
+ if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
883
1125
  }
884
1126
  }
885
1127
  }
@@ -1003,9 +1245,12 @@ internal class NativeListRowView(
1003
1245
  members.add(children.getJSONObject(index))
1004
1246
  }
1005
1247
  }
1248
+ if (members.first().has("height")) setPadding(dp(1), dp(1), dp(1), dp(1))
1006
1249
  walletGroupDragChildCount = members.size - 1
1007
1250
  walletGroupExpandedHeightPx =
1008
- dp(members.size * 68 + walletGroupDragChildCount * 12)
1251
+ // OneKey patch: expanded groups include individual wallet badge heights.
1252
+ // dp(members.size * 68 + walletGroupDragChildCount * 12)
1253
+ dp(members.sumOf { it.optInt("height", if ((it.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68) } + walletGroupDragChildCount * 12 + if (members.first().has("height")) 2 else 0)
1009
1254
  walletGroupDragBadgeBackgroundPaint.color = color(
1010
1255
  theme,
1011
1256
  "inverseBackground",
@@ -1048,8 +1293,11 @@ internal class NativeListRowView(
1048
1293
  null,
1049
1294
  memberJson.optBoolean("selected", false),
1050
1295
  checkboxState,
1296
+ useSourceScale = selectorUsesSourceScale,
1051
1297
  )
1052
- memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(68)).apply {
1298
+ // OneKey patch: member geometry matches the outer group height calculation.
1299
+ // memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(68)).apply {
1300
+ memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(memberJson.optInt("height", if ((memberJson.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68))).apply {
1053
1301
  if (index > 0) topMargin = dp(12)
1054
1302
  }
1055
1303
  addView(memberRow)
@@ -1082,8 +1330,11 @@ internal class NativeListRowView(
1082
1330
  titleLine.gravity = Gravity.CENTER
1083
1331
  titleLine.packsChildrenAtStart = false
1084
1332
  titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1085
- title.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
1333
+ // OneKey patch: the original wallet title ellipsizes within the full inner row width.
1334
+ title.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1086
1335
  title.gravity = Gravity.CENTER
1336
+ // OneKey patch: this branch returns before the common identity ellipsis setup.
1337
+ if (item.json.has("height")) title.ellipsize = TextUtils.TruncateAt.END
1087
1338
  showText(title, item.json.optString("title"), 1)
1088
1339
  title.setTextColor(
1089
1340
  color(
@@ -1095,13 +1346,41 @@ internal class NativeListRowView(
1095
1346
  addView(
1096
1347
  mainColumn,
1097
1348
  LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1098
- topMargin = dp(4)
1349
+ // OneKey patch: snap the source 4 + 40 + 4 sequence once instead of rounding each gap.
1350
+ topMargin = if (item.json.has("height")) dp(48) - dp(44) else dp(4)
1099
1351
  },
1100
1352
  )
1353
+ // OneKey patch: wallet tags are a centered line below the wallet name.
1354
+ item.json.optJSONArray("badges")?.takeIf { it.length() > 0 }?.let { badges ->
1355
+ val isSelector = item.json.has("height")
1356
+ val badgeLineHeight = if (isSelector) 14 else 16
1357
+ val badgeHeight = badgeLineHeight + 4
1358
+ val line = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER }
1359
+ for (index in 0 until badges.length()) {
1360
+ val badge = TextView(context).apply {
1361
+ text = badges.getJSONObject(index).optString("text")
1362
+ textSize = sp(if (isSelector) 11f else 12f)
1363
+ typeface = NativeListFonts.regular(context)
1364
+ includeFontPadding = false
1365
+ maxLines = 1
1366
+ ellipsize = TextUtils.TruncateAt.END
1367
+ val warning = isSelector && badges.getJSONObject(index).optString("tone") == "warning"
1368
+ setTextColor(color(theme, if (warning) "caution" else "secondaryText", if (warning) "#AB6400" else "#0000009B"))
1369
+ background = roundedFill(color(theme, if (warning) "cautionBackground" else if (isSelector) "subduedBackground" else "strongBackground", if (warning) "#FFF4D5" else "#00000006"), 4f)
1370
+ setPadding(dp(if (isSelector) 6 else 4), dp(2), dp(if (isSelector) 6 else 4), dp(2))
1371
+ }
1372
+ TextViewCompat.setLineHeight(badge, dp(badgeLineHeight))
1373
+ line.addView(badge, LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply { if (index > 0) marginStart = dp(4) })
1374
+ }
1375
+ mainColumn.addView(line, LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeHeight)).apply { topMargin = dp(4) })
1376
+ selectorViews.add(line)
1377
+ }
1101
1378
  return
1102
1379
  }
1103
1380
  if (item.json.optString("presentation") == "networkSelector") {
1104
- setPadding(dp(12), dp(7), dp(12), dp(8))
1381
+ // OneKey patch: the explicit 48-point row centers its 32-point network icon.
1382
+ // setPadding(dp(12), dp(7), dp(12), dp(8))
1383
+ setPadding(dp(12), dp(if (item.json.has("height")) 8 else 7), dp(12), dp(8))
1105
1384
  }
1106
1385
  val leading = item.json.optJSONObject("leading")
1107
1386
  item.json.optJSONObject("leadingAction")?.let { action ->
@@ -1136,6 +1415,13 @@ internal class NativeListRowView(
1136
1415
  item.json.optString("presentation") == "accountSelector"
1137
1416
  ) 32 else 40,
1138
1417
  )
1418
+ // OneKey patch: custom network initials retain LetterAvatar typography.
1419
+ if (item.json.optString("presentation") == "networkSelector" && leading?.optJSONObject("image") == null && leading?.optJSONObject("fallbackIcon") == null && !leading?.optString("fallbackText").isNullOrEmpty()) {
1420
+ leadingFallback.textSize = sp(19f)
1421
+ leadingFallback.typeface = NativeListFonts.semibold(context)
1422
+ leadingFallback.setTextColor(color(theme, "inverseText", "#FCFCFC"))
1423
+ TextViewCompat.setLineHeight(leadingFallback, dp(27))
1424
+ }
1139
1425
  addView(mainColumn, weighted())
1140
1426
  titleLine.packsChildrenAtStart = true
1141
1427
  title.ellipsize = TextUtils.TruncateAt.END
@@ -1144,6 +1430,43 @@ internal class NativeListRowView(
1144
1430
  title.typeface = NativeListFonts.regular(context)
1145
1431
  }
1146
1432
  showText(subtitle, item.json.optString("subtitle"), item.json.optInt("subtitleLines", 2))
1433
+ // OneKey patch: use separate labels for independently truncated balance/address.
1434
+ item.json.optJSONArray("subtitleSegments")?.takeIf { it.length() > 0 }?.let { segments ->
1435
+ subtitle.visibility = GONE
1436
+ val line = SelectorSubtitleLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
1437
+ for (index in 0 until segments.length()) {
1438
+ val segment = segments.getJSONObject(index)
1439
+ if (segment.optBoolean("separatorBefore", false)) {
1440
+ val dot = View(context).apply { background = roundedFill(color(theme, "disabledText", "#00000072"), 2f) }
1441
+ line.addView(dot, LayoutParams(dp(4), dp(4)).apply { marginStart = dp(6); marginEnd = dp(6) })
1442
+ }
1443
+ val label = TextView(context).apply {
1444
+ text = segment.optString("text")
1445
+ typeface = NativeListFonts.regular(context)
1446
+ textSize = sp(14f)
1447
+ includeFontPadding = false
1448
+ maxLines = 1
1449
+ ellipsize = TextUtils.TruncateAt.END
1450
+ val toneKey = when (segment.optString("tone")) { "primary" -> "primaryText"; "disabled" -> "disabledText"; "caution" -> "caution"; "positive" -> "positive"; "negative" -> "negative"; else -> "secondaryText" }
1451
+ setTextColor(color(theme, toneKey, if (toneKey == "caution") "#AB6400" else "#0000009B"))
1452
+ }
1453
+ TextViewCompat.setLineHeight(label, dp(20))
1454
+ applyValueSegments(label, segment.optJSONArray("textSegments"), 14, 20, false)
1455
+ line.addView(label, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
1456
+ }
1457
+ mainColumn.addView(line, 2, LayoutParams(LayoutParams.MATCH_PARENT, dp(20)))
1458
+ selectorViews.add(line)
1459
+ }
1460
+ item.json.optJSONArray("titleMatch")?.takeIf { it.length() > 0 }?.let { matches ->
1461
+ val highlighted = SpannableStringBuilder(title.text)
1462
+ for (index in 0 until matches.length()) {
1463
+ val match = matches.getJSONObject(index)
1464
+ val start = match.optInt("start")
1465
+ val end = match.optInt("end")
1466
+ if (start >= 0 && end > start && end <= highlighted.length) highlighted.setSpan(ForegroundColorSpan(color(theme, "info", "#0D74CE")), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
1467
+ }
1468
+ title.text = highlighted
1469
+ }
1147
1470
  showText(tertiary, item.json.optString("tertiary"), 1)
1148
1471
  tertiary.setTextColor(
1149
1472
  color(
@@ -1164,6 +1487,10 @@ internal class NativeListRowView(
1164
1487
  }
1165
1488
  addView(trailingColumn, wrap())
1166
1489
  val accessories = item.json.optJSONArray("trailing")
1490
+ if (item.json.has("height") && item.json.optString("presentation") == "networkSelector" && accessories.hasAccessory("checkbox")) {
1491
+ // OneKey patch: retain ListItem's title-to-accessory gap when measuring truncation.
1492
+ (mainColumn.layoutParams as LayoutParams).marginEnd = dp(12)
1493
+ }
1167
1494
  if (accessories.hasAccessory("checkbox") && accessories.hasAccessory("value")) {
1168
1495
  trailingColumn.orientation = HORIZONTAL
1169
1496
  trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
@@ -1847,10 +2174,15 @@ internal class NativeListRowView(
1847
2174
  val isSummary = variant == "summary"
1848
2175
  val isGallery = variant == "gallery"
1849
2176
  val isTable = currentLayout == "table"
2177
+ // OneKey patch: title help emits its own frame instead of toggling the section.
2178
+ item.json.optString("titleActionKey").takeIf { it.isNotEmpty() }?.let { key ->
2179
+ title.setOnClickListener { emitAction(item, key, null, title, "leadingAction") }
2180
+ }
1850
2181
  val isNetworkSelector = item.json.optString("presentation") == "networkSelector"
1851
2182
  val isHistory = variant == "history" || item.sectionKey?.startsWith("history-") == true
1852
2183
  val isTokenManager = item.sectionKey in setOf("linear-tokens", "action-tokens")
1853
- val hasDottedTitle = isSummary || isNetworkSelector ||
2184
+ val isExplicitNetworkHeader = isNetworkSelector && item.json.has("height")
2185
+ val hasDottedTitle = isSummary || (isNetworkSelector && (!isExplicitNetworkHeader || item.json.optString("titleActionKey").isNotEmpty())) ||
1854
2186
  (item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null)
1855
2187
  // Linear/sectioned snapshots reserve the ListItem mx=8 at RecyclerView
1856
2188
  // level, so header-local insets below are source px minus that outer inset.
@@ -1870,7 +2202,8 @@ internal class NativeListRowView(
1870
2202
  ),
1871
2203
  )
1872
2204
  if (isNetworkSelector) {
1873
- setPadding(dp(headerHorizontalInset), dp(12), dp(headerHorizontalInset), dp(12))
2205
+ val verticalInset = if (isExplicitNetworkHeader && item.json.optString("titleActionKey").isEmpty()) 8 else 12
2206
+ setPadding(dp(headerHorizontalInset), dp(verticalInset), dp(headerHorizontalInset), dp(verticalInset))
1874
2207
  title.textSize = sp(14f)
1875
2208
  title.typeface = NativeListFonts.medium(context)
1876
2209
  TextViewCompat.setLineHeight(title, dp(20))
@@ -1924,14 +2257,22 @@ internal class NativeListRowView(
1924
2257
  item.json.optString("valueActionKey"),
1925
2258
  color(theme, "secondaryText", "#0000009B"),
1926
2259
  )
2260
+ trailingViews[0].setTag(com.facebook.react.R.id.react_test_id, item.json.optString("valueActionTestID").takeIf { it.isNotEmpty() })
2261
+ trailingViews[0].accessibilityDelegate = selectorAccessibilityDelegate
1927
2262
  trailingViews[0].textSize = sp(16f)
1928
2263
  trailingViews[0].typeface = NativeListFonts.medium(context)
1929
2264
  TextViewCompat.setLineHeight(trailingViews[0], dp(24))
1930
- trailingViews[0].setPadding(dp(14), dp(6), dp(14), dp(6))
2265
+ // OneKey patch: the migrated media button shares the original 24-point text box.
2266
+ if (isExplicitNetworkHeader) trailingViews[0].setPadding(0, 0, 0, 0)
2267
+ else trailingViews[0].setPadding(dp(14), dp(6), dp(14), dp(6))
1931
2268
  trailingViews[0].layoutParams = wrap()
1932
2269
  } else if (!isGallery && !isHistory && !isTokenManager) {
1933
2270
  val value = item.json.optString("value")
1934
2271
  val checkboxData = item.json.optJSONObject("checkbox")
2272
+ if (isExplicitNetworkHeader && checkboxData != null) {
2273
+ // OneKey patch: the asset section title reserves its original 8dp trailing margin.
2274
+ (mainColumn.layoutParams as LayoutParams).marginEnd = dp(8)
2275
+ }
1935
2276
  if (checkboxData != null && value.isNotEmpty()) {
1936
2277
  // Value and checkbox share the trailing edge as one compound accessory.
1937
2278
  trailingColumn.orientation = HORIZONTAL
@@ -1966,6 +2307,27 @@ internal class NativeListRowView(
1966
2307
  }
1967
2308
  checkboxData?.let { bindCheckbox(item, it, checkboxState) }
1968
2309
  }
2310
+ applyValueSegments(trailingViews[0], item.json.optJSONArray("valueSegments"))
2311
+ }
2312
+
2313
+ // OneKey patch: preserve compact zero-count digits without changing their baseline.
2314
+ private fun applyValueSegments(view: TextView, segments: JSONArray?, fontSize: Int = 16, lineHeight: Int = 24, medium: Boolean = true) {
2315
+ if (segments == null || segments.length() == 0) return
2316
+ val value = SpannableStringBuilder()
2317
+ for (index in 0 until segments.length()) {
2318
+ val segment = segments.getJSONObject(index)
2319
+ val start = value.length
2320
+ value.append(segment.optString("text"))
2321
+ if (segment.optString("style") == "subscript") {
2322
+ val size = kotlin.math.ceil(fontSize * 0.6).toFloat()
2323
+ val span = if (selectorUsesSourceScale) AbsoluteSizeSpan(kotlin.math.ceil((size * resources.displayMetrics.density).toDouble()).toInt(), false) else AbsoluteSizeSpan(sp(size).roundToInt(), true)
2324
+ value.setSpan(span, start, value.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2325
+ }
2326
+ }
2327
+ view.textSize = sp(fontSize.toFloat())
2328
+ view.typeface = if (medium) NativeListFonts.medium(context) else NativeListFonts.regular(context)
2329
+ TextViewCompat.setLineHeight(view, dp(lineHeight))
2330
+ view.text = value
1969
2331
  }
1970
2332
 
1971
2333
  private fun bindAction(
@@ -1978,12 +2340,13 @@ internal class NativeListRowView(
1978
2340
  addLeading(icon, if (isAccountSelector) 32 else 40)
1979
2341
  leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
1980
2342
  if (!icon.has("backgroundColor")) leadingFrame.background = null
2343
+ if (isAccountSelector) leadingFrame.background = roundedFill(safeColor(icon.optString("backgroundColor"), color(theme, "strongBackground", "#0000000F")), 8f)
1981
2344
  }
1982
2345
  addView(mainColumn, weighted())
1983
2346
  showText(title, item.json.optString("title"), 1)
1984
2347
  if (isAccountSelector) {
1985
- title.typeface = NativeListFonts.regular(context)
1986
- title.setTextColor(color(theme, "secondaryText", "#0000009B"))
2348
+ title.typeface = if (item.json.has("icon")) NativeListFonts.medium(context) else NativeListFonts.regular(context)
2349
+ title.setTextColor(color(theme, if (item.json.optString("tone") == "primary") "primaryText" else "secondaryText", "#0000009B"))
1987
2350
  } else if (item.json.optString("tone") == "danger") {
1988
2351
  title.setTextColor(color(theme, "negative", "#C40006D3"))
1989
2352
  }
@@ -2003,6 +2366,17 @@ internal class NativeListRowView(
2003
2366
 
2004
2367
  private fun bindSystem(item: NativeListItem, theme: JSONObject?) {
2005
2368
  val variant = item.json.optString("variant")
2369
+ // OneKey patch: warning title/description wrap inside the actual scroll content.
2370
+ if (variant == "warning") {
2371
+ setPadding(dp(12), dp(14), dp(12), dp(14))
2372
+ addView(mainColumn, weighted())
2373
+ showText(title, item.json.optString("title"), Int.MAX_VALUE)
2374
+ showText(subtitle, item.json.optString("message"), Int.MAX_VALUE)
2375
+ title.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
2376
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { topMargin = dp(4) }
2377
+ titleLine.packsChildrenAtStart = false
2378
+ return
2379
+ }
2006
2380
  if (variant == "spacer") {
2007
2381
  minimumHeight = dp(item.json.optInt("height", 0))
2008
2382
  return
@@ -2072,7 +2446,11 @@ internal class NativeListRowView(
2072
2446
  spacingDp: Int = 12,
2073
2447
  ) {
2074
2448
  leadingFrame.visibility = VISIBLE
2075
- leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)).apply { marginEnd = dp(spacingDp) }
2449
+ leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)).apply {
2450
+ val item = tag as? NativeListItem
2451
+ // OneKey patch: Yoga rounds cumulative selector edges, not each 12dp gap separately.
2452
+ marginEnd = if (item?.json?.has("height") == true && item.json.optString("presentation") in setOf("accountSelector", "networkSelector")) dp(12 + sizeDp + spacingDp) - dp(12) - dp(sizeDp) else dp(spacingDp)
2453
+ }
2076
2454
  addView(leadingFrame)
2077
2455
  leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
2078
2456
  if (visual == null) return
@@ -2104,7 +2482,7 @@ internal class NativeListRowView(
2104
2482
  leadingFrame.background = GradientDrawable().apply {
2105
2483
  setColor(visualBackground)
2106
2484
  setStroke(1, parseNativeListColor("#0000001F"))
2107
- cornerRadius = NativeListScale.dp(resources, leadingCornerRadius(shape, sizeDp))
2485
+ cornerRadius = scaledDp(leadingCornerRadius(shape, sizeDp))
2108
2486
  }
2109
2487
  leadingIcon.iconName = visual.optString("name")
2110
2488
  leadingIcon.tintColor = safeColor(
@@ -2161,7 +2539,89 @@ internal class NativeListRowView(
2161
2539
  else -> leadingOutlineProvider(shape)
2162
2540
  }
2163
2541
  image.clipToOutline = true
2164
- bindImage(source, image, boundKey ?: "", index, variant)
2542
+ val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
2543
+ val expectedEpoch = bindingEpoch
2544
+ bindImage(source, image, boundKey ?: "", index, variant,
2545
+ onLoad = if (fallbackIcon == null) null else ({
2546
+ if (bindingEpoch == expectedEpoch) { image.visibility = VISIBLE; leadingIcon.visibility = GONE }
2547
+ }),
2548
+ onError = if (fallbackIcon == null) null else ({
2549
+ if (bindingEpoch == expectedEpoch) {
2550
+ image.visibility = GONE
2551
+ leadingFallback.visibility = GONE
2552
+ leadingIcon.iconName = fallbackIcon.optString("name")
2553
+ leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
2554
+ leadingIcon.visibility = VISIBLE
2555
+ }
2556
+ }),
2557
+ )
2558
+ }
2559
+ // OneKey patch: wallet overlays retain source images, provider colors and QR text.
2560
+ visual.optJSONArray("overlays")?.let { overlays ->
2561
+ for (index in 0 until overlays.length()) {
2562
+ val overlay = overlays.getJSONObject(index)
2563
+ val size = overlay.optInt("size", 20)
2564
+ val inset = dp(overlay.optInt("padding", 0))
2565
+ val isWalletText = selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && overlay.optString("text").isNotEmpty() && overlay.optJSONObject("image") == null && overlay.optString("name").isEmpty()
2566
+ val width = overlay.optInt("width", size)
2567
+ val height = overlay.optInt("height", if (isWalletText) 16 else size)
2568
+ val offsetX = dp(overlay.optInt("offsetX", overlay.optInt("offset", 2)))
2569
+ val offsetY = dp(overlay.optInt("offsetY", overlay.optInt("offset", 2)))
2570
+ val frame = FrameLayout(context).apply {
2571
+ setPadding(if (isWalletText) dp(2) else inset, if (isWalletText) 0 else inset, if (isWalletText) dp(2) else inset, if (isWalletText) 0 else inset)
2572
+ background = roundedFill(safeColor(overlay.optString("backgroundColor"), Color.TRANSPARENT), minOf(width, height) / 2f)
2573
+ outlineProvider = ViewOutlineProvider.BACKGROUND
2574
+ clipToOutline = true
2575
+ }
2576
+ val image = overlay.optJSONObject("image")
2577
+ val view = when {
2578
+ image != null -> OneKeyImageReusableView(reactContext).also {
2579
+ bindImage(image, it, boundKey ?: "", 10 + index, "generic")
2580
+ selectorImages.add(it)
2581
+ }
2582
+ overlay.optString("text").isNotEmpty() -> TextView(context).apply {
2583
+ text = overlay.optString("text")
2584
+ textSize = sp(if (isWalletText) 12f else 10f)
2585
+ typeface = if (isWalletText) NativeListFonts.regular(context) else NativeListFonts.medium(context)
2586
+ includeFontPadding = false
2587
+ gravity = Gravity.CENTER
2588
+ if (isWalletText) TextViewCompat.setLineHeight(this, dp(16))
2589
+ setTextColor(safeColor(overlay.optString("tintColor"), color(null, "secondaryText", "#0000009B")))
2590
+ }
2591
+ else -> OneKeyIconView(context).apply {
2592
+ iconName = overlay.optString("name")
2593
+ tintColor = safeColor(overlay.optString("tintColor"), parseNativeListColor("#0000009B"))
2594
+ }
2595
+ }
2596
+ frame.addView(view, FrameLayout.LayoutParams(if (isWalletText && !overlay.has("width")) FrameLayout.LayoutParams.WRAP_CONTENT else FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT))
2597
+ val topLeft = overlay.optString("position") == "topLeft"
2598
+ leadingFrame.addView(frame, FrameLayout.LayoutParams(if (isWalletText && !overlay.has("width")) FrameLayout.LayoutParams.WRAP_CONTENT else dp(width), dp(height), if (topLeft) Gravity.START or Gravity.TOP else Gravity.END or Gravity.BOTTOM).apply {
2599
+ if (topLeft) { marginStart = -offsetX; topMargin = -offsetY } else { marginEnd = -offsetX; bottomMargin = -offsetY }
2600
+ })
2601
+ selectorViews.add(frame)
2602
+ }
2603
+ }
2604
+ visual.optJSONObject("fallbackIcon")?.takeIf { sources.isEmpty() }?.let { fallbackIcon ->
2605
+ leadingFallback.visibility = GONE
2606
+ leadingIcon.visibility = VISIBLE
2607
+ leadingIcon.iconName = fallbackIcon.optString("name")
2608
+ leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
2609
+ if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && leadingIcon.iconName == "PlusSmallOutline") {
2610
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
2611
+ leadingIcon.glyphSizeDp = 24
2612
+ }
2613
+ if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar" && leadingIcon.iconName == "LockSolid") {
2614
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(40), dp(40), Gravity.CENTER)
2615
+ leadingIcon.glyphSizeDp = 40
2616
+ }
2617
+ }
2618
+ if (visual.optString("borderStyle") == "dashed") {
2619
+ leadingFrame.background = GradientDrawable().apply {
2620
+ setColor(visualBackground)
2621
+ cornerRadius = dp(sizeDp / 2).toFloat()
2622
+ setStroke(dp(if (selectorUsesSourceScale && (tag as? NativeListItem)?.json?.optString("presentation") == "walletSidebar") 1 else 2), safeColor(visual.optString("borderColor"), parseNativeListColor("#00000072")), dp(4).toFloat(), dp(4).toFloat())
2623
+ }
2624
+ leadingFallback.background = null
2165
2625
  }
2166
2626
  }
2167
2627
 
@@ -2223,7 +2683,11 @@ internal class NativeListRowView(
2223
2683
  for (index in 0 until minOf(2, accessories.length())) {
2224
2684
  val accessory = accessories.getJSONObject(index)
2225
2685
  when (accessory.optString("kind")) {
2226
- "value" -> showTrailing(textIndex++, accessory.optString("text"), !accessory.optBoolean("secondary", false))
2686
+ "value" -> {
2687
+ showTrailing(textIndex, accessory.optString("text"), !accessory.optBoolean("secondary", false))
2688
+ applyValueSegments(trailingViews[textIndex], accessory.optJSONArray("textSegments"))
2689
+ textIndex++
2690
+ }
2227
2691
  "valuePair" -> showTrailingValuePair(textIndex++, accessory, theme)
2228
2692
  "checkbox" -> bindCheckbox(item, accessory, checkboxState)
2229
2693
  "radio" -> showTrailing(
@@ -2285,11 +2749,25 @@ internal class NativeListRowView(
2285
2749
  return
2286
2750
  }
2287
2751
  checkbox.visibility = VISIBLE
2288
- checkbox.setState(state, checkboxUncheckedColor)
2289
- checkbox.background = if (state == "unchecked") {
2290
- roundedStroke(checkboxBorderColor, checkboxUncheckedColor, 4f)
2752
+ checkbox.setState(state, checkboxIconColor)
2753
+ val usesSourceCheckboxGeometry = checkboxUsesSelectorStyle && item.json.has("height")
2754
+ checkbox.usesSelectorGeometry = usesSourceCheckboxGeometry
2755
+ if (usesSourceCheckboxGeometry) {
2756
+ // OneKey patch: source Yoga children may round into padding; retain their complete border.
2757
+ clipToPadding = false
2758
+ // OneKey patch: even a transparent source border changes RN's background clipping path.
2759
+ checkbox.background = null
2760
+ BackgroundStyleApplicator.setBackgroundColor(checkbox, if (state == "unchecked") checkboxUncheckedColor else checkboxCheckedColor)
2761
+ BackgroundStyleApplicator.setBorderWidth(checkbox, LogicalEdge.ALL, 2f)
2762
+ BackgroundStyleApplicator.setBorderColor(checkbox, LogicalEdge.ALL, if (state == "unchecked") checkboxBorderColor else Color.TRANSPARENT)
2763
+ BackgroundStyleApplicator.setBorderRadius(checkbox, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(4f, LengthPercentageType.POINT))
2291
2764
  } else {
2292
- roundedFill(checkboxCheckedColor, 4f)
2765
+ checkbox.background = if (state == "unchecked") {
2766
+ if (checkboxUsesSelectorStyle) GradientDrawable().apply { setColor(checkboxUncheckedColor); setStroke(dp(2), checkboxBorderColor); cornerRadius = scaledDp(4f) }
2767
+ else roundedStroke(checkboxBorderColor, checkboxUncheckedColor, 4f)
2768
+ } else {
2769
+ roundedFill(checkboxCheckedColor, 4f)
2770
+ }
2293
2771
  }
2294
2772
  // Row-level disabled opacity already applies to this child. Only apply a
2295
2773
  // local 0.5 when the accessory alone is disabled, never 0.5 * 0.5.
@@ -2330,6 +2808,8 @@ internal class NativeListRowView(
2330
2808
  }
2331
2809
  val groupPosition = when {
2332
2810
  item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> "single"
2811
+ // OneKey patch: explicit selector rows preserve the v1 ListItem corner radius.
2812
+ item.type == "identity" && item.json.optString("presentation") in setOf("accountSelector", "networkSelector") && item.json.has("height") -> "single"
2333
2813
  else -> when (item.type) {
2334
2814
  "metricCard" -> "single"
2335
2815
  "rail" -> "rail"
@@ -2337,7 +2817,7 @@ internal class NativeListRowView(
2337
2817
  }
2338
2818
  }
2339
2819
  var backgroundGroupPosition = if (item.type == "mediaTile") "mediaTile" else groupPosition
2340
- if (layout == "sectioned") {
2820
+ if (layout == "sectioned" && !item.json.optBoolean("selected", false)) {
2341
2821
  // Selection in sectioned lists is represented by the OneKey checkbox,
2342
2822
  // matching iOS and the app-monorepo network selector.
2343
2823
  rowBackground = color(theme, "rowBackground", "#FFFFFF")
@@ -2351,6 +2831,8 @@ internal class NativeListRowView(
2351
2831
  rowBackground = color(theme, "subduedBackground", "#F9F9F9")
2352
2832
  backgroundGroupPosition = ""
2353
2833
  }
2834
+ // OneKey patch: section heading backgrounds are independent of list rows.
2835
+ if (item.json.has("backgroundColor")) rowBackground = safeColor(item.json.optString("backgroundColor"), rowBackground)
2354
2836
  restingRowBackground = groupedBackground(backgroundGroupPosition, rowBackground)
2355
2837
  background = restingRowBackground
2356
2838
  }
@@ -2466,7 +2948,26 @@ internal class NativeListRowView(
2466
2948
  val icon = trailingIcons[index]
2467
2949
  icon.iconName = data.optString("name")
2468
2950
  icon.tintColor = safeColor(data.optString("tintColor"), iconSubduedColor)
2469
- if (icon.iconName == "ChevronRightSmallOutline") {
2951
+ // OneKey patch: preserve the original 38dp press target around its 24dp layout slot.
2952
+ icon.setTag(com.facebook.react.R.id.react_test_id, data.optString("testID").takeIf { it.isNotEmpty() })
2953
+ icon.accessibilityDelegate = selectorAccessibilityDelegate
2954
+ icon.contentDescription = data.optString("accessibilityLabel").takeIf { it.isNotEmpty() }
2955
+ if (item.json.optString("presentation") == "accountSelector") {
2956
+ icon.glyphSizeDp = 24
2957
+ val isSourceMenu = item.json.has("height") && icon.iconName == "DotHorOutline"
2958
+ val size = if (isSourceMenu) 24 else if (item.json.has("height") && icon.iconName == "PlusSmallOutline") 36 else 38
2959
+ icon.layoutParams = LayoutParams(dp(size), dp(size)).apply {
2960
+ gravity = Gravity.CENTER_VERTICAL
2961
+ if (!isSourceMenu) {
2962
+ marginStart = -dp(7)
2963
+ marginEnd = -dp(7)
2964
+ }
2965
+ }
2966
+ if (isSourceMenu) {
2967
+ // OneKey patch: the native ActionList trigger measures 24dp; Yoga rounds its trailing edge cumulatively.
2968
+ setPadding(paddingLeft, paddingTop, (12 * resources.displayMetrics.density).toInt(), paddingBottom)
2969
+ }
2970
+ } else if (icon.iconName == "ChevronRightSmallOutline") {
2470
2971
  icon.glyphSizeDp = null
2471
2972
  // ListItem.DrillIn is a 24dp icon with mx=-6, for a 12dp layout footprint.
2472
2973
  icon.layoutParams = LayoutParams(dp(24), dp(24)).apply {
@@ -2516,7 +3017,7 @@ internal class NativeListRowView(
2516
3017
  when (item.type) {
2517
3018
  "message" -> 14f
2518
3019
  "sectionHeader" -> when {
2519
- isNetworkSelectorSection -> 14f
3020
+ isNetworkSelectorSection && item.json.optString("variant") != "summary" -> 14f
2520
3021
  item.json.optString("variant") == "gallery" -> 18f
2521
3022
  item.json.optString("variant") == "summary" -> 16f
2522
3023
  currentLayout == "table" -> 11f
@@ -2530,11 +3031,14 @@ internal class NativeListRowView(
2530
3031
  }
2531
3032
  },
2532
3033
  )
2533
- title.typeface = if (isWalletSidebar || isAccountSelectorIdentity || isAccountSelectorAction) {
3034
+ title.typeface = if (isAccountSelectorAction && item.json.has("icon")) {
3035
+ NativeListFonts.medium(context)
3036
+ } else if (isWalletSidebar || isAccountSelectorIdentity || isAccountSelectorAction) {
2534
3037
  NativeListFonts.regular(context)
2535
3038
  } else {
2536
3039
  when (item.type) {
2537
3040
  "sectionHeader" -> when {
3041
+ isNetworkSelectorSection && item.json.has("height") && item.json.optString("variant") != "summary" && (item.json.optJSONObject("checkbox") != null || item.json.optString("titleActionKey").isEmpty()) -> NativeListFonts.semibold(context)
2538
3042
  isNetworkSelectorSection -> NativeListFonts.medium(context)
2539
3043
  currentLayout == "table" -> NativeListFonts.regular(context)
2540
3044
  item.json.optString("variant") == "summary" -> NativeListFonts.medium(context)
@@ -2552,7 +3056,7 @@ internal class NativeListRowView(
2552
3056
  else -> 14f
2553
3057
  })
2554
3058
  tertiary.textSize = sp(14f)
2555
- if (isNetworkSelectorSection) {
3059
+ if (isNetworkSelectorSection && item.json.optString("variant") != "summary") {
2556
3060
  TextViewCompat.setLineHeight(title, dp(20))
2557
3061
  } else if (item.type == "sectionHeader" && item.json.optString("variant") == "gallery") {
2558
3062
  TextViewCompat.setLineHeight(title, dp(24))
@@ -2589,11 +3093,42 @@ internal class NativeListRowView(
2589
3093
  column.typeface = NativeListFonts.medium(context)
2590
3094
  column.fontFeatureSettings = "tnum"
2591
3095
  }
3096
+ // OneKey patch: exact selector row dimensions override template minimums.
3097
+ selectorHeight = if (item.json.has("height")) dp(item.json.optInt("height")) else null
3098
+ // OneKey patch: React Native's section spacers and letter blocks truncate physical heights.
3099
+ val isSelectorLetter = isNetworkSelectorSection && item.json.has("height") &&
3100
+ item.json.optString("variant") != "summary" && item.json.optString("titleActionKey").isEmpty() &&
3101
+ item.json.optJSONObject("checkbox") == null
3102
+ val isSelectorSectionSpacer = selectorUsesSourceScale && currentLayout == "sectioned" &&
3103
+ item.type == "system" && item.json.optString("variant") == "spacer"
3104
+ if (isSelectorLetter || isSelectorSectionSpacer) {
3105
+ selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).toInt()
3106
+ }
3107
+ if (isSelectorLetter) {
3108
+ val inset = (20 * resources.displayMetrics.density).toInt() - dp(8)
3109
+ // OneKey patch: SectionHeader has a fixed height and centered text, without vertical padding.
3110
+ setPadding(inset, 0, inset, 0)
3111
+ }
3112
+ if (item.json.has("height") && isNetworkSelectorSection && item.json.optString("variant") != "summary" && item.json.optString("titleActionKey").isNotEmpty() && item.json.optJSONObject("checkbox") == null) {
3113
+ // OneKey patch: the text-and-underline header's fractional measured height rounds up in React Native.
3114
+ selectorHeight = kotlin.math.ceil((item.json.optInt("height") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toDouble()).toInt()
3115
+ }
3116
+ // OneKey patch: an explicit per-row policy preserves each source list's measured heights.
3117
+ if (item.json.has("height")) {
3118
+ when (item.json.optString("heightRounding")) {
3119
+ "floor" -> selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).toInt()
3120
+ "nearest" -> selectorHeight = (item.json.optInt("height") * resources.displayMetrics.density).roundToInt()
3121
+ }
3122
+ }
2592
3123
  val baseHeight = when {
3124
+ item.json.has("height") -> item.json.optInt("height")
2593
3125
  item.type == "system" && item.json.optString("variant") == "spacer" -> item.json.optInt("height", 0)
2594
3126
  item.type == "walletGroup" -> {
2595
3127
  val childCount = item.json.optJSONArray("children")?.length() ?: 0
2596
- (childCount + 1) * 68 + childCount * 12
3128
+ // OneKey patch: include badge heights in the group layout.
3129
+ // (childCount + 1) * 68 + childCount * 12
3130
+ val members = listOf(item.json.getJSONObject("parent")) + (0 until childCount).map { item.json.getJSONArray("children").getJSONObject(it) }
3131
+ members.sumOf { it.optInt("height", if ((it.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68) } + childCount * 12 + if (members.first().has("height")) 2 else 0
2597
3132
  }
2598
3133
  isNetworkSelectorIdentity -> 47
2599
3134
  else -> when (item.type) {
@@ -2617,6 +3152,7 @@ internal class NativeListRowView(
2617
3152
  else -> 36
2618
3153
  }
2619
3154
  "system" -> when (item.json.optString("variant")) {
3155
+ "warning" -> 0
2620
3156
  "noMatch", "end" -> 36
2621
3157
  "retry" -> 44
2622
3158
  else -> 56
@@ -2636,14 +3172,14 @@ internal class NativeListRowView(
2636
3172
  56
2637
3173
  }
2638
3174
  else -> when {
2639
- item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> 68
3175
+ item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> if ((item.json.optJSONArray("badges")?.length() ?: 0) > 0) 92 else 68
2640
3176
  item.type == "identity" && item.json.optString("tertiary").isNotEmpty() -> 72
2641
3177
  item.type == "identity" && item.json.optString("subtitle").isNotEmpty() -> 60
2642
3178
  else -> 56
2643
3179
  }
2644
3180
  }
2645
3181
  }
2646
- val modifier = if (isNetworkSelectorIdentity) {
3182
+ val modifier = if (item.json.has("height") || isNetworkSelectorIdentity) {
2647
3183
  0
2648
3184
  } else if (
2649
3185
  item.type == "sectionHeader" && item.json.optString("variant") in listOf("summary", "gallery")
@@ -2713,7 +3249,12 @@ internal class NativeListRowView(
2713
3249
  override fun getOutline(view: View, outline: Outline) {
2714
3250
  when (shape) {
2715
3251
  "square" -> outline.setRect(0, 0, view.width, view.height)
2716
- "rounded" -> outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
3252
+ "rounded" -> {
3253
+ // OneKey patch: the account avatar has an 8-point radius at its 32-point size.
3254
+ // outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
3255
+ val radius = if ((tag as? NativeListItem)?.json?.optString("presentation") == "accountSelector" && (tag as? NativeListItem)?.json?.has("height") == true) dp(8).toFloat() else dp(10).toFloat()
3256
+ outline.setRoundRect(0, 0, view.width, view.height, radius)
3257
+ }
2717
3258
  else -> outline.setOval(0, 0, view.width, view.height)
2718
3259
  }
2719
3260
  }
@@ -2742,7 +3283,13 @@ internal class NativeListRowView(
2742
3283
  token: String,
2743
3284
  slot: Int,
2744
3285
  variant: String,
3286
+ onLoad: (() -> Unit)? = null,
3287
+ onError: (() -> Unit)? = null,
3288
+ retryAttempt: Int = 0,
2745
3289
  ) {
3290
+ selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
3291
+ val expectedEpoch = bindingEpoch
3292
+ val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
2746
3293
  val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
2747
3294
  imageView.configure(
2748
3295
  sourceUri = uri,
@@ -2751,17 +3298,39 @@ internal class NativeListRowView(
2751
3298
  contentFit = source.optString("contentFit", "cover"),
2752
3299
  cachePolicy = source.optString("cachePolicy", "memory-disk"),
2753
3300
  autoplay = source.optBoolean("autoplay", false),
2754
- recyclingKey = "$token:$slot",
2755
- optimizeTos = source.optBoolean("optimizeTos", true),
3301
+ recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt",
3302
+ optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
2756
3303
  overscan = source.optDouble("overscan", 1.1),
2757
3304
  loadingStrategy = source.optString("loadingStrategy", "static"),
3305
+ onLoad = if (retryLimit == 0) onLoad else ({
3306
+ if (bindingEpoch == expectedEpoch) {
3307
+ selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
3308
+ onLoad?.invoke()
3309
+ }
3310
+ }),
3311
+ onError = if (retryLimit == 0) onError else ({
3312
+ if (bindingEpoch == expectedEpoch) {
3313
+ if (retryAttempt >= retryLimit) onError?.invoke()
3314
+ else if (!selectorImageRetries.containsKey(imageView)) {
3315
+ val retry = Runnable {
3316
+ if (bindingEpoch == expectedEpoch) {
3317
+ selectorImageRetries.remove(imageView)
3318
+ bindImage(source, imageView, token, slot, variant, onLoad, onError, retryAttempt + 1)
3319
+ }
3320
+ }
3321
+ selectorImageRetries[imageView] = retry
3322
+ imageView.postDelayed(retry, kotlin.random.Random.nextLong(3) * 1000L)
3323
+ }
3324
+ }
3325
+ }),
2758
3326
  )
2759
3327
  }
2760
3328
 
2761
3329
  private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
2762
3330
  private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
2763
- private fun dp(value: Int): Int = NativeListScale.dp(resources, value)
2764
- private fun sp(value: Float): Float = NativeListScale.font(resources, value)
3331
+ private fun dp(value: Int): Int = if (selectorUsesSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
3332
+ private fun scaledDp(value: Float): Float = if (selectorUsesSourceScale) (value * resources.displayMetrics.density).roundToInt().toFloat() else NativeListScale.dp(resources, value)
3333
+ private fun sp(value: Float): Float = if (selectorUsesSourceScale) value else NativeListScale.font(resources, value)
2765
3334
 
2766
3335
  private fun color(theme: JSONObject?, key: String, fallback: String): Int =
2767
3336
  safeColor(theme?.optString(key, fallback), parseNativeListColor(fallback))
@@ -2774,30 +3343,30 @@ internal class NativeListRowView(
2774
3343
 
2775
3344
  private fun roundedFill(color: Int, radiusDp: Float) = GradientDrawable().apply {
2776
3345
  setColor(color)
2777
- cornerRadius = NativeListScale.dp(resources, radiusDp)
3346
+ cornerRadius = scaledDp(radiusDp)
2778
3347
  }
2779
3348
 
2780
3349
  private fun roundedStroke(stroke: Int, fill: Int, radiusDp: Float) = GradientDrawable().apply {
2781
3350
  setColor(fill)
2782
3351
  setStroke(dp(2), stroke)
2783
- cornerRadius = NativeListScale.dp(resources, radiusDp)
3352
+ cornerRadius = scaledDp(radiusDp)
2784
3353
  }
2785
3354
 
2786
3355
  private fun roundedHairlineStroke(stroke: Int, radiusDp: Float) = GradientDrawable().apply {
2787
3356
  setColor(Color.TRANSPARENT)
2788
3357
  setStroke(1, stroke)
2789
- cornerRadius = NativeListScale.dp(resources, radiusDp)
3358
+ cornerRadius = scaledDp(radiusDp)
2790
3359
  }
2791
3360
 
2792
3361
  private fun groupedBackground(position: String, color: Int) = GradientDrawable().apply {
2793
3362
  setColor(color)
2794
- val radius = NativeListScale.dp(resources, 12f)
3363
+ val radius = scaledDp(12f)
2795
3364
  cornerRadii = when (position) {
2796
3365
  "first" -> floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f)
2797
3366
  "last" -> floatArrayOf(0f, 0f, 0f, 0f, radius, radius, radius, radius)
2798
3367
  "single" -> FloatArray(8) { radius }
2799
- "rail" -> FloatArray(8) { NativeListScale.dp(resources, 8f) }
2800
- "mediaTile" -> FloatArray(8) { NativeListScale.dp(resources, 16f) }
3368
+ "rail" -> FloatArray(8) { scaledDp(8f) }
3369
+ "mediaTile" -> FloatArray(8) { scaledDp(16f) }
2801
3370
  else -> FloatArray(8)
2802
3371
  }
2803
3372
  }
@@ -2805,6 +3374,7 @@ internal class NativeListRowView(
2805
3374
  }
2806
3375
 
2807
3376
  private class OneKeyIconView(context: android.content.Context) : View(context) {
3377
+ var useSourceScale = false
2808
3378
  var iconName: String = ""
2809
3379
  set(value) {
2810
3380
  field = value
@@ -2828,9 +3398,11 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
2828
3398
  val pathData = iconPaths[iconName] ?: return
2829
3399
  val drawSize = minOf(
2830
3400
  minOf(width, height).toFloat(),
2831
- glyphSizeDp?.let { NativeListScale.dp(resources, it).toFloat() } ?: Float.MAX_VALUE,
3401
+ glyphSizeDp?.let { if (useSourceScale) (it * resources.displayMetrics.density).roundToInt().toFloat() else NativeListScale.dp(resources, it).toFloat() } ?: Float.MAX_VALUE,
2832
3402
  )
2833
- val scale = drawSize / 24f
3403
+ // OneKey patch: custom account-error artwork uses an 18-point viewBox.
3404
+ // val scale = drawSize / 24f
3405
+ val scale = drawSize / (selectorIconViewBoxes[iconName] ?: 24f)
2834
3406
  fill.color = tintColor
2835
3407
  canvas.save()
2836
3408
  canvas.translate((width - drawSize) / 2f, (height - drawSize) / 2f)
@@ -2839,6 +3411,8 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
2839
3411
  pathData.forEachIndexed { index, data ->
2840
3412
  PathParser.createPathFromPathData(data)?.let { path ->
2841
3413
  path.fillType = sourceFillTypes?.getOrNull(index) ?: Path.FillType.EVEN_ODD
3414
+ // OneKey patch: provider illustration colors are part of their source asset.
3415
+ fill.color = selectorIconColors[iconName]?.getOrNull(index) ?: tintColor
2842
3416
  canvas.drawPath(path, fill)
2843
3417
  }
2844
3418
  }
@@ -2846,10 +3420,42 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
2846
3420
  }
2847
3421
 
2848
3422
  companion object {
3423
+ // OneKey patch: preserve provider colors and non-24 viewBoxes.
3424
+ private val selectorIconColors: Map<String, List<Int?>> = mapOf(
3425
+ "GlobusOutline" to listOf(null),
3426
+ "LockSolid" to listOf(null),
3427
+ "GoogleIllus" to listOf(parseNativeListColor("#4285F4"), parseNativeListColor("#34A853"), parseNativeListColor("#FBBC05"), parseNativeListColor("#EA4335")),
3428
+ "AppleBrand" to listOf(null),
3429
+ "BotIllus" to listOf(parseNativeListColor("#8897A5"), parseNativeListColor("#3FA9F5"), parseNativeListColor("#8897A5"), parseNativeListColor("#8897A5"), parseNativeListColor("#10243E"), parseNativeListColor("#10243E"), parseNativeListColor("#10243E")),
3430
+ "AllNetworksSolid" to listOf(null, null),
3431
+ "CrossedSmallSolid" to listOf(null),
3432
+ "AccountErrorCustom" to listOf(Color.argb(0x72, 0, 0, 0), Color.argb(0x72, 0, 0, 0)),
3433
+ "Circle" to listOf(null),
3434
+ )
3435
+ private val selectorIconViewBoxes = mapOf(
3436
+ "GlobusOutline" to 24f,
3437
+ "LockSolid" to 24f,
3438
+ "GoogleIllus" to 24f,
3439
+ "AppleBrand" to 16f,
3440
+ "BotIllus" to 24f,
3441
+ "AllNetworksSolid" to 24f,
3442
+ "CrossedSmallSolid" to 24f,
3443
+ "AccountErrorCustom" to 18f,
3444
+ "Circle" to 24f,
3445
+ )
2849
3446
  // Keep the SVG fill-rule used by each Action-row source path. React Native
2850
3447
  // SVG defaults to nonzero (WINDING); only paths declaring fillRule="evenodd"
2851
3448
  // use EVEN_ODD. Other existing icons retain their prior rendering behavior.
2852
3449
  private val actionIconFillTypes = mapOf(
3450
+ "GlobusOutline" to listOf(Path.FillType.EVEN_ODD),
3451
+ "LockSolid" to listOf(Path.FillType.EVEN_ODD),
3452
+ "GoogleIllus" to listOf(Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING),
3453
+ "AppleBrand" to listOf(Path.FillType.WINDING),
3454
+ "BotIllus" to listOf(Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING, Path.FillType.WINDING),
3455
+ "AllNetworksSolid" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3456
+ "CrossedSmallSolid" to listOf(Path.FillType.WINDING),
3457
+ "AccountErrorCustom" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3458
+ "Circle" to listOf(Path.FillType.WINDING),
2853
3459
  "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
2854
3460
  "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
2855
3461
  "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
@@ -2867,6 +3473,16 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
2867
3473
 
2868
3474
  // Exact 24x24 paths from app-monorepo packages/components Icon sources.
2869
3475
  private val iconPaths = mapOf(
3476
+ // OneKey patch: official selector SVG path geometry.
3477
+ "GlobusOutline" to listOf("M12 2c5.185 0 9.448 3.947 9.95 9H22v2h-.05c-.502 5.053-4.765 9-9.95 9s-9.448-3.947-9.95-9H2v-2h.05C2.552 5.947 6.815 2 12 2M9.523 13c.09 1.982.438 3.726.934 5.002.29.746.612 1.282.917 1.614.304.331.517.384.626.384s.322-.053.626-.384c.305-.332.627-.868.917-1.614.496-1.276.845-3.02.934-5.002zm-5.459 0a8 8 0 0 0 4.8 6.36 10 10 0 0 1-.271-.633C7.994 17.187 7.61 15.189 7.52 13zm12.416 0c-.09 2.189-.474 4.187-1.073 5.727a10 10 0 0 1-.271.633 8 8 0 0 0 4.8-6.36zM8.863 4.639A8 8 0 0 0 4.064 11h3.457c.09-2.189.473-4.187 1.072-5.727q.127-.327.27-.634M12 4c-.109 0-.322.053-.626.384-.305.332-.627.868-.917 1.614-.496 1.276-.844 3.02-.934 5.002h4.954c-.09-1.982-.438-3.726-.934-5.002-.29-.746-.612-1.282-.917-1.614C12.322 4.053 12.109 4 12 4m3.136.639q.144.307.271.634c.599 1.54.982 3.538 1.073 5.727h3.456a8 8 0 0 0-4.8-6.361"),
3478
+ "LockSolid" to listOf("M12 2a5 5 0 0 1 5 5v2h3v13H4V9h3V7a5 5 0 0 1 5-5m-1 11v5h2v-5zm1-9a3 3 0 0 0-3 3v2h6V7a3 3 0 0 0-3-3"),
3479
+ "GoogleIllus" to listOf("M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09", "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23", "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22z", "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53"),
3480
+ "AppleBrand" to listOf("M11.67.834c.117 1.074-.315 2.153-.955 2.928-.64.773-1.692 1.378-2.718 1.298-.14-1.054.38-2.151.971-2.836C9.63 1.45 10.746.872 11.67.834M14.994 7.093c-.176.108-1.992 1.224-1.972 3.482.025 2.769 2.428 3.693 2.46 3.705l-.004.015a10.1 10.1 0 0 1-1.264 2.593c-.764 1.116-1.556 2.229-2.806 2.254-.598.011-1-.162-1.416-.343-.437-.19-.891-.386-1.609-.386-.751 0-1.226.203-1.683.398-.397.169-.78.333-1.32.354-1.208.047-2.124-1.207-2.895-2.32C.909 14.57-.294 10.414 1.322 7.612c.803-1.395 2.237-2.275 3.794-2.298.671-.014 1.32.244 1.89.47.434.172.821.326 1.135.326.282 0 .659-.149 1.099-.323.692-.273 1.539-.607 2.41-.518.599.026 2.276.24 3.354 1.818z"),
3481
+ "BotIllus" to listOf("M11 2a1 1 0 1 1 2 0v1.8l1.6 1.6a1 1 0 1 1-1.4 1.4L12 5.6l-1.2 1.2a1 1 0 0 1-1.4-1.4L11 3.8z", "M8.0 6.0h8.0a5.0 5.0 0 0 1 5.0 5.0v4.0a5.0 5.0 0 0 1 -5.0 5.0h-8.0a5.0 5.0 0 0 1 -5.0 -5.0v-4.0a5.0 5.0 0 0 1 5.0 -5.0z", "M3.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z", "M21.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z", "M7.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0", "M13.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0", "M8.5 15.4c.9.8 2.08 1.2 3.5 1.2s2.6-.4 3.5-1.2c.24-.2.6-.18.8.06.2.23.17.6-.06.8-1.14.98-2.58 1.46-4.24 1.46s-3.1-.48-4.24-1.46a.58.58 0 0 1-.06-.8c.2-.24.56-.26.8-.06"),
3482
+ "AllNetworksSolid" to listOf("M15.333 13.998a1.335 1.335 0 1 1 0 2.67 1.335 1.335 0 0 1 0-2.67", "M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0M8 12.668A2 2 0 0 0 6 14.666V16c0 1.103.895 1.997 1.998 1.998h1.334A2 2 0 0 0 11.33 16v-1.334a2 2 0 0 0-1.998-1.998zm7.333 0a2.665 2.665 0 1 0 0 5.33 2.665 2.665 0 0 0 0-5.33M7.999 6.001A2 2 0 0 0 6.001 8v1.334c0 1.103.895 1.998 1.998 1.998h1.334a2 2 0 0 0 1.998-1.998V7.999a2 2 0 0 0-1.998-1.998zm6.667 0A2 2 0 0 0 12.668 8v1.334c0 1.103.895 1.998 1.998 1.998H16a2 2 0 0 0 1.998-1.998V7.999A2 2 0 0 0 16 6.001z"),
3483
+ "CrossedSmallSolid" to listOf("M17.87 8.25 14.12 12l3.75 3.75-2.12 2.121-3.75-3.75-3.75 3.75-2.121-2.121L9.879 12l-3.75-3.75 2.12-2.121L12 9.879l3.75-3.75 2.122 2.121Z"),
3484
+ "AccountErrorCustom" to listOf("M12.5 12.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5", "M0 3.5A3.5 3.5 0 0 1 3.5 0h8.088A2.41 2.41 0 0 1 14 2.412V5h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H4a4 4 0 0 1-4-4zm2 3.163V14a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H3.5c-.537 0-1.045-.12-1.5-.337M2 3.5A1.5 1.5 0 0 0 3.5 5H12V2.412A.41.41 0 0 0 11.588 2H3.5A1.5 1.5 0 0 0 2 3.5"),
3485
+ "Circle" to listOf("M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0"),
2870
3486
  "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
2871
3487
  "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
2872
3488
  "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),
@@ -2910,6 +3526,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
2910
3526
  }
2911
3527
 
2912
3528
  private class OneKeyCheckboxView(context: android.content.Context) : View(context) {
3529
+ var usesSelectorGeometry = false
2913
3530
  private val glyphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
2914
3531
  private var state = "unchecked"
2915
3532
 
@@ -2926,9 +3543,11 @@ private class OneKeyCheckboxView(context: android.content.Context) : View(contex
2926
3543
  "indeterminate" -> "M4 8a1 1 0 0 1 1-1h6a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1"
2927
3544
  else -> return
2928
3545
  }
2929
- val drawSize = minOf(width, height) * 0.8f
3546
+ // OneKey patch: the source icon is a 16dp child after the 2dp border, not 80% of a rounded frame.
3547
+ val drawSize = if (usesSelectorGeometry) (16 * resources.displayMetrics.density).roundToInt().toFloat() else minOf(width, height) * 0.8f
3548
+ val borderOffset = (2 * resources.displayMetrics.density).roundToInt().toFloat()
2930
3549
  canvas.save()
2931
- canvas.translate((width - drawSize) / 2f, (height - drawSize) / 2f)
3550
+ canvas.translate(if (usesSelectorGeometry) borderOffset else (width - drawSize) / 2f, if (usesSelectorGeometry) borderOffset else (height - drawSize) / 2f)
2932
3551
  canvas.scale(drawSize / 16f, drawSize / 16f)
2933
3552
  PathParser.createPathFromPathData(pathData)?.let { canvas.drawPath(it, glyphPaint) }
2934
3553
  canvas.restore()