@onekeyfe/react-native-native-list 3.0.114 → 3.0.116

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.
@@ -6,12 +6,18 @@ import android.animation.TimeInterpolator
6
6
  import android.animation.ValueAnimator
7
7
  import android.graphics.Color
8
8
  import android.graphics.Canvas
9
+ import android.graphics.LinearGradient
10
+ import android.graphics.Matrix
9
11
  import android.graphics.Outline
10
12
  import android.graphics.Paint
11
13
  import android.graphics.Path
14
+ import android.graphics.RectF
15
+ import android.graphics.Shader
12
16
  import android.graphics.Typeface
13
17
  import android.graphics.drawable.Drawable
14
18
  import android.graphics.drawable.GradientDrawable
19
+ import android.os.Handler
20
+ import android.os.Looper
15
21
  import android.text.Spannable
16
22
  import android.text.SpannableStringBuilder
17
23
  import android.text.TextUtils
@@ -24,6 +30,7 @@ import android.view.MotionEvent
24
30
  import android.view.View
25
31
  import android.view.ViewGroup
26
32
  import android.view.ViewOutlineProvider
33
+ import android.view.animation.LinearInterpolator
27
34
  import android.widget.FrameLayout
28
35
  import android.widget.LinearLayout
29
36
  import android.widget.ProgressBar
@@ -38,10 +45,74 @@ import com.facebook.react.uimanager.style.LogicalEdge
38
45
  import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
39
46
  import androidx.core.graphics.PathParser
40
47
  import androidx.core.widget.TextViewCompat
48
+ import androidx.recyclerview.widget.RecyclerView
41
49
  import org.json.JSONArray
42
50
  import org.json.JSONObject
43
51
  import kotlin.math.roundToInt
44
52
 
53
+ // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
54
+ private class NativeListMarketSkeleton(context: android.content.Context, backgroundColor: Int) : View(context) {
55
+ private val marks = Array(5) { RectF() }
56
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
57
+ private val matrix = Matrix()
58
+ private val shaders = arrayOfNulls<LinearGradient>(5)
59
+ private val dark = Color.red(backgroundColor) * 0.299 + Color.green(backgroundColor) * 0.587 + Color.blue(backgroundColor) * 0.114 < 128
60
+ private val colors = intArrayOf(
61
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
62
+ Color.parseColor(if (dark) "#333333" else "#CDCDCD"),
63
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
64
+ )
65
+ private var phase = 0f
66
+ private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
67
+ duration = 3000
68
+ repeatCount = ValueAnimator.INFINITE
69
+ interpolator = LinearInterpolator()
70
+ addUpdateListener { phase = it.animatedValue as Float; invalidate() }
71
+ }
72
+
73
+ private fun dp(value: Float) = NativeListScale.dp(resources, value)
74
+
75
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
76
+ super.onSizeChanged(w, h, oldw, oldh)
77
+ marks[0].set(0f, 0f, dp(32f), dp(32f))
78
+ marks[1].set(dp(44f), 0f, dp(124f), dp(16f))
79
+ marks[2].set(dp(44f), dp(20f), dp(104f), dp(32f))
80
+ marks[3].set(w - dp(168f), dp(7f), w - dp(88f), dp(25f))
81
+ marks[4].set(w - dp(80f), dp(7f), w.toFloat(), dp(25f))
82
+ marks.forEachIndexed { index, mark ->
83
+ shaders[index] = LinearGradient(0f, 0f, mark.width(), 0f, colors, floatArrayOf(0f, 0.5f, 1f), Shader.TileMode.CLAMP)
84
+ }
85
+ }
86
+
87
+ override fun onDraw(canvas: Canvas) {
88
+ super.onDraw(canvas)
89
+ marks.forEachIndexed { index, mark ->
90
+ matrix.setTranslate(mark.left - mark.width() + phase * mark.width() * 3f, 0f)
91
+ shaders[index]?.setLocalMatrix(matrix)
92
+ paint.shader = shaders[index]
93
+ val radius = dp(if (index == 0) 16f else 8f)
94
+ canvas.drawRoundRect(mark, radius, radius, paint)
95
+ }
96
+ }
97
+
98
+ override fun onAttachedToWindow() {
99
+ super.onAttachedToWindow()
100
+ if (windowVisibility == VISIBLE) animator.start()
101
+ }
102
+
103
+ override fun onDetachedFromWindow() {
104
+ animator.cancel()
105
+ super.onDetachedFromWindow()
106
+ }
107
+
108
+ override fun onWindowVisibilityChanged(visibility: Int) {
109
+ super.onWindowVisibilityChanged(visibility)
110
+ if (visibility == VISIBLE && isAttachedToWindow) {
111
+ if (!animator.isStarted) animator.start()
112
+ } else animator.cancel()
113
+ }
114
+ }
115
+
45
116
  internal data class NativeListActionOrigin(
46
117
  val sourceView: View,
47
118
  val ownerRowView: NativeListRowView,
@@ -49,6 +120,7 @@ internal data class NativeListActionOrigin(
49
120
  val source: String,
50
121
  val slot: Int? = null,
51
122
  val anchorInsetPixels: Int = 0,
123
+ val windowPointPixels: android.graphics.PointF? = null,
52
124
  )
53
125
 
54
126
  /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
@@ -139,6 +211,8 @@ private class DottedUnderlineTextView(context: android.content.Context) : TextVi
139
211
 
140
212
  private class PackedTitleLineLayout(context: android.content.Context) : LinearLayout(context) {
141
213
  var packsChildrenAtStart = false
214
+ // OneKey patch: optional cap for the Market subtitle's localized name.
215
+ var leadingTextMaxWidth = Int.MAX_VALUE
142
216
 
143
217
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
144
218
  if (!packsChildrenAtStart || childCount < 2) {
@@ -147,27 +221,26 @@ private class PackedTitleLineLayout(context: android.content.Context) : LinearLa
147
221
  }
148
222
 
149
223
  val title = getChildAt(0)
150
- val badge = getChildAt(1)
151
224
  val widthMode = MeasureSpec.getMode(widthMeasureSpec)
152
225
  val widthSize = MeasureSpec.getSize(widthMeasureSpec)
153
- if (badge.visibility != GONE) {
226
+ var accessoryWidth = 0
227
+ for (index in 1 until childCount) {
228
+ val child = getChildAt(index)
229
+ if (child.visibility == GONE) continue
154
230
  measureChildWithMargins(
155
- badge,
231
+ child,
156
232
  widthMeasureSpec,
157
- paddingLeft + paddingRight,
233
+ paddingLeft + paddingRight + accessoryWidth,
158
234
  heightMeasureSpec,
159
235
  paddingTop + paddingBottom,
160
236
  )
161
- }
162
-
163
- val badgeMargins = badge.layoutParams as MarginLayoutParams
164
- val badgeWidth = if (badge.visibility == GONE) 0 else {
165
- badge.measuredWidth + badgeMargins.leftMargin + badgeMargins.rightMargin
237
+ val margins = child.layoutParams as MarginLayoutParams
238
+ accessoryWidth += child.measuredWidth + margins.leftMargin + margins.rightMargin
166
239
  }
167
240
  val titleMargins = title.layoutParams as MarginLayoutParams
168
241
  if (widthMode != MeasureSpec.UNSPECIFIED) {
169
- (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - badgeWidth -
170
- titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0)
242
+ (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - accessoryWidth -
243
+ titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0).coerceAtMost(leadingTextMaxWidth)
171
244
  }
172
245
  (title.layoutParams as LayoutParams).apply {
173
246
  width = LayoutParams.WRAP_CONTENT
@@ -324,7 +397,16 @@ private class NativeListTableColumnView(context: android.content.Context) : Line
324
397
  internal class NativeListRowView(
325
398
  private val reactContext: ThemedReactContext,
326
399
  ) : LinearLayout(reactContext) {
327
- private val leadingFrame = FrameLayout(context)
400
+ private var marketLeadingUsesSourceClip = false
401
+ private val leadingFrame = object : FrameLayout(context) {
402
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
403
+ if (!marketLeadingUsesSourceClip || child !== leadingImages[0]) return super.drawChild(canvas, child, drawingTime)
404
+ // OneKey patch: clip only the Market avatar to the source border's padding box.
405
+ val saved = canvas.save()
406
+ BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
407
+ return try { super.drawChild(canvas, child, drawingTime) } finally { canvas.restoreToCount(saved) }
408
+ }
409
+ }
328
410
  // OneKey patch: reset selector fragments and corner decorations on every bind.
329
411
  private val selectorViews = mutableListOf<View>()
330
412
  private var selectorUsesSourceScale = false
@@ -336,6 +418,7 @@ internal class NativeListRowView(
336
418
  }
337
419
  private val selectorOriginalFontFeatures = mutableMapOf<TextView, String?>()
338
420
  private val selectorOriginalPaintFlags = mutableMapOf<TextView, Int>()
421
+ private val marketOriginalPaintFlags = mutableMapOf<TextView, Int>()
339
422
  private val selectorLineHeights = mutableMapOf<TextView, Int>()
340
423
  private val selectorFontSizes = mutableMapOf<TextView, Float>()
341
424
  private val selectorImages = mutableListOf<OneKeyImageReusableView>()
@@ -358,10 +441,16 @@ internal class NativeListRowView(
358
441
  private val titleLine = PackedTitleLineLayout(context)
359
442
  private val title = DottedUnderlineTextView(context)
360
443
  private val subtitle = TextView(context)
444
+ // OneKey patch: the first text shrinks while the volume retains its width.
445
+ private val marketSubtitleLine = PackedTitleLineLayout(context)
361
446
  private val tertiary = TextView(context)
362
447
  private val status = TextView(context)
363
448
  private val metricSubtitle = TextView(context)
364
449
  private val badgeLine = TextView(context)
450
+ private val marketBadgeViews = List(3) { LinearLayout(context) }
451
+ private val marketBadgeLabels = List(3) { TextView(context) }
452
+ private val marketBadgeImages = List(3) { OneKeyImageReusableView(reactContext) }
453
+ private val marketBadgeGlyphs = List(3) { OneKeyIconView(context) }
365
454
  private val activityContentRow = LinearLayout(context)
366
455
  private val actionLine = LinearLayout(context)
367
456
  private val actionViews = List(3) { TextView(context) }
@@ -402,6 +491,13 @@ internal class NativeListRowView(
402
491
  private var pressedRowBackground: Drawable? = null
403
492
  // OneKey patch: preserve a held row independently from RecyclerView snapshot rebinding.
404
493
  private var touchPressed = false
494
+ private val marketLongPressHandler = Handler(Looper.getMainLooper())
495
+ private var marketLongPressRunnable: Runnable? = null
496
+ private var marketTouchStartX = 0f
497
+ private var marketTouchStartY = 0f
498
+ private var marketTouchX = 0f
499
+ private var marketTouchY = 0f
500
+ private var marketLongPressFired = false
405
501
  private var reorderActive = false
406
502
  private var checkboxCheckedColor = Color.rgb(32, 32, 32)
407
503
  private var checkboxUncheckedColor = Color.rgb(252, 252, 252)
@@ -466,6 +562,23 @@ internal class NativeListRowView(
466
562
  titleLine.gravity = Gravity.CENTER_VERTICAL
467
563
  titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
468
564
  titleLine.addView(badgeLine, wrap())
565
+ marketBadgeViews.forEachIndexed { index, badge ->
566
+ badge.orientation = HORIZONTAL
567
+ badge.gravity = Gravity.CENTER_VERTICAL
568
+ badge.isClickable = true
569
+ marketBadgeGlyphs[index].visibility = GONE
570
+ marketBadgeImages[index].visibility = GONE
571
+ marketBadgeLabels[index].apply {
572
+ includeFontPadding = false
573
+ maxLines = 1
574
+ ellipsize = TextUtils.TruncateAt.END
575
+ textSize = sp(11f)
576
+ typeface = NativeListFonts.medium(context)
577
+ }
578
+ badge.addView(marketBadgeGlyphs[index], LayoutParams(dp(16), dp(16)))
579
+ badge.addView(marketBadgeImages[index], LayoutParams(dp(14), dp(14)))
580
+ badge.addView(marketBadgeLabels[index], wrap())
581
+ }
469
582
  mainColumn.addView(titleLine)
470
583
  mainColumn.addView(subtitle)
471
584
  mainColumn.addView(tertiary)
@@ -510,19 +623,55 @@ internal class NativeListRowView(
510
623
  when (event.actionMasked) {
511
624
  MotionEvent.ACTION_DOWN -> if (isEnabled) {
512
625
  touchPressed = true
626
+ marketLongPressFired = false
627
+ val item = tag as? NativeListItem
628
+ if (item?.type == "market") {
629
+ marketTouchStartX = event.x
630
+ marketTouchStartY = event.y
631
+ marketTouchX = event.x
632
+ marketTouchY = event.y
633
+ item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
634
+ emitAction(item, actionKey, null, this, "row")
635
+ }
636
+ item.json.optString("longPressActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
637
+ val expectedKey = item.key
638
+ val runnable = Runnable {
639
+ val current = tag as? NativeListItem
640
+ if (touchPressed && current?.key == expectedKey && current.type == "market") {
641
+ marketLongPressRunnable = null
642
+ marketLongPressFired = true
643
+ val location = IntArray(2)
644
+ getLocationInWindow(location)
645
+ onAction?.invoke(current, actionKey, null, actionOrigin(this, "row").copy(
646
+ windowPointPixels = android.graphics.PointF(location[0] + marketTouchX, location[1] + marketTouchY),
647
+ ))
648
+ }
649
+ }
650
+ marketLongPressRunnable = runnable
651
+ marketLongPressHandler.postDelayed(runnable, 800L)
652
+ }
653
+ }
513
654
  if ((tag as? NativeListItem)?.type == "mediaTile") {
514
655
  leadingFrame.alpha = 0.8f
515
656
  } else {
516
657
  background = pressedRowBackground
517
658
  }
518
659
  }
519
- MotionEvent.ACTION_MOVE -> if (
520
- event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
521
- ) {
522
- touchPressed = false
523
- restoreRestingBackground()
660
+ MotionEvent.ACTION_MOVE -> {
661
+ marketTouchX = event.x
662
+ marketTouchY = event.y
663
+ val outside = event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
664
+ val movedMarket = (tag as? NativeListItem)?.type == "market" &&
665
+ (kotlin.math.abs(event.x - marketTouchStartX) > dp(10) ||
666
+ kotlin.math.abs(event.y - marketTouchStartY) > dp(10))
667
+ if (outside || movedMarket) {
668
+ cancelMarketLongPress()
669
+ touchPressed = false
670
+ restoreRestingBackground()
671
+ }
524
672
  }
525
673
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
674
+ cancelMarketLongPress()
526
675
  touchPressed = false
527
676
  restoreRestingBackground()
528
677
  }
@@ -531,6 +680,10 @@ internal class NativeListRowView(
531
680
  }
532
681
  setOnClickListener { view ->
533
682
  (view.tag as? NativeListItem)?.let { item ->
683
+ if (item.type == "market" && marketLongPressFired) {
684
+ marketLongPressFired = false
685
+ return@let
686
+ }
534
687
  // OneKey patch: allow create-address accessories when whole-row press is gated.
535
688
  if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
536
689
  }
@@ -586,6 +739,22 @@ internal class NativeListRowView(
586
739
  }
587
740
 
588
741
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
742
+ val marketItem = (tag as? NativeListItem)?.takeIf { it.type == "market" }
743
+ val marketStyle = marketItem?.json?.optJSONObject("style")
744
+ if (marketStyle?.has("horizontalPadding") == true) {
745
+ // OneKey patch: Yoga rounds the two absolute edges, not both padding values.
746
+ val sourcePadding = marketStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
747
+ val width = MeasureSpec.getSize(widthMeasureSpec)
748
+ val rowHeight = selectorHeight
749
+ val sourceVerticalPadding = marketStyle.takeIf { it.has("verticalPadding") && rowHeight != null }
750
+ ?.optDouble("verticalPadding")?.times(resources.displayMetrics.density)
751
+ setPadding(
752
+ sourcePadding.roundToInt(),
753
+ sourceVerticalPadding?.roundToInt() ?: paddingTop,
754
+ width - (width - sourcePadding).roundToInt(),
755
+ if (sourceVerticalPadding != null && rowHeight != null) rowHeight - (rowHeight - sourceVerticalPadding).roundToInt() else paddingBottom,
756
+ )
757
+ }
589
758
  if (isMediaTile) {
590
759
  val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
591
760
  .coerceAtLeast(0)
@@ -610,6 +779,63 @@ internal class NativeListRowView(
610
779
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
611
780
  super.onLayout(changed, left, top, right, bottom)
612
781
  val item = tag as? NativeListItem ?: return
782
+ if (item.type == "market" && item.json.optJSONObject("style") != null) {
783
+ // OneKey patch: preserve Yoga's half-pixel centering for Market columns and badges.
784
+ for (column in listOf(leadingFrame, mainColumn, trailingColumn)) {
785
+ if (column.parent === this && column.visibility != GONE) {
786
+ column.offsetTopAndBottom((height - column.height + 1) / 2 - column.top)
787
+ }
788
+ }
789
+ // OneKey patch: token avatars stay centered in the row when text rounding adds a pixel.
790
+ if (item.json.optString("variant") != "token" && leadingFrame.parent === this && mainColumn.parent === this) {
791
+ leadingFrame.offsetTopAndBottom(mainColumn.top + (mainColumn.height - leadingFrame.height + 1) / 2 - leadingFrame.top)
792
+ }
793
+ // OneKey patch: preserve Yoga's absolute-edge rounding for the Market network badge.
794
+ if (leadingOverlayBackground.visibility == VISIBLE && item.json.optString("variant") == "token") {
795
+ val recycler = parent as? RecyclerView
796
+ val adapter = recycler?.adapter as? NativeListAdapter
797
+ val position = recycler?.getChildAdapterPosition(this) ?: RecyclerView.NO_POSITION
798
+ if (recycler != null && adapter != null && position != RecyclerView.NO_POSITION && adapter.itemAt(position)?.key == item.key) {
799
+ val density = resources.displayMetrics.density.toDouble()
800
+ val rowTop = adapter.marketSourceEdgePx(position)
801
+ val rowHeight = adapter.marketSourceEdgePx(position + 1) - rowTop
802
+ val imageHeight = item.json.optJSONObject("style")?.optJSONObject("image")?.optDouble("height", 32.0) ?: 32.0
803
+ val contentTop = (recycler.paddingTop / density).roundToInt() * density
804
+ val badgeTop = contentTop + rowTop + (rowHeight - imageHeight * density) / 2 + (imageHeight - 16) * density
805
+ val badgeHeight = (badgeTop + 20 * density).roundToInt() - badgeTop.roundToInt()
806
+ val top = ((imageHeight - 16) * density).roundToInt()
807
+ leadingOverlayBackground.layout(leadingOverlayBackground.left, top, leadingOverlayBackground.right, top + badgeHeight)
808
+ }
809
+ }
810
+ for (badge in marketBadgeViews) {
811
+ if (badge.parent === titleLine && badge.visibility != GONE) {
812
+ badge.offsetTopAndBottom((titleLine.height - badge.height + 1) / 2 - badge.top)
813
+ }
814
+ }
815
+ // OneKey patch: preserve fractional text advances and gaps until the final edge.
816
+ val style = item.json.optJSONObject("style")
817
+ for ((line, gap) in listOf(
818
+ titleLine to style.optDouble("titleBadgeGap", 4.0),
819
+ marketSubtitleLine to (item.json.optJSONObject("subtitlePrefix")?.optDouble("gap", 4.0) ?: 0.0),
820
+ )) {
821
+ var cursor = 0f
822
+ var hasPrevious = false
823
+ for (index in 0 until line.childCount) {
824
+ val child = line.getChildAt(index)
825
+ if (child.visibility == GONE) continue
826
+ if (hasPrevious) {
827
+ cursor += (gap * resources.displayMetrics.density).toFloat()
828
+ child.offsetLeftAndRight(cursor.roundToInt() - child.left)
829
+ }
830
+ val advance = if (index == 0 && child is TextView) {
831
+ val textWidth = if (line === titleLine) child.paint.measureText(child.text.toString()) else child.layout?.getLineWidth(0)
832
+ textWidth?.coerceAtMost(child.width.toFloat()) ?: child.width.toFloat()
833
+ } else child.width.toFloat()
834
+ cursor = if (hasPrevious) cursor + advance else child.left + advance
835
+ hasPrevious = true
836
+ }
837
+ }
838
+ }
613
839
  val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
614
840
  if (item.type == "identity" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && accessory?.optString("kind") == "icon" && accessory.optString("name") == "PlusSmallOutline") {
615
841
  // OneKey patch: the borderless Plus retains the source's fixed top18/negative7 slot.
@@ -650,13 +876,15 @@ internal class NativeListRowView(
650
876
  useSourceScale: Boolean = false,
651
877
  ) {
652
878
  val shouldRestorePressed = touchPressed && boundKey == item.key
879
+ cancelMarketLongPress()
880
+ marketLongPressFired = false
653
881
  invalidateCurrentBinding()
654
882
  bindingEpoch += 1
655
883
  boundKey = item.key
656
884
  touchPressed = shouldRestorePressed
657
885
  currentLayout = layout
658
886
  tag = item
659
- selectorUsesSourceScale = item.usesSelectorSourceScale || useSourceScale
887
+ selectorUsesSourceScale = item.type == "market" || item.type == "system" && item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry" || item.usesSelectorSourceScale || useSourceScale
660
888
  reorderActive = false
661
889
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
662
890
  secondaryImage.prepareForReuse()
@@ -742,6 +970,7 @@ internal class NativeListRowView(
742
970
  "activity" -> bindActivity(item, theme)
743
971
  "message" -> bindMessage(item, theme)
744
972
  "dataRow" -> bindDataRow(item, theme, checkboxState)
973
+ "market" -> bindMarket(item, theme)
745
974
  "mediaTile" -> bindMediaTile(item, theme)
746
975
  "metricCard" -> bindMetricCard(item, theme)
747
976
  "sectionHeader" -> bindSectionHeader(item, theme, checkboxState)
@@ -776,6 +1005,8 @@ internal class NativeListRowView(
776
1005
  }
777
1006
 
778
1007
  fun recycle() {
1008
+ cancelMarketLongPress()
1009
+ marketLongPressFired = false
779
1010
  touchPressed = false
780
1011
  restoreRestingBackground()
781
1012
  invalidateCurrentBinding()
@@ -866,6 +1097,7 @@ internal class NativeListRowView(
866
1097
  }
867
1098
 
868
1099
  fun dispose() {
1100
+ cancelMarketLongPress()
869
1101
  invalidateCurrentBinding()
870
1102
  restoreRestingBackground()
871
1103
  selectorImages.forEach(OneKeyImageReusableView::dispose)
@@ -874,6 +1106,7 @@ internal class NativeListRowView(
874
1106
  secondaryImage.dispose()
875
1107
  mediaNetworkImage.dispose()
876
1108
  metricVisualImages.forEach(OneKeyImageReusableView::dispose)
1109
+ marketBadgeImages.forEach(OneKeyImageReusableView::dispose)
877
1110
  walletGroupRows.forEach(NativeListRowView::dispose)
878
1111
  }
879
1112
 
@@ -945,6 +1178,8 @@ internal class NativeListRowView(
945
1178
  selectorOriginalFontFeatures.clear()
946
1179
  selectorOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
947
1180
  selectorOriginalPaintFlags.clear()
1181
+ marketOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1182
+ marketOriginalPaintFlags.clear()
948
1183
  selectorLineHeights.clear()
949
1184
  selectorFontSizes.clear()
950
1185
  // OneKey patch: selector-only views cannot survive a recycled binding.
@@ -953,6 +1188,35 @@ internal class NativeListRowView(
953
1188
  selectorImages.forEach(OneKeyImageReusableView::dispose)
954
1189
  selectorImages.clear()
955
1190
  selectorHeight = null
1191
+ marketBadgeViews.forEachIndexed { index, badge ->
1192
+ (badge.parent as? ViewGroup)?.removeView(badge)
1193
+ badge.visibility = GONE
1194
+ badge.background = null
1195
+ badge.setPadding(0, 0, 0, 0)
1196
+ badge.setOnClickListener(null)
1197
+ badge.contentDescription = null
1198
+ marketBadgeLabels[index].apply {
1199
+ // OneKey patch: restore every optional Market badge typography property on reuse.
1200
+ text = ""
1201
+ layoutParams = wrap()
1202
+ fontFeatureSettings = null
1203
+ textSize = sp(11f)
1204
+ typeface = NativeListFonts.medium(context)
1205
+ setLineSpacing(0f, 1f)
1206
+ includeFontPadding = false
1207
+ maxLines = 1
1208
+ ellipsize = TextUtils.TruncateAt.END
1209
+ gravity = Gravity.START or Gravity.CENTER_VERTICAL
1210
+ setTextColor(color(null, "secondaryText", "#0000009B"))
1211
+ }
1212
+ marketBadgeImages[index].prepareForReuse()
1213
+ marketBadgeImages[index].visibility = GONE
1214
+ marketBadgeGlyphs[index].apply {
1215
+ visibility = GONE
1216
+ iconName = ""
1217
+ tintColor = color(null, "secondaryText", "#0000009B")
1218
+ }
1219
+ }
956
1220
  title.setOnClickListener(null)
957
1221
  title.isClickable = false
958
1222
  walletGroupRows.forEach { it.invalidateCurrentBinding() }
@@ -987,6 +1251,9 @@ internal class NativeListRowView(
987
1251
  boundCheckboxData = null
988
1252
  mainColumn.orientation = VERTICAL
989
1253
  mainColumn.gravity = Gravity.CENTER_VERTICAL
1254
+ // OneKey patch: remove the Market group before restoring shared labels.
1255
+ marketSubtitleLine.removeAllViews()
1256
+ mainColumn.removeView(marketSubtitleLine)
990
1257
  mediaMetadataRow.removeView(subtitle)
991
1258
  mediaMetadataRow.removeView(mediaNetworkImage)
992
1259
  mainColumn.removeView(mediaMetadataRow)
@@ -1012,6 +1279,10 @@ internal class NativeListRowView(
1012
1279
  badgeLine.text = ""
1013
1280
  title.setLineSpacing(0f, 1f)
1014
1281
  subtitle.setLineSpacing(0f, 1f)
1282
+ tertiary.setLineSpacing(0f, 1f)
1283
+ tertiary.maxWidth = Int.MAX_VALUE
1284
+ tertiary.ellipsize = TextUtils.TruncateAt.END
1285
+ tertiary.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1015
1286
  title.letterSpacing = 0f
1016
1287
  title.showsDottedUnderline = false
1017
1288
  title.setPadding(0, 0, 0, 0)
@@ -1065,6 +1336,7 @@ internal class NativeListRowView(
1065
1336
  spinner.visibility = GONE
1066
1337
  spinner.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
1067
1338
  spinner.alpha = 1f
1339
+ marketLeadingUsesSourceClip = false
1068
1340
  leadingFrame.visibility = GONE
1069
1341
  leadingFrame.background = null
1070
1342
  leadingFrame.clipChildren = false
@@ -1121,12 +1393,21 @@ internal class NativeListRowView(
1121
1393
  mainColumn.removeView(skeletonSecondary)
1122
1394
  setOnClickListener { view ->
1123
1395
  (view.tag as? NativeListItem)?.let { item ->
1396
+ if (item.type == "market" && marketLongPressFired) {
1397
+ marketLongPressFired = false
1398
+ return@let
1399
+ }
1124
1400
  // OneKey patch: allow create-address accessories when whole-row press is gated.
1125
1401
  if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
1126
1402
  }
1127
1403
  }
1128
1404
  }
1129
1405
 
1406
+ private fun cancelMarketLongPress() {
1407
+ marketLongPressRunnable?.let(marketLongPressHandler::removeCallbacks)
1408
+ marketLongPressRunnable = null
1409
+ }
1410
+
1130
1411
  private fun restoreRestingBackground() {
1131
1412
  background = if (reorderActive) pressedRowBackground else restingRowBackground
1132
1413
  leadingFrame.alpha = 1f
@@ -1733,6 +2014,354 @@ internal class NativeListRowView(
1733
2014
  addView(dataContainer, weighted())
1734
2015
  }
1735
2016
 
2017
+ private fun marketTypeface(weight: String, fallback: String): Typeface = when (
2018
+ weight.ifEmpty { fallback }
2019
+ ) {
2020
+ "regular" -> NativeListFonts.regular(context)
2021
+ "semibold" -> NativeListFonts.semibold(context)
2022
+ "bold" -> NativeListFonts.bold(context)
2023
+ else -> NativeListFonts.medium(context)
2024
+ }
2025
+
2026
+ private fun applyMarketTextStyle(
2027
+ view: TextView,
2028
+ style: JSONObject?,
2029
+ defaultSize: Float,
2030
+ defaultLineHeight: Int,
2031
+ defaultWeight: String,
2032
+ defaultColor: Int,
2033
+ defaultAlignment: String,
2034
+ ) {
2035
+ view.includeFontPadding = false
2036
+ view.fontFeatureSettings = "tnum"
2037
+ view.textSize = sp(style?.optDouble("fontSize", defaultSize.toDouble())?.toFloat() ?: defaultSize)
2038
+ view.typeface = marketTypeface(style?.optString("fontWeight").orEmpty(), defaultWeight)
2039
+ view.setTextColor(safeColor(style?.optString("color"), defaultColor))
2040
+ val alignment = style?.optString("alignment", defaultAlignment) ?: defaultAlignment
2041
+ view.gravity = Gravity.CENTER_VERTICAL or when (alignment) {
2042
+ "center" -> Gravity.CENTER_HORIZONTAL
2043
+ "end" -> Gravity.END
2044
+ else -> Gravity.START
2045
+ }
2046
+ view.maxLines = style?.optInt("lines", 1)?.coerceIn(1, 2) ?: 1
2047
+ view.ellipsize = TextUtils.TruncateAt.END
2048
+ TextViewCompat.setLineHeight(
2049
+ view,
2050
+ dp(style?.optDouble("lineHeight", defaultLineHeight.toDouble())?.roundToInt() ?: defaultLineHeight),
2051
+ )
2052
+ }
2053
+
2054
+ // OneKey patch: opt-in Market text uses the same pixel rounding and line box as RN.
2055
+ private fun applyMarketTextMetrics(view: TextView, style: JSONObject?) {
2056
+ if (style == null || view.visibility != VISIBLE || view.text.isEmpty()) return
2057
+ marketOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
2058
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
2059
+ if (style.has("fontSize")) {
2060
+ val sourceSize = sp(style.optDouble("fontSize").toFloat()) * resources.displayMetrics.density
2061
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
2062
+ }
2063
+ val text = SpannableStringBuilder(view.text)
2064
+ text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
2065
+ if (style.has("lineHeight")) {
2066
+ val lineHeight = kotlin.math.ceil(style.optDouble("lineHeight") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toInt()
2067
+ text.setSpan(SelectorLineHeightSpan(lineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2068
+ view.setLineSpacing(0f, 1f)
2069
+ }
2070
+ view.text = text
2071
+ }
2072
+
2073
+ private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
2074
+ if (segments == null || segments.length() == 0) return value
2075
+ val result = SpannableStringBuilder()
2076
+ for (index in 0 until segments.length()) {
2077
+ val segment = segments.getJSONObject(index)
2078
+ val start = result.length
2079
+ result.append(segment.optString("text"))
2080
+ if (segment.optString("style") == "subscript") {
2081
+ result.setSpan(
2082
+ AbsoluteSizeSpan(sp(kotlin.math.ceil(fontSize * 0.6f)).roundToInt(), true),
2083
+ start,
2084
+ result.length,
2085
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2086
+ )
2087
+ }
2088
+ }
2089
+ return result
2090
+ }
2091
+
2092
+ private fun bindMarket(item: NativeListItem, theme: JSONObject?) {
2093
+ // Network badges extend beyond the leading image's frame.
2094
+ clipChildren = false
2095
+ val variant = item.json.optString("variant")
2096
+ val style = item.json.optJSONObject("style")
2097
+ val imageStyle = style?.optJSONObject("image")
2098
+ val imageWidth = imageStyle?.optDouble("width", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
2099
+ ?: if (variant == "stock") 40 else 32
2100
+ val imageHeight = imageStyle?.optDouble("height", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
2101
+ ?: if (variant == "stock") 40 else 32
2102
+ val horizontalPadding = style?.optDouble("horizontalPadding", if (variant == "perp") 16.0 else 20.0)?.roundToInt()
2103
+ ?: if (variant == "perp") 16 else 20
2104
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0)?.roundToInt() ?: 12
2105
+ val leadingGap = style?.optDouble("leadingGap", if (variant == "perp") 8.0 else 14.0)?.roundToInt()
2106
+ ?: if (variant == "perp") 8 else 14
2107
+ setPadding(dp(horizontalPadding), dp(verticalPadding), dp(horizontalPadding), dp(verticalPadding))
2108
+
2109
+ val leading = JSONObject(item.json.getJSONObject("leading").toString())
2110
+ imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
2111
+ imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
2112
+ leading.optJSONObject("image")?.put("contentFit", contentFit)
2113
+ }
2114
+ val shape = imageStyle?.optString("shape", leading.optString("shape", "circle"))
2115
+ ?: leading.optString("shape", "circle")
2116
+ val cornerRadius = imageStyle?.takeIf { it.has("cornerRadius") }?.optDouble("cornerRadius")?.toFloat()
2117
+ ?: when (shape) {
2118
+ "square" -> 0f
2119
+ "rounded" -> 8f
2120
+ else -> minOf(imageWidth, imageHeight) / 2f
2121
+ }
2122
+ addLeading(
2123
+ leading,
2124
+ sizeDp = imageWidth,
2125
+ spacingDp = leadingGap,
2126
+ heightDp = imageHeight,
2127
+ cornerRadiusDp = cornerRadius,
2128
+ )
2129
+
2130
+ // OneKey patch: retain the source gap without changing other templates.
2131
+ // addView(mainColumn, weighted())
2132
+ addView(mainColumn, weighted().apply {
2133
+ marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
2134
+ })
2135
+ titleLine.packsChildrenAtStart = true
2136
+ showText(title, item.json.optString("title"), style?.optJSONObject("title")?.optInt("lines", 1) ?: 1)
2137
+ applyMarketTextStyle(
2138
+ title,
2139
+ style?.optJSONObject("title"),
2140
+ 16f,
2141
+ 24,
2142
+ "medium",
2143
+ color(theme, "primaryText", "#202020"),
2144
+ "start",
2145
+ )
2146
+ val badges = item.json.optJSONArray("badges")
2147
+ val titleBadgeGap = style?.optDouble("titleBadgeGap", 4.0)?.roundToInt() ?: 4
2148
+ if (badges != null) {
2149
+ for (index in 0 until minOf(marketBadgeViews.size, badges.length())) {
2150
+ val badge = badges.getJSONObject(index)
2151
+ val badgeView = marketBadgeViews[index]
2152
+ val badgeLabel = marketBadgeLabels[index]
2153
+ val badgeStyle = badge.optJSONObject("style")
2154
+ val hasGlyph = badge.optString("iconName") == "verified"
2155
+ val remoteIcon = badge.optJSONObject("icon")
2156
+ val hasIcon = hasGlyph || remoteIcon != null
2157
+ val text = badge.optString("text")
2158
+ val toneColor = when (badge.optString("tone")) {
2159
+ "success" -> color(theme, "positive", "#218358")
2160
+ "danger" -> color(theme, "negative", "#CE2C31")
2161
+ "info" -> color(theme, "info", "#0D74CE")
2162
+ "warning" -> color(theme, "primaryText", "#202020")
2163
+ else -> color(theme, "secondaryText", "#646464")
2164
+ }
2165
+ val foreground = safeColor(badge.optString("textColor"), toneColor)
2166
+ badgeView.visibility = VISIBLE
2167
+ val iconOnly = hasIcon && text.isEmpty()
2168
+ // OneKey patch: Market callers can request the original badge padding.
2169
+ val padding = badgeStyle?.optDouble("horizontalPadding", 5.0)?.roundToInt() ?: 5
2170
+ val leftPadding = if (badgeStyle?.has("horizontalPadding") == true) padding else if (hasIcon) 2 else 5
2171
+ // badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2172
+ badgeView.setPadding(dp(if (iconOnly) 0 else leftPadding), 0, dp(if (iconOnly) 0 else padding), 0)
2173
+ badgeView.background = roundedFill(
2174
+ safeColor(
2175
+ badge.optString("backgroundColor"),
2176
+ if (hasIcon && text.isEmpty()) Color.TRANSPARENT else color(theme, "strongBackground", "#0000000F"),
2177
+ ),
2178
+ 4f,
2179
+ ).apply {
2180
+ if (badgeStyle != null) this.cornerRadius = 4f * resources.displayMetrics.density
2181
+ }
2182
+ badgeLabel.text = text
2183
+ // OneKey patch: match SizableText's tabular numerals only for explicit Market metrics.
2184
+ badgeLabel.fontFeatureSettings = if (badgeStyle != null) "tnum" else null
2185
+ badgeLabel.textSize = sp(badgeStyle?.optDouble("fontSize", 11.0)?.toFloat() ?: 11f)
2186
+ badgeLabel.typeface = marketTypeface(badgeStyle?.optString("fontWeight").orEmpty(), "medium")
2187
+ if (badgeStyle?.has("lineHeight") == true) {
2188
+ TextViewCompat.setLineHeight(badgeLabel, dp(badgeStyle.optDouble("lineHeight").roundToInt()))
2189
+ }
2190
+ badgeLabel.setTextColor(foreground)
2191
+ badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
2192
+ if (hasGlyph) {
2193
+ marketBadgeGlyphs[index].apply {
2194
+ iconName = "BadgeVerifiedSolid"
2195
+ tintColor = foreground
2196
+ visibility = VISIBLE
2197
+ }
2198
+ }
2199
+ remoteIcon?.let { icon ->
2200
+ marketBadgeImages[index].visibility = VISIBLE
2201
+ marketBadgeImages[index].outlineProvider = circleOutlineProvider
2202
+ marketBadgeImages[index].clipToOutline = true
2203
+ bindImage(icon, marketBadgeImages[index], item.key, 20 + index, "generic")
2204
+ }
2205
+ val actionKey = badge.optString("actionKey")
2206
+ badgeView.isClickable = actionKey.isNotEmpty()
2207
+ if (actionKey.isNotEmpty()) {
2208
+ badgeView.setOnClickListener {
2209
+ emitAction(item, actionKey, null, badgeView, "marketBadge", index)
2210
+ }
2211
+ }
2212
+ badgeView.contentDescription = badge.optString("accessibilityLabel", text)
2213
+ titleLine.addView(
2214
+ badgeView,
2215
+ // OneKey patch: preserve 18dp unless the Market row opts into source metrics.
2216
+ // LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2217
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeStyle?.optDouble("height", 18.0)?.roundToInt() ?: 18)).apply { marginStart = dp(titleBadgeGap) },
2218
+ )
2219
+ }
2220
+ }
2221
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2222
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2223
+ }
2224
+ val subtitleText = item.json.optString("subtitle")
2225
+ val subtitleSegments = item.json.optJSONArray("subtitleSegments")
2226
+ if (subtitleText.isNotEmpty() || (subtitleSegments?.length() ?: 0) > 0) {
2227
+ subtitle.visibility = VISIBLE
2228
+ applyMarketTextStyle(
2229
+ subtitle,
2230
+ style?.optJSONObject("subtitle"),
2231
+ 14f,
2232
+ 20,
2233
+ "regular",
2234
+ color(theme, "secondaryText", "#646464"),
2235
+ "start",
2236
+ )
2237
+ subtitle.text = marketText(
2238
+ subtitleText,
2239
+ subtitleSegments,
2240
+ style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2241
+ )
2242
+ }
2243
+ // OneKey patch: preserve independent name metrics, truncation, and volume.
2244
+ val subtitlePrefix = item.json.optJSONObject("subtitlePrefix")
2245
+ val subtitlePadding = style?.optDouble("subtitleTrailingPadding", 0.0)?.roundToInt() ?: 0
2246
+ if (subtitlePrefix != null || subtitlePadding > 0) {
2247
+ mainColumn.removeView(subtitle)
2248
+ mainColumn.removeView(tertiary)
2249
+ marketSubtitleLine.orientation = HORIZONTAL
2250
+ marketSubtitleLine.gravity = Gravity.CENTER_VERTICAL
2251
+ marketSubtitleLine.packsChildrenAtStart = true
2252
+ marketSubtitleLine.leadingTextMaxWidth = subtitlePrefix?.takeIf { it.has("maxWidth") }
2253
+ ?.optDouble("maxWidth")?.roundToInt()?.let(::dp) ?: Int.MAX_VALUE
2254
+ marketSubtitleLine.setPadding(0, 0, dp(subtitlePadding), 0)
2255
+ showText(tertiary, subtitlePrefix?.optString("text") ?: "", 1)
2256
+ applyMarketTextStyle(tertiary, subtitlePrefix?.optJSONObject("style"), 12f, 16, "regular", color(theme, "secondaryText", "#646464"), "start")
2257
+ marketSubtitleLine.addView(tertiary, wrap())
2258
+ marketSubtitleLine.addView(subtitle, wrap().apply {
2259
+ if (tertiary.visibility == VISIBLE && subtitle.visibility == VISIBLE) {
2260
+ marginStart = dp(subtitlePrefix?.optDouble("gap", 4.0)?.roundToInt() ?: 4)
2261
+ }
2262
+ })
2263
+ mainColumn.addView(marketSubtitleLine, 1, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2264
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2265
+ })
2266
+ marketSubtitleLine.visibility = if (tertiary.visibility == VISIBLE || subtitle.visibility == VISIBLE) VISIBLE else GONE
2267
+ }
2268
+ trailingColumn.orientation = HORIZONTAL
2269
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2270
+ addView(trailingColumn, wrap())
2271
+ bindMarketQuote(item, theme)
2272
+ item.json.optJSONObject("diagnostics")?.optString("imageBindActionKey")
2273
+ ?.takeIf(String::isNotEmpty)
2274
+ ?.takeIf { leading.optJSONObject("image") != null || leading.optJSONObject("networkImage") != null }
2275
+ ?.let { actionKey -> onAction?.invoke(item, actionKey, null, null) }
2276
+ }
2277
+
2278
+ fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {
2279
+ if (boundKey != item.key || item.type != "market") return
2280
+ tag = item
2281
+ val style = item.json.optJSONObject("style")
2282
+ val priceStyle = style?.optJSONObject("price")
2283
+ val price = trailingViews[0]
2284
+ price.isClickable = false
2285
+ price.isLongClickable = false
2286
+ price.visibility = VISIBLE
2287
+ price.maxWidth = dp(112)
2288
+ applyMarketTextStyle(
2289
+ price,
2290
+ priceStyle,
2291
+ 16f,
2292
+ 24,
2293
+ "medium",
2294
+ color(theme, "primaryText", "#202020"),
2295
+ "end",
2296
+ )
2297
+ price.text = marketText(
2298
+ item.json.optString("price"),
2299
+ item.json.optJSONArray("priceSegments"),
2300
+ priceStyle?.optDouble("fontSize", 16.0)?.toFloat() ?: 16f,
2301
+ )
2302
+ val trailingGap = style?.optDouble("trailingGap", 8.0)?.roundToInt() ?: 8
2303
+ price.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
2304
+ marginEnd = dp(trailingGap)
2305
+ }
2306
+
2307
+ val changeData = item.json.getJSONObject("change")
2308
+ val changeStyle = style?.optJSONObject("change")
2309
+ val change = trailingViews[1]
2310
+ change.isClickable = false
2311
+ change.isLongClickable = false
2312
+ val toneColor = when (changeData.optString("tone")) {
2313
+ "positive" -> color(theme, "positive", "#218358")
2314
+ "negative" -> color(theme, "negative", "#CE2C31")
2315
+ else -> color(theme, "secondaryText", "#8D8D8D")
2316
+ }
2317
+ val textColor = safeColor(
2318
+ changeData.optString("textColor"),
2319
+ color(theme, "inverseText", "#FFFFFF"),
2320
+ )
2321
+ change.visibility = VISIBLE
2322
+ applyMarketTextStyle(change, changeStyle, 14f, 20, "medium", textColor, "center")
2323
+ change.text = marketText(
2324
+ changeData.optString("text"),
2325
+ changeData.optJSONArray("textSegments"),
2326
+ changeStyle?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2327
+ )
2328
+ change.background = roundedFill(
2329
+ safeColor(changeData.optString("backgroundColor"), toneColor),
2330
+ style?.optDouble("changeCornerRadius", 8.0)?.toFloat() ?: 8f,
2331
+ )
2332
+ change.layoutParams = LayoutParams(
2333
+ dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
2334
+ dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
2335
+ )
2336
+ applyMarketTextMetrics(title, style?.optJSONObject("title"))
2337
+ applyMarketTextMetrics(subtitle, style?.optJSONObject("subtitle"))
2338
+ applyMarketTextMetrics(tertiary, item.json.optJSONObject("subtitlePrefix")?.optJSONObject("style"))
2339
+ applyMarketTextMetrics(price, priceStyle)
2340
+ applyMarketTextMetrics(change, changeStyle)
2341
+ val badges = item.json.optJSONArray("badges")
2342
+ marketBadgeLabels.forEachIndexed { index, label ->
2343
+ val badge = badges?.optJSONObject(index)
2344
+ val badgeStyle = badge?.optJSONObject("style")
2345
+ applyMarketTextMetrics(label, badgeStyle)
2346
+ if (badge != null && label.visibility == VISIBLE && badgeStyle?.has("horizontalPadding") == true &&
2347
+ badge.optJSONObject("icon") == null && badge.optString("iconName").isEmpty()) {
2348
+ // OneKey patch: match RN's intrinsic text width and one rounded padding pair.
2349
+ label.layoutParams = wrap().apply { width = label.paint.measureText(label.text.toString()).roundToInt() }
2350
+ val padding = badgeStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
2351
+ marketBadgeViews[index].setPadding(kotlin.math.floor(padding).toInt(), 0, (padding * 2).roundToInt() - kotlin.math.floor(padding).toInt(), 0)
2352
+ }
2353
+ }
2354
+ contentDescription = item.json.optString(
2355
+ "accessibilityLabel",
2356
+ listOf(
2357
+ item.json.optString("title"),
2358
+ item.json.optString("subtitle"),
2359
+ item.json.optString("price"),
2360
+ changeData.optString("text"),
2361
+ ).filter(String::isNotEmpty).joinToString(", "),
2362
+ )
2363
+ }
2364
+
1736
2365
  private fun styledDataText(
1737
2366
  column: JSONObject,
1738
2367
  badges: JSONArray?,
@@ -2367,6 +2996,65 @@ internal class NativeListRowView(
2367
2996
 
2368
2997
  private fun bindSystem(item: NativeListItem, theme: JSONObject?) {
2369
2998
  val variant = item.json.optString("variant")
2999
+ val isMarket = item.json.optString("presentation") == "market"
3000
+ if (isMarket) setPadding(dp(20), dp(12), dp(20), dp(12))
3001
+ if (variant == "loading" && item.json.optString("loadingStyle") == "skeleton") {
3002
+ setPadding(dp(20), dp(12), dp(20), dp(12))
3003
+ addView(
3004
+ NativeListMarketSkeleton(context, color(theme, "background", "#FFFFFF")),
3005
+ LayoutParams(LayoutParams.MATCH_PARENT, dp(32)),
3006
+ )
3007
+ return
3008
+ }
3009
+ if (variant == "loading" && item.json.optString("loadingStyle") == "spinner") {
3010
+ gravity = Gravity.CENTER
3011
+ setPadding(0, dp(16), 0, dp(16))
3012
+ addView(
3013
+ ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
3014
+ isIndeterminate = true
3015
+ indeterminateTintList = android.content.res.ColorStateList.valueOf(color(theme, "icon", "#0000009B"))
3016
+ },
3017
+ LayoutParams(dp(20), dp(20)),
3018
+ )
3019
+ return
3020
+ }
3021
+ if (isMarket && variant == "retry") {
3022
+ val message = item.json.optString("message")
3023
+ orientation = VERTICAL
3024
+ gravity = Gravity.CENTER
3025
+ setPadding(dp(32), dp(if (message.isEmpty()) 11 else 32), dp(32), dp(if (message.isEmpty()) 11 else 27))
3026
+ if (message.isNotEmpty()) {
3027
+ showText(title, message, 2)
3028
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3029
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3030
+ applyMarketTextMetrics(title, style)
3031
+ addView(mainColumn, wrap().apply { bottomMargin = kotlin.math.ceil(7.0 * resources.displayMetrics.density).toInt() })
3032
+ }
3033
+ showTrailing(0, item.json.optString("actionText", "Retry"), true, item.json.optString("actionKey"))
3034
+ val button = trailingViews[0]
3035
+ val style = JSONObject().put("fontSize", 14).put("lineHeight", 20)
3036
+ applyMarketTextStyle(button, style, 14f, 20, "medium", color(theme, "secondaryText", "#0000009B"), "center")
3037
+ applyMarketTextMetrics(button, style)
3038
+ button.background = null
3039
+ // Match Yoga's text rounding before adding the tertiary Button's border/padding.
3040
+ val density = resources.displayMetrics.density
3041
+ val buttonWidth = (kotlin.math.ceil(button.paint.measureText(button.text.toString()).toDouble()) + 18 * density).roundToInt()
3042
+ val buttonHeight = kotlin.math.ceil(kotlin.math.ceil(20.0 * density) + 10 * density).toInt()
3043
+ button.setPadding(0, 0, 0, 0)
3044
+ button.layoutParams = LayoutParams(buttonWidth, buttonHeight)
3045
+ addView(trailingColumn, wrap())
3046
+ return
3047
+ }
3048
+ if (isMarket && variant == "noMatch") {
3049
+ gravity = Gravity.CENTER
3050
+ setPadding(dp(32), dp(32), dp(32), dp(32))
3051
+ showText(title, item.json.optString("message"), 1)
3052
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3053
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3054
+ applyMarketTextMetrics(title, style)
3055
+ addView(mainColumn, wrap())
3056
+ return
3057
+ }
2370
3058
  // OneKey patch: warning title/description wrap inside the actual scroll content.
2371
3059
  if (variant == "warning") {
2372
3060
  setPadding(dp(12), dp(14), dp(12), dp(14))
@@ -2400,11 +3088,11 @@ internal class NativeListRowView(
2400
3088
  }
2401
3089
  gravity = Gravity.CENTER
2402
3090
  if (variant == "loading") {
2403
- addLeading(JSONObject().put("kind", "skeleton"), 40)
3091
+ addLeading(JSONObject().put("kind", "skeleton"), if (isMarket) 32 else 40)
2404
3092
  leadingFallback.text = ""
2405
3093
  leadingFallback.background = roundedFill(
2406
3094
  color(theme, "strongBackground", "#0000000F"),
2407
- 20f,
3095
+ if (isMarket) 16f else 20f,
2408
3096
  )
2409
3097
  addSkeleton(skeletonPrimary, 120, 12, theme, bottomMarginDp = 8)
2410
3098
  addSkeleton(skeletonSecondary, 80, 12, theme)
@@ -2445,15 +3133,17 @@ internal class NativeListRowView(
2445
3133
  sizeDp: Int = 40,
2446
3134
  secondaryVisual: JSONObject? = null,
2447
3135
  spacingDp: Int = 12,
3136
+ heightDp: Int = sizeDp,
3137
+ cornerRadiusDp: Float? = null,
2448
3138
  ) {
2449
3139
  leadingFrame.visibility = VISIBLE
2450
- leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)).apply {
3140
+ leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(heightDp)).apply {
2451
3141
  val item = tag as? NativeListItem
2452
3142
  // OneKey patch: Yoga rounds cumulative selector edges, not each 12dp gap separately.
2453
3143
  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)
2454
3144
  }
2455
3145
  addView(leadingFrame)
2456
- leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
3146
+ leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
2457
3147
  if (visual == null) return
2458
3148
  val kind = visual.optString("kind")
2459
3149
  val shape = visual.optString(
@@ -2474,16 +3164,19 @@ internal class NativeListRowView(
2474
3164
  if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
2475
3165
  leadingFrame.background = roundedFill(
2476
3166
  visualBackground,
2477
- leadingCornerRadius(shape, sizeDp),
3167
+ cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
2478
3168
  )
2479
3169
  }
2480
- leadingFallback.background = roundedFill(visualBackground, leadingCornerRadius(shape, sizeDp))
3170
+ leadingFallback.background = roundedFill(
3171
+ visualBackground,
3172
+ cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
3173
+ )
2481
3174
  leadingFallback.visibility = if (!isIcon && sources.isEmpty()) VISIBLE else GONE
2482
3175
  if (isIcon) {
2483
3176
  leadingFrame.background = GradientDrawable().apply {
2484
3177
  setColor(visualBackground)
2485
3178
  setStroke(1, parseNativeListColor("#0000001F"))
2486
- cornerRadius = scaledDp(leadingCornerRadius(shape, sizeDp))
3179
+ cornerRadius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
2487
3180
  }
2488
3181
  leadingIcon.iconName = visual.optString("name")
2489
3182
  leadingIcon.tintColor = safeColor(
@@ -2505,12 +3198,19 @@ internal class NativeListRowView(
2505
3198
  if (tokenPair) {
2506
3199
  leadingOverlayBackground.visibility = VISIBLE
2507
3200
  leadingOverlayBackground.background = roundedFill(visualBackdropColor, 10f)
3201
+ val item = tag as? NativeListItem
3202
+ val marketPadding = if (item?.type == "market") item.json.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0 else null
3203
+ val density = resources.displayMetrics.density
3204
+ // Yoga rounds the absolute horizontal edges, including the avatar's fractional origin.
3205
+ val badgeRight = marketPadding?.let { ((it + sizeDp + 4) * density).roundToInt() }
3206
+ val badgeLeft = marketPadding?.let { ((it + sizeDp - 16) * density).roundToInt() }
3207
+ val avatarRight = marketPadding?.let { ((it + sizeDp) * density).roundToInt() }
2508
3208
  leadingOverlayBackground.layoutParams = FrameLayout.LayoutParams(
2509
- dp(20),
3209
+ if (badgeRight != null && badgeLeft != null) badgeRight - badgeLeft else dp(20),
2510
3210
  dp(20),
2511
3211
  Gravity.END or Gravity.BOTTOM,
2512
3212
  ).apply {
2513
- marginEnd = -dp(4)
3213
+ marginEnd = if (badgeRight != null && avatarRight != null) avatarRight - badgeRight else -dp(4)
2514
3214
  bottomMargin = -dp(4)
2515
3215
  }
2516
3216
  }
@@ -2541,13 +3241,38 @@ internal class NativeListRowView(
2541
3241
  index = index,
2542
3242
  count = visibleSources.size,
2543
3243
  sizeDp = sizeDp,
3244
+ heightDp = heightDp,
2544
3245
  tokenPair = tokenPair,
2545
3246
  )
2546
3247
  image.outlineProvider = when {
2547
3248
  tokenPair && index == 1 -> circleOutlineProvider
3249
+ cornerRadiusDp != null -> roundedOutlineProvider(cornerRadiusDp)
2548
3250
  else -> leadingOutlineProvider(shape)
2549
3251
  }
2550
3252
  image.clipToOutline = true
3253
+ if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3254
+ val inset = dp(1)
3255
+ val density = resources.displayMetrics.density
3256
+ val sourceRadius = cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp))
3257
+ // OneKey patch: retain the source's fractional border and background rendering.
3258
+ leadingFrame.background = null
3259
+ BackgroundStyleApplicator.setBackgroundColor(leadingFrame, visualBackground)
3260
+ // Match the source's physical edges: React Native rasterizes ALL as a different stroke path.
3261
+ for (edge in arrayOf(LogicalEdge.LEFT, LogicalEdge.TOP, LogicalEdge.RIGHT, LogicalEdge.BOTTOM)) {
3262
+ BackgroundStyleApplicator.setBorderWidth(leadingFrame, edge, 1f)
3263
+ BackgroundStyleApplicator.setBorderColor(leadingFrame, edge, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3264
+ }
3265
+ BackgroundStyleApplicator.setBorderRadius(leadingFrame, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(sourceRadius, LengthPercentageType.POINT))
3266
+ // OneKey patch: round the source image's absolute edges inside its border.
3267
+ val sourceLeft = ((tag as? NativeListItem)?.json?.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0) * density
3268
+ val imageWidth = (sourceLeft + (sizeDp - 1) * density).roundToInt() - (sourceLeft + density).roundToInt()
3269
+ image.layoutParams = FrameLayout.LayoutParams(imageWidth, dp(heightDp - 2)).apply {
3270
+ leftMargin = inset
3271
+ topMargin = inset
3272
+ }
3273
+ marketLeadingUsesSourceClip = true
3274
+ image.clipToOutline = false
3275
+ }
2551
3276
  val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
2552
3277
  val expectedEpoch = bindingEpoch
2553
3278
  bindImage(source, image, boundKey ?: "", index, variant,
@@ -2664,10 +3389,11 @@ internal class NativeListRowView(
2664
3389
  index: Int,
2665
3390
  count: Int,
2666
3391
  sizeDp: Int,
3392
+ heightDp: Int,
2667
3393
  tokenPair: Boolean,
2668
3394
  ): FrameLayout.LayoutParams {
2669
3395
  if (count == 1 || tokenPair && index == 0) {
2670
- return FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
3396
+ return FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
2671
3397
  }
2672
3398
  if (tokenPair && index == 1) {
2673
3399
  return FrameLayout.LayoutParams(dp(16), dp(16), Gravity.END or Gravity.BOTTOM).apply {
@@ -3009,6 +3735,32 @@ internal class NativeListRowView(
3009
3735
  }
3010
3736
 
3011
3737
  private fun applySize(item: NativeListItem) {
3738
+ if (item.type == "system" && item.json.optString("presentation") == "market" &&
3739
+ item.json.optString("variant") in setOf("retry", "noMatch")) {
3740
+ val defaultHeight = if (item.json.optString("variant") == "noMatch") 88.0 else if (item.json.optString("message").isEmpty()) 52.0 else 120.0
3741
+ minimumHeight = (item.json.optDouble("height", defaultHeight).toFloat() * resources.displayMetrics.density).roundToInt()
3742
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3743
+ return
3744
+ }
3745
+ if (item.type == "market") {
3746
+ val style = item.json.optJSONObject("style")
3747
+ val imageHeight = style?.optJSONObject("image")?.optDouble(
3748
+ "height",
3749
+ if (item.json.optString("variant") == "stock") 40.0 else 32.0,
3750
+ ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3751
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3752
+ val defaultHeight = if (item.json.optString("variant") == "stock") 72.0 else 68.0
3753
+ // OneKey patch: preserve fractional DP until the final physical pixel edge.
3754
+ val rowHeight = item.json.optDouble(
3755
+ "height",
3756
+ maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3757
+ )
3758
+ val sourceScale = if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)
3759
+ minimumHeight = (rowHeight * sourceScale * resources.displayMetrics.density).roundToInt()
3760
+ // OneKey patch: explicit Market row heights must not grow after child pixel rounding.
3761
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3762
+ return
3763
+ }
3012
3764
  val isWalletSidebar =
3013
3765
  item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
3014
3766
  val isAccountSelectorIdentity =
@@ -3161,9 +3913,14 @@ internal class NativeListRowView(
3161
3913
  else -> 36
3162
3914
  }
3163
3915
  "system" -> when (item.json.optString("variant")) {
3916
+ "loading" -> when (item.json.optString("loadingStyle")) {
3917
+ "skeleton" -> 56
3918
+ "spinner" -> 52
3919
+ else -> if (item.json.optString("presentation") == "market") 68 else 56
3920
+ }
3921
+ "noMatch", "retry" -> if (item.json.optString("presentation") == "market") 44 else if (item.json.optString("variant") == "noMatch") 36 else 44
3164
3922
  "warning" -> 0
3165
- "noMatch", "end" -> 36
3166
- "retry" -> 44
3923
+ "end" -> if (item.json.optString("presentation") == "market") 44 else 36
3167
3924
  else -> 56
3168
3925
  }
3169
3926
  "action" -> when {
@@ -3269,6 +4026,12 @@ internal class NativeListRowView(
3269
4026
  }
3270
4027
  }
3271
4028
 
4029
+ private fun roundedOutlineProvider(radiusDp: Float) = object : ViewOutlineProvider() {
4030
+ override fun getOutline(view: View, outline: Outline) {
4031
+ outline.setRoundRect(0, 0, view.width, view.height, scaledDp(radiusDp))
4032
+ }
4033
+ }
4034
+
3272
4035
  private fun JSONArray?.hasAccessory(kind: String): Boolean {
3273
4036
  if (this == null) return false
3274
4037
  return (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
@@ -3465,6 +4228,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3465
4228
  "CrossedSmallSolid" to listOf(Path.FillType.WINDING),
3466
4229
  "AccountErrorCustom" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3467
4230
  "Circle" to listOf(Path.FillType.WINDING),
4231
+ "BadgeVerifiedSolid" to listOf(Path.FillType.EVEN_ODD),
3468
4232
  "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
3469
4233
  "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3470
4234
  "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
@@ -3492,6 +4256,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3492
4256
  "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"),
3493
4257
  "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"),
3494
4258
  "Circle" to listOf("M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0"),
4259
+ "BadgeVerifiedSolid" to listOf("M9.483 11.458v3.5h-1v-3.5z M10.467 2.698a2.03 2.03 0 0 1 3.065 0l1.358 1.564a.03.03 0 0 0 .028.01l2.046-.325a2.03 2.03 0 0 1 2.347 1.971l.037 2.07q0 .016.014.026l1.776 1.066a2.03 2.03 0 0 1 .532 3.019l-1.304 1.609a.03.03 0 0 0-.005.03l.675 1.956a2.03 2.03 0 0 1-1.533 2.656l-2.033.394a.03.03 0 0 0-.023.019l-.741 1.933a2.03 2.03 0 0 1-2.88 1.05l-1.811-1.006a.03.03 0 0 0-.03 0l-1.811 1.005a2.03 2.03 0 0 1-2.88-1.049l-.742-1.933a.03.03 0 0 0-.023-.019l-2.033-.394a2.03 2.03 0 0 1-1.532-2.656l.675-1.957a.03.03 0 0 0-.005-.029l-1.304-1.61a2.03 2.03 0 0 1 .532-3.018l1.776-1.066a.03.03 0 0 0 .014-.026l.035-2.07a2.03 2.03 0 0 1 2.349-1.97l2.045.324a.03.03 0 0 0 .028-.01zm1.516 3.76a.5.5 0 0 0-.447.276l-1.861 3.724H8.483a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h6.692a2 2 0 0 0 1.981-1.73l.341-2.5a2 2 0 0 0-1.982-2.27h-1.939l.197-1.269a1.5 1.5 0 0 0-1.481-1.731z"),
3495
4260
  "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
3496
4261
  "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
3497
4262
  "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),