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

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
@@ -42,6 +49,69 @@ import org.json.JSONArray
42
49
  import org.json.JSONObject
43
50
  import kotlin.math.roundToInt
44
51
 
52
+ // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
53
+ private class NativeListMarketSkeleton(context: android.content.Context, backgroundColor: Int) : View(context) {
54
+ private val marks = Array(5) { RectF() }
55
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
56
+ private val matrix = Matrix()
57
+ private val shaders = arrayOfNulls<LinearGradient>(5)
58
+ private val dark = Color.red(backgroundColor) * 0.299 + Color.green(backgroundColor) * 0.587 + Color.blue(backgroundColor) * 0.114 < 128
59
+ private val colors = intArrayOf(
60
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
61
+ Color.parseColor(if (dark) "#333333" else "#CDCDCD"),
62
+ Color.parseColor(if (dark) "#111111" else "#FAFAFA"),
63
+ )
64
+ private var phase = 0f
65
+ private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
66
+ duration = 3000
67
+ repeatCount = ValueAnimator.INFINITE
68
+ interpolator = LinearInterpolator()
69
+ addUpdateListener { phase = it.animatedValue as Float; invalidate() }
70
+ }
71
+
72
+ private fun dp(value: Float) = NativeListScale.dp(resources, value)
73
+
74
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
75
+ super.onSizeChanged(w, h, oldw, oldh)
76
+ marks[0].set(0f, 0f, dp(32f), dp(32f))
77
+ marks[1].set(dp(44f), 0f, dp(124f), dp(16f))
78
+ marks[2].set(dp(44f), dp(20f), dp(104f), dp(32f))
79
+ marks[3].set(w - dp(168f), dp(7f), w - dp(88f), dp(25f))
80
+ marks[4].set(w - dp(80f), dp(7f), w.toFloat(), dp(25f))
81
+ marks.forEachIndexed { index, mark ->
82
+ shaders[index] = LinearGradient(0f, 0f, mark.width(), 0f, colors, floatArrayOf(0f, 0.5f, 1f), Shader.TileMode.CLAMP)
83
+ }
84
+ }
85
+
86
+ override fun onDraw(canvas: Canvas) {
87
+ super.onDraw(canvas)
88
+ marks.forEachIndexed { index, mark ->
89
+ matrix.setTranslate(mark.left - mark.width() + phase * mark.width() * 3f, 0f)
90
+ shaders[index]?.setLocalMatrix(matrix)
91
+ paint.shader = shaders[index]
92
+ val radius = dp(if (index == 0) 16f else 8f)
93
+ canvas.drawRoundRect(mark, radius, radius, paint)
94
+ }
95
+ }
96
+
97
+ override fun onAttachedToWindow() {
98
+ super.onAttachedToWindow()
99
+ if (windowVisibility == VISIBLE) animator.start()
100
+ }
101
+
102
+ override fun onDetachedFromWindow() {
103
+ animator.cancel()
104
+ super.onDetachedFromWindow()
105
+ }
106
+
107
+ override fun onWindowVisibilityChanged(visibility: Int) {
108
+ super.onWindowVisibilityChanged(visibility)
109
+ if (visibility == VISIBLE && isAttachedToWindow) {
110
+ if (!animator.isStarted) animator.start()
111
+ } else animator.cancel()
112
+ }
113
+ }
114
+
45
115
  internal data class NativeListActionOrigin(
46
116
  val sourceView: View,
47
117
  val ownerRowView: NativeListRowView,
@@ -147,26 +217,25 @@ private class PackedTitleLineLayout(context: android.content.Context) : LinearLa
147
217
  }
148
218
 
149
219
  val title = getChildAt(0)
150
- val badge = getChildAt(1)
151
220
  val widthMode = MeasureSpec.getMode(widthMeasureSpec)
152
221
  val widthSize = MeasureSpec.getSize(widthMeasureSpec)
153
- if (badge.visibility != GONE) {
222
+ var accessoryWidth = 0
223
+ for (index in 1 until childCount) {
224
+ val child = getChildAt(index)
225
+ if (child.visibility == GONE) continue
154
226
  measureChildWithMargins(
155
- badge,
227
+ child,
156
228
  widthMeasureSpec,
157
- paddingLeft + paddingRight,
229
+ paddingLeft + paddingRight + accessoryWidth,
158
230
  heightMeasureSpec,
159
231
  paddingTop + paddingBottom,
160
232
  )
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
233
+ val margins = child.layoutParams as MarginLayoutParams
234
+ accessoryWidth += child.measuredWidth + margins.leftMargin + margins.rightMargin
166
235
  }
167
236
  val titleMargins = title.layoutParams as MarginLayoutParams
168
237
  if (widthMode != MeasureSpec.UNSPECIFIED) {
169
- (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - badgeWidth -
238
+ (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - accessoryWidth -
170
239
  titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0)
171
240
  }
172
241
  (title.layoutParams as LayoutParams).apply {
@@ -362,6 +431,10 @@ internal class NativeListRowView(
362
431
  private val status = TextView(context)
363
432
  private val metricSubtitle = TextView(context)
364
433
  private val badgeLine = TextView(context)
434
+ private val marketBadgeViews = List(3) { LinearLayout(context) }
435
+ private val marketBadgeLabels = List(3) { TextView(context) }
436
+ private val marketBadgeImages = List(3) { OneKeyImageReusableView(reactContext) }
437
+ private val marketBadgeGlyphs = List(3) { OneKeyIconView(context) }
365
438
  private val activityContentRow = LinearLayout(context)
366
439
  private val actionLine = LinearLayout(context)
367
440
  private val actionViews = List(3) { TextView(context) }
@@ -402,6 +475,11 @@ internal class NativeListRowView(
402
475
  private var pressedRowBackground: Drawable? = null
403
476
  // OneKey patch: preserve a held row independently from RecyclerView snapshot rebinding.
404
477
  private var touchPressed = false
478
+ private val marketLongPressHandler = Handler(Looper.getMainLooper())
479
+ private var marketLongPressRunnable: Runnable? = null
480
+ private var marketTouchStartX = 0f
481
+ private var marketTouchStartY = 0f
482
+ private var marketLongPressFired = false
405
483
  private var reorderActive = false
406
484
  private var checkboxCheckedColor = Color.rgb(32, 32, 32)
407
485
  private var checkboxUncheckedColor = Color.rgb(252, 252, 252)
@@ -466,6 +544,23 @@ internal class NativeListRowView(
466
544
  titleLine.gravity = Gravity.CENTER_VERTICAL
467
545
  titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
468
546
  titleLine.addView(badgeLine, wrap())
547
+ marketBadgeViews.forEachIndexed { index, badge ->
548
+ badge.orientation = HORIZONTAL
549
+ badge.gravity = Gravity.CENTER_VERTICAL
550
+ badge.isClickable = true
551
+ marketBadgeGlyphs[index].visibility = GONE
552
+ marketBadgeImages[index].visibility = GONE
553
+ marketBadgeLabels[index].apply {
554
+ includeFontPadding = false
555
+ maxLines = 1
556
+ ellipsize = TextUtils.TruncateAt.END
557
+ textSize = sp(11f)
558
+ typeface = NativeListFonts.medium(context)
559
+ }
560
+ badge.addView(marketBadgeGlyphs[index], LayoutParams(dp(16), dp(16)))
561
+ badge.addView(marketBadgeImages[index], LayoutParams(dp(14), dp(14)))
562
+ badge.addView(marketBadgeLabels[index], wrap())
563
+ }
469
564
  mainColumn.addView(titleLine)
470
565
  mainColumn.addView(subtitle)
471
566
  mainColumn.addView(tertiary)
@@ -510,19 +605,47 @@ internal class NativeListRowView(
510
605
  when (event.actionMasked) {
511
606
  MotionEvent.ACTION_DOWN -> if (isEnabled) {
512
607
  touchPressed = true
608
+ marketLongPressFired = false
609
+ val item = tag as? NativeListItem
610
+ if (item?.type == "market") {
611
+ marketTouchStartX = event.x
612
+ marketTouchStartY = event.y
613
+ item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
614
+ emitAction(item, actionKey, null, this, "row")
615
+ }
616
+ item.json.optString("longPressActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
617
+ val expectedKey = item.key
618
+ val runnable = Runnable {
619
+ val current = tag as? NativeListItem
620
+ if (touchPressed && current?.key == expectedKey && current.type == "market") {
621
+ marketLongPressRunnable = null
622
+ marketLongPressFired = true
623
+ emitAction(current, actionKey, null, this, "row")
624
+ }
625
+ }
626
+ marketLongPressRunnable = runnable
627
+ marketLongPressHandler.postDelayed(runnable, 800L)
628
+ }
629
+ }
513
630
  if ((tag as? NativeListItem)?.type == "mediaTile") {
514
631
  leadingFrame.alpha = 0.8f
515
632
  } else {
516
633
  background = pressedRowBackground
517
634
  }
518
635
  }
519
- MotionEvent.ACTION_MOVE -> if (
520
- event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
521
- ) {
522
- touchPressed = false
523
- restoreRestingBackground()
636
+ MotionEvent.ACTION_MOVE -> {
637
+ val outside = event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
638
+ val movedMarket = (tag as? NativeListItem)?.type == "market" &&
639
+ (kotlin.math.abs(event.x - marketTouchStartX) > dp(10) ||
640
+ kotlin.math.abs(event.y - marketTouchStartY) > dp(10))
641
+ if (outside || movedMarket) {
642
+ cancelMarketLongPress()
643
+ touchPressed = false
644
+ restoreRestingBackground()
645
+ }
524
646
  }
525
647
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
648
+ cancelMarketLongPress()
526
649
  touchPressed = false
527
650
  restoreRestingBackground()
528
651
  }
@@ -531,6 +654,10 @@ internal class NativeListRowView(
531
654
  }
532
655
  setOnClickListener { view ->
533
656
  (view.tag as? NativeListItem)?.let { item ->
657
+ if (item.type == "market" && marketLongPressFired) {
658
+ marketLongPressFired = false
659
+ return@let
660
+ }
534
661
  // OneKey patch: allow create-address accessories when whole-row press is gated.
535
662
  if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
536
663
  }
@@ -650,13 +777,15 @@ internal class NativeListRowView(
650
777
  useSourceScale: Boolean = false,
651
778
  ) {
652
779
  val shouldRestorePressed = touchPressed && boundKey == item.key
780
+ cancelMarketLongPress()
781
+ marketLongPressFired = false
653
782
  invalidateCurrentBinding()
654
783
  bindingEpoch += 1
655
784
  boundKey = item.key
656
785
  touchPressed = shouldRestorePressed
657
786
  currentLayout = layout
658
787
  tag = item
659
- selectorUsesSourceScale = item.usesSelectorSourceScale || useSourceScale
788
+ selectorUsesSourceScale = item.type == "market" || item.usesSelectorSourceScale || useSourceScale
660
789
  reorderActive = false
661
790
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
662
791
  secondaryImage.prepareForReuse()
@@ -742,6 +871,7 @@ internal class NativeListRowView(
742
871
  "activity" -> bindActivity(item, theme)
743
872
  "message" -> bindMessage(item, theme)
744
873
  "dataRow" -> bindDataRow(item, theme, checkboxState)
874
+ "market" -> bindMarket(item, theme)
745
875
  "mediaTile" -> bindMediaTile(item, theme)
746
876
  "metricCard" -> bindMetricCard(item, theme)
747
877
  "sectionHeader" -> bindSectionHeader(item, theme, checkboxState)
@@ -776,6 +906,8 @@ internal class NativeListRowView(
776
906
  }
777
907
 
778
908
  fun recycle() {
909
+ cancelMarketLongPress()
910
+ marketLongPressFired = false
779
911
  touchPressed = false
780
912
  restoreRestingBackground()
781
913
  invalidateCurrentBinding()
@@ -866,6 +998,7 @@ internal class NativeListRowView(
866
998
  }
867
999
 
868
1000
  fun dispose() {
1001
+ cancelMarketLongPress()
869
1002
  invalidateCurrentBinding()
870
1003
  restoreRestingBackground()
871
1004
  selectorImages.forEach(OneKeyImageReusableView::dispose)
@@ -874,6 +1007,7 @@ internal class NativeListRowView(
874
1007
  secondaryImage.dispose()
875
1008
  mediaNetworkImage.dispose()
876
1009
  metricVisualImages.forEach(OneKeyImageReusableView::dispose)
1010
+ marketBadgeImages.forEach(OneKeyImageReusableView::dispose)
877
1011
  walletGroupRows.forEach(NativeListRowView::dispose)
878
1012
  }
879
1013
 
@@ -953,6 +1087,25 @@ internal class NativeListRowView(
953
1087
  selectorImages.forEach(OneKeyImageReusableView::dispose)
954
1088
  selectorImages.clear()
955
1089
  selectorHeight = null
1090
+ marketBadgeViews.forEachIndexed { index, badge ->
1091
+ (badge.parent as? ViewGroup)?.removeView(badge)
1092
+ badge.visibility = GONE
1093
+ badge.background = null
1094
+ badge.setPadding(0, 0, 0, 0)
1095
+ badge.setOnClickListener(null)
1096
+ badge.contentDescription = null
1097
+ marketBadgeLabels[index].apply {
1098
+ text = ""
1099
+ setTextColor(color(null, "secondaryText", "#0000009B"))
1100
+ }
1101
+ marketBadgeImages[index].prepareForReuse()
1102
+ marketBadgeImages[index].visibility = GONE
1103
+ marketBadgeGlyphs[index].apply {
1104
+ visibility = GONE
1105
+ iconName = ""
1106
+ tintColor = color(null, "secondaryText", "#0000009B")
1107
+ }
1108
+ }
956
1109
  title.setOnClickListener(null)
957
1110
  title.isClickable = false
958
1111
  walletGroupRows.forEach { it.invalidateCurrentBinding() }
@@ -1121,12 +1274,21 @@ internal class NativeListRowView(
1121
1274
  mainColumn.removeView(skeletonSecondary)
1122
1275
  setOnClickListener { view ->
1123
1276
  (view.tag as? NativeListItem)?.let { item ->
1277
+ if (item.type == "market" && marketLongPressFired) {
1278
+ marketLongPressFired = false
1279
+ return@let
1280
+ }
1124
1281
  // OneKey patch: allow create-address accessories when whole-row press is gated.
1125
1282
  if (!item.json.optBoolean("pressDisabled", false)) onRowPress?.invoke(item, actionOrigin(view, "row"))
1126
1283
  }
1127
1284
  }
1128
1285
  }
1129
1286
 
1287
+ private fun cancelMarketLongPress() {
1288
+ marketLongPressRunnable?.let(marketLongPressHandler::removeCallbacks)
1289
+ marketLongPressRunnable = null
1290
+ }
1291
+
1130
1292
  private fun restoreRestingBackground() {
1131
1293
  background = if (reorderActive) pressedRowBackground else restingRowBackground
1132
1294
  leadingFrame.alpha = 1f
@@ -1733,6 +1895,272 @@ internal class NativeListRowView(
1733
1895
  addView(dataContainer, weighted())
1734
1896
  }
1735
1897
 
1898
+ private fun marketTypeface(weight: String, fallback: String): Typeface = when (
1899
+ weight.ifEmpty { fallback }
1900
+ ) {
1901
+ "regular" -> NativeListFonts.regular(context)
1902
+ "semibold" -> NativeListFonts.semibold(context)
1903
+ "bold" -> NativeListFonts.bold(context)
1904
+ else -> NativeListFonts.medium(context)
1905
+ }
1906
+
1907
+ private fun applyMarketTextStyle(
1908
+ view: TextView,
1909
+ style: JSONObject?,
1910
+ defaultSize: Float,
1911
+ defaultLineHeight: Int,
1912
+ defaultWeight: String,
1913
+ defaultColor: Int,
1914
+ defaultAlignment: String,
1915
+ ) {
1916
+ view.includeFontPadding = false
1917
+ view.fontFeatureSettings = "tnum"
1918
+ view.textSize = sp(style?.optDouble("fontSize", defaultSize.toDouble())?.toFloat() ?: defaultSize)
1919
+ view.typeface = marketTypeface(style?.optString("fontWeight").orEmpty(), defaultWeight)
1920
+ view.setTextColor(safeColor(style?.optString("color"), defaultColor))
1921
+ val alignment = style?.optString("alignment", defaultAlignment) ?: defaultAlignment
1922
+ view.gravity = Gravity.CENTER_VERTICAL or when (alignment) {
1923
+ "center" -> Gravity.CENTER_HORIZONTAL
1924
+ "end" -> Gravity.END
1925
+ else -> Gravity.START
1926
+ }
1927
+ view.maxLines = style?.optInt("lines", 1)?.coerceIn(1, 2) ?: 1
1928
+ view.ellipsize = TextUtils.TruncateAt.END
1929
+ TextViewCompat.setLineHeight(
1930
+ view,
1931
+ dp(style?.optDouble("lineHeight", defaultLineHeight.toDouble())?.roundToInt() ?: defaultLineHeight),
1932
+ )
1933
+ }
1934
+
1935
+ private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
1936
+ if (segments == null || segments.length() == 0) return value
1937
+ val result = SpannableStringBuilder()
1938
+ for (index in 0 until segments.length()) {
1939
+ val segment = segments.getJSONObject(index)
1940
+ val start = result.length
1941
+ result.append(segment.optString("text"))
1942
+ if (segment.optString("style") == "subscript") {
1943
+ result.setSpan(
1944
+ AbsoluteSizeSpan(sp(kotlin.math.ceil(fontSize * 0.6f)).roundToInt(), true),
1945
+ start,
1946
+ result.length,
1947
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
1948
+ )
1949
+ }
1950
+ }
1951
+ return result
1952
+ }
1953
+
1954
+ private fun bindMarket(item: NativeListItem, theme: JSONObject?) {
1955
+ // Network badges extend beyond the leading image's frame.
1956
+ clipChildren = false
1957
+ val variant = item.json.optString("variant")
1958
+ val style = item.json.optJSONObject("style")
1959
+ val imageStyle = style?.optJSONObject("image")
1960
+ val imageWidth = imageStyle?.optDouble("width", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
1961
+ ?: if (variant == "stock") 40 else 32
1962
+ val imageHeight = imageStyle?.optDouble("height", if (variant == "stock") 40.0 else 32.0)?.roundToInt()
1963
+ ?: if (variant == "stock") 40 else 32
1964
+ val horizontalPadding = style?.optDouble("horizontalPadding", if (variant == "perp") 16.0 else 20.0)?.roundToInt()
1965
+ ?: if (variant == "perp") 16 else 20
1966
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0)?.roundToInt() ?: 12
1967
+ val leadingGap = style?.optDouble("leadingGap", if (variant == "perp") 8.0 else 14.0)?.roundToInt()
1968
+ ?: if (variant == "perp") 8 else 14
1969
+ setPadding(dp(horizontalPadding), dp(verticalPadding), dp(horizontalPadding), dp(verticalPadding))
1970
+
1971
+ val leading = JSONObject(item.json.getJSONObject("leading").toString())
1972
+ imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
1973
+ imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
1974
+ leading.optJSONObject("image")?.put("contentFit", contentFit)
1975
+ }
1976
+ val shape = imageStyle?.optString("shape", leading.optString("shape", "circle"))
1977
+ ?: leading.optString("shape", "circle")
1978
+ val cornerRadius = imageStyle?.takeIf { it.has("cornerRadius") }?.optDouble("cornerRadius")?.toFloat()
1979
+ ?: when (shape) {
1980
+ "square" -> 0f
1981
+ "rounded" -> 8f
1982
+ else -> minOf(imageWidth, imageHeight) / 2f
1983
+ }
1984
+ addLeading(
1985
+ leading,
1986
+ sizeDp = imageWidth,
1987
+ spacingDp = leadingGap,
1988
+ heightDp = imageHeight,
1989
+ cornerRadiusDp = cornerRadius,
1990
+ )
1991
+
1992
+ addView(mainColumn, weighted())
1993
+ titleLine.packsChildrenAtStart = true
1994
+ showText(title, item.json.optString("title"), style?.optJSONObject("title")?.optInt("lines", 1) ?: 1)
1995
+ applyMarketTextStyle(
1996
+ title,
1997
+ style?.optJSONObject("title"),
1998
+ 16f,
1999
+ 24,
2000
+ "medium",
2001
+ color(theme, "primaryText", "#202020"),
2002
+ "start",
2003
+ )
2004
+ val badges = item.json.optJSONArray("badges")
2005
+ val titleBadgeGap = style?.optDouble("titleBadgeGap", 4.0)?.roundToInt() ?: 4
2006
+ if (badges != null) {
2007
+ for (index in 0 until minOf(marketBadgeViews.size, badges.length())) {
2008
+ val badge = badges.getJSONObject(index)
2009
+ val badgeView = marketBadgeViews[index]
2010
+ val badgeLabel = marketBadgeLabels[index]
2011
+ val hasGlyph = badge.optString("iconName") == "verified"
2012
+ val remoteIcon = badge.optJSONObject("icon")
2013
+ val hasIcon = hasGlyph || remoteIcon != null
2014
+ val text = badge.optString("text")
2015
+ val toneColor = when (badge.optString("tone")) {
2016
+ "success" -> color(theme, "positive", "#218358")
2017
+ "danger" -> color(theme, "negative", "#CE2C31")
2018
+ "info" -> color(theme, "info", "#0D74CE")
2019
+ "warning" -> color(theme, "primaryText", "#202020")
2020
+ else -> color(theme, "secondaryText", "#646464")
2021
+ }
2022
+ val foreground = safeColor(badge.optString("textColor"), toneColor)
2023
+ badgeView.visibility = VISIBLE
2024
+ val iconOnly = hasIcon && text.isEmpty()
2025
+ badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2026
+ badgeView.background = roundedFill(
2027
+ safeColor(
2028
+ badge.optString("backgroundColor"),
2029
+ if (hasIcon && text.isEmpty()) Color.TRANSPARENT else color(theme, "strongBackground", "#0000000F"),
2030
+ ),
2031
+ 4f,
2032
+ )
2033
+ badgeLabel.text = text
2034
+ badgeLabel.setTextColor(foreground)
2035
+ badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
2036
+ if (hasGlyph) {
2037
+ marketBadgeGlyphs[index].apply {
2038
+ iconName = "BadgeVerifiedSolid"
2039
+ tintColor = foreground
2040
+ visibility = VISIBLE
2041
+ }
2042
+ }
2043
+ remoteIcon?.let { icon ->
2044
+ marketBadgeImages[index].visibility = VISIBLE
2045
+ marketBadgeImages[index].outlineProvider = circleOutlineProvider
2046
+ marketBadgeImages[index].clipToOutline = true
2047
+ bindImage(icon, marketBadgeImages[index], item.key, 20 + index, "generic")
2048
+ }
2049
+ val actionKey = badge.optString("actionKey")
2050
+ badgeView.isClickable = actionKey.isNotEmpty()
2051
+ if (actionKey.isNotEmpty()) {
2052
+ badgeView.setOnClickListener {
2053
+ emitAction(item, actionKey, null, badgeView, "marketBadge", index)
2054
+ }
2055
+ }
2056
+ badgeView.contentDescription = badge.optString("accessibilityLabel", text)
2057
+ titleLine.addView(
2058
+ badgeView,
2059
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2060
+ )
2061
+ }
2062
+ }
2063
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2064
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2065
+ }
2066
+ val subtitleText = item.json.optString("subtitle")
2067
+ val subtitleSegments = item.json.optJSONArray("subtitleSegments")
2068
+ if (subtitleText.isNotEmpty() || (subtitleSegments?.length() ?: 0) > 0) {
2069
+ subtitle.visibility = VISIBLE
2070
+ applyMarketTextStyle(
2071
+ subtitle,
2072
+ style?.optJSONObject("subtitle"),
2073
+ 14f,
2074
+ 20,
2075
+ "regular",
2076
+ color(theme, "secondaryText", "#646464"),
2077
+ "start",
2078
+ )
2079
+ subtitle.text = marketText(
2080
+ subtitleText,
2081
+ subtitleSegments,
2082
+ style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2083
+ )
2084
+ }
2085
+ trailingColumn.orientation = HORIZONTAL
2086
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2087
+ addView(trailingColumn, wrap())
2088
+ bindMarketQuote(item, theme)
2089
+ item.json.optJSONObject("diagnostics")?.optString("imageBindActionKey")
2090
+ ?.takeIf(String::isNotEmpty)
2091
+ ?.takeIf { leading.optJSONObject("image") != null || leading.optJSONObject("networkImage") != null }
2092
+ ?.let { actionKey -> onAction?.invoke(item, actionKey, null, null) }
2093
+ }
2094
+
2095
+ fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {
2096
+ if (boundKey != item.key || item.type != "market") return
2097
+ tag = item
2098
+ val style = item.json.optJSONObject("style")
2099
+ val priceStyle = style?.optJSONObject("price")
2100
+ val price = trailingViews[0]
2101
+ price.isClickable = false
2102
+ price.isLongClickable = false
2103
+ price.visibility = VISIBLE
2104
+ price.maxWidth = dp(112)
2105
+ applyMarketTextStyle(
2106
+ price,
2107
+ priceStyle,
2108
+ 16f,
2109
+ 24,
2110
+ "medium",
2111
+ color(theme, "primaryText", "#202020"),
2112
+ "end",
2113
+ )
2114
+ price.text = marketText(
2115
+ item.json.optString("price"),
2116
+ item.json.optJSONArray("priceSegments"),
2117
+ priceStyle?.optDouble("fontSize", 16.0)?.toFloat() ?: 16f,
2118
+ )
2119
+ val trailingGap = style?.optDouble("trailingGap", 8.0)?.roundToInt() ?: 8
2120
+ price.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
2121
+ marginEnd = dp(trailingGap)
2122
+ }
2123
+
2124
+ val changeData = item.json.getJSONObject("change")
2125
+ val changeStyle = style?.optJSONObject("change")
2126
+ val change = trailingViews[1]
2127
+ change.isClickable = false
2128
+ change.isLongClickable = false
2129
+ val toneColor = when (changeData.optString("tone")) {
2130
+ "positive" -> color(theme, "positive", "#218358")
2131
+ "negative" -> color(theme, "negative", "#CE2C31")
2132
+ else -> color(theme, "secondaryText", "#8D8D8D")
2133
+ }
2134
+ val textColor = safeColor(
2135
+ changeData.optString("textColor"),
2136
+ color(theme, "inverseText", "#FFFFFF"),
2137
+ )
2138
+ change.visibility = VISIBLE
2139
+ applyMarketTextStyle(change, changeStyle, 14f, 20, "medium", textColor, "center")
2140
+ change.text = marketText(
2141
+ changeData.optString("text"),
2142
+ changeData.optJSONArray("textSegments"),
2143
+ changeStyle?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2144
+ )
2145
+ change.background = roundedFill(
2146
+ safeColor(changeData.optString("backgroundColor"), toneColor),
2147
+ style?.optDouble("changeCornerRadius", 8.0)?.toFloat() ?: 8f,
2148
+ )
2149
+ change.layoutParams = LayoutParams(
2150
+ dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
2151
+ dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
2152
+ )
2153
+ contentDescription = item.json.optString(
2154
+ "accessibilityLabel",
2155
+ listOf(
2156
+ item.json.optString("title"),
2157
+ item.json.optString("subtitle"),
2158
+ item.json.optString("price"),
2159
+ changeData.optString("text"),
2160
+ ).filter(String::isNotEmpty).joinToString(", "),
2161
+ )
2162
+ }
2163
+
1736
2164
  private fun styledDataText(
1737
2165
  column: JSONObject,
1738
2166
  badges: JSONArray?,
@@ -2367,6 +2795,40 @@ internal class NativeListRowView(
2367
2795
 
2368
2796
  private fun bindSystem(item: NativeListItem, theme: JSONObject?) {
2369
2797
  val variant = item.json.optString("variant")
2798
+ val isMarket = item.json.optString("presentation") == "market"
2799
+ if (isMarket) setPadding(dp(20), dp(12), dp(20), dp(12))
2800
+ if (variant == "loading" && item.json.optString("loadingStyle") == "skeleton") {
2801
+ setPadding(dp(20), dp(12), dp(20), dp(12))
2802
+ addView(
2803
+ NativeListMarketSkeleton(context, color(theme, "background", "#FFFFFF")),
2804
+ LayoutParams(LayoutParams.MATCH_PARENT, dp(32)),
2805
+ )
2806
+ return
2807
+ }
2808
+ if (variant == "loading" && item.json.optString("loadingStyle") == "spinner") {
2809
+ gravity = Gravity.CENTER
2810
+ setPadding(0, dp(16), 0, dp(16))
2811
+ addView(
2812
+ ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
2813
+ isIndeterminate = true
2814
+ indeterminateTintList = android.content.res.ColorStateList.valueOf(color(theme, "icon", "#0000009B"))
2815
+ },
2816
+ LayoutParams(dp(20), dp(20)),
2817
+ )
2818
+ return
2819
+ }
2820
+ if (isMarket && variant == "noMatch") {
2821
+ gravity = Gravity.CENTER
2822
+ setPadding(dp(32), dp(32), dp(32), dp(32))
2823
+ title.textSize = sp(16f)
2824
+ title.typeface = NativeListFonts.regular(context)
2825
+ title.gravity = Gravity.CENTER
2826
+ title.setTextColor(color(theme, "secondaryText", "#0000009B"))
2827
+ TextViewCompat.setLineHeight(title, dp(24))
2828
+ showText(title, item.json.optString("message"), 1)
2829
+ addView(mainColumn, weighted())
2830
+ return
2831
+ }
2370
2832
  // OneKey patch: warning title/description wrap inside the actual scroll content.
2371
2833
  if (variant == "warning") {
2372
2834
  setPadding(dp(12), dp(14), dp(12), dp(14))
@@ -2400,11 +2862,11 @@ internal class NativeListRowView(
2400
2862
  }
2401
2863
  gravity = Gravity.CENTER
2402
2864
  if (variant == "loading") {
2403
- addLeading(JSONObject().put("kind", "skeleton"), 40)
2865
+ addLeading(JSONObject().put("kind", "skeleton"), if (isMarket) 32 else 40)
2404
2866
  leadingFallback.text = ""
2405
2867
  leadingFallback.background = roundedFill(
2406
2868
  color(theme, "strongBackground", "#0000000F"),
2407
- 20f,
2869
+ if (isMarket) 16f else 20f,
2408
2870
  )
2409
2871
  addSkeleton(skeletonPrimary, 120, 12, theme, bottomMarginDp = 8)
2410
2872
  addSkeleton(skeletonSecondary, 80, 12, theme)
@@ -2445,15 +2907,17 @@ internal class NativeListRowView(
2445
2907
  sizeDp: Int = 40,
2446
2908
  secondaryVisual: JSONObject? = null,
2447
2909
  spacingDp: Int = 12,
2910
+ heightDp: Int = sizeDp,
2911
+ cornerRadiusDp: Float? = null,
2448
2912
  ) {
2449
2913
  leadingFrame.visibility = VISIBLE
2450
- leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)).apply {
2914
+ leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(heightDp)).apply {
2451
2915
  val item = tag as? NativeListItem
2452
2916
  // OneKey patch: Yoga rounds cumulative selector edges, not each 12dp gap separately.
2453
2917
  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
2918
  }
2455
2919
  addView(leadingFrame)
2456
- leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
2920
+ leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
2457
2921
  if (visual == null) return
2458
2922
  val kind = visual.optString("kind")
2459
2923
  val shape = visual.optString(
@@ -2474,16 +2938,19 @@ internal class NativeListRowView(
2474
2938
  if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
2475
2939
  leadingFrame.background = roundedFill(
2476
2940
  visualBackground,
2477
- leadingCornerRadius(shape, sizeDp),
2941
+ cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
2478
2942
  )
2479
2943
  }
2480
- leadingFallback.background = roundedFill(visualBackground, leadingCornerRadius(shape, sizeDp))
2944
+ leadingFallback.background = roundedFill(
2945
+ visualBackground,
2946
+ cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
2947
+ )
2481
2948
  leadingFallback.visibility = if (!isIcon && sources.isEmpty()) VISIBLE else GONE
2482
2949
  if (isIcon) {
2483
2950
  leadingFrame.background = GradientDrawable().apply {
2484
2951
  setColor(visualBackground)
2485
2952
  setStroke(1, parseNativeListColor("#0000001F"))
2486
- cornerRadius = scaledDp(leadingCornerRadius(shape, sizeDp))
2953
+ cornerRadius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
2487
2954
  }
2488
2955
  leadingIcon.iconName = visual.optString("name")
2489
2956
  leadingIcon.tintColor = safeColor(
@@ -2541,13 +3008,33 @@ internal class NativeListRowView(
2541
3008
  index = index,
2542
3009
  count = visibleSources.size,
2543
3010
  sizeDp = sizeDp,
3011
+ heightDp = heightDp,
2544
3012
  tokenPair = tokenPair,
2545
3013
  )
2546
3014
  image.outlineProvider = when {
2547
3015
  tokenPair && index == 1 -> circleOutlineProvider
3016
+ cornerRadiusDp != null -> roundedOutlineProvider(cornerRadiusDp)
2548
3017
  else -> leadingOutlineProvider(shape)
2549
3018
  }
2550
3019
  image.clipToOutline = true
3020
+ if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3021
+ val inset = dp(1)
3022
+ val radius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
3023
+ leadingFrame.background = GradientDrawable().apply {
3024
+ setColor(visualBackground)
3025
+ setStroke(inset, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3026
+ this.cornerRadius = radius
3027
+ }
3028
+ image.layoutParams = FrameLayout.LayoutParams(dp(sizeDp) - inset * 2, dp(heightDp) - inset * 2).apply {
3029
+ leftMargin = inset
3030
+ topMargin = inset
3031
+ }
3032
+ image.outlineProvider = object : ViewOutlineProvider() {
3033
+ override fun getOutline(view: View, outline: Outline) {
3034
+ outline.setRoundRect(-inset, -inset, view.width + inset, view.height + inset, radius)
3035
+ }
3036
+ }
3037
+ }
2551
3038
  val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
2552
3039
  val expectedEpoch = bindingEpoch
2553
3040
  bindImage(source, image, boundKey ?: "", index, variant,
@@ -2664,10 +3151,11 @@ internal class NativeListRowView(
2664
3151
  index: Int,
2665
3152
  count: Int,
2666
3153
  sizeDp: Int,
3154
+ heightDp: Int,
2667
3155
  tokenPair: Boolean,
2668
3156
  ): FrameLayout.LayoutParams {
2669
3157
  if (count == 1 || tokenPair && index == 0) {
2670
- return FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
3158
+ return FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
2671
3159
  }
2672
3160
  if (tokenPair && index == 1) {
2673
3161
  return FrameLayout.LayoutParams(dp(16), dp(16), Gravity.END or Gravity.BOTTOM).apply {
@@ -3009,6 +3497,22 @@ internal class NativeListRowView(
3009
3497
  }
3010
3498
 
3011
3499
  private fun applySize(item: NativeListItem) {
3500
+ if (item.type == "market") {
3501
+ val style = item.json.optJSONObject("style")
3502
+ val imageHeight = style?.optJSONObject("image")?.optDouble(
3503
+ "height",
3504
+ if (item.json.optString("variant") == "stock") 40.0 else 32.0,
3505
+ ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3506
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3507
+ val defaultHeight = if (item.json.optString("variant") == "stock") 72.0 else 68.0
3508
+ minimumHeight = dp(
3509
+ item.json.optDouble(
3510
+ "height",
3511
+ maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3512
+ ).roundToInt(),
3513
+ )
3514
+ return
3515
+ }
3012
3516
  val isWalletSidebar =
3013
3517
  item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
3014
3518
  val isAccountSelectorIdentity =
@@ -3161,9 +3665,14 @@ internal class NativeListRowView(
3161
3665
  else -> 36
3162
3666
  }
3163
3667
  "system" -> when (item.json.optString("variant")) {
3668
+ "loading" -> when (item.json.optString("loadingStyle")) {
3669
+ "skeleton" -> 56
3670
+ "spinner" -> 52
3671
+ else -> if (item.json.optString("presentation") == "market") 68 else 56
3672
+ }
3673
+ "noMatch", "retry" -> if (item.json.optString("presentation") == "market") 44 else if (item.json.optString("variant") == "noMatch") 36 else 44
3164
3674
  "warning" -> 0
3165
- "noMatch", "end" -> 36
3166
- "retry" -> 44
3675
+ "end" -> if (item.json.optString("presentation") == "market") 44 else 36
3167
3676
  else -> 56
3168
3677
  }
3169
3678
  "action" -> when {
@@ -3269,6 +3778,12 @@ internal class NativeListRowView(
3269
3778
  }
3270
3779
  }
3271
3780
 
3781
+ private fun roundedOutlineProvider(radiusDp: Float) = object : ViewOutlineProvider() {
3782
+ override fun getOutline(view: View, outline: Outline) {
3783
+ outline.setRoundRect(0, 0, view.width, view.height, scaledDp(radiusDp))
3784
+ }
3785
+ }
3786
+
3272
3787
  private fun JSONArray?.hasAccessory(kind: String): Boolean {
3273
3788
  if (this == null) return false
3274
3789
  return (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
@@ -3465,6 +3980,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3465
3980
  "CrossedSmallSolid" to listOf(Path.FillType.WINDING),
3466
3981
  "AccountErrorCustom" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3467
3982
  "Circle" to listOf(Path.FillType.WINDING),
3983
+ "BadgeVerifiedSolid" to listOf(Path.FillType.EVEN_ODD),
3468
3984
  "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
3469
3985
  "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3470
3986
  "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
@@ -3492,6 +4008,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3492
4008
  "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
4009
  "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
4010
  "Circle" to listOf("M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0"),
4011
+ "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
4012
  "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
3496
4013
  "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
3497
4014
  "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),