@onekeyfe/react-native-native-list 3.0.112 → 3.0.113

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(
@@ -2535,13 +3002,33 @@ internal class NativeListRowView(
2535
3002
  index = index,
2536
3003
  count = visibleSources.size,
2537
3004
  sizeDp = sizeDp,
3005
+ heightDp = heightDp,
2538
3006
  tokenPair = tokenPair,
2539
3007
  )
2540
3008
  image.outlineProvider = when {
2541
3009
  tokenPair && index == 1 -> circleOutlineProvider
3010
+ cornerRadiusDp != null -> roundedOutlineProvider(cornerRadiusDp)
2542
3011
  else -> leadingOutlineProvider(shape)
2543
3012
  }
2544
3013
  image.clipToOutline = true
3014
+ if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3015
+ val inset = dp(1)
3016
+ val radius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
3017
+ leadingFrame.background = GradientDrawable().apply {
3018
+ setColor(visualBackground)
3019
+ setStroke(inset, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3020
+ this.cornerRadius = radius
3021
+ }
3022
+ image.layoutParams = FrameLayout.LayoutParams(dp(sizeDp) - inset * 2, dp(heightDp) - inset * 2).apply {
3023
+ leftMargin = inset
3024
+ topMargin = inset
3025
+ }
3026
+ image.outlineProvider = object : ViewOutlineProvider() {
3027
+ override fun getOutline(view: View, outline: Outline) {
3028
+ outline.setRoundRect(-inset, -inset, view.width + inset, view.height + inset, radius)
3029
+ }
3030
+ }
3031
+ }
2545
3032
  val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
2546
3033
  val expectedEpoch = bindingEpoch
2547
3034
  bindImage(source, image, boundKey ?: "", index, variant,
@@ -2658,10 +3145,11 @@ internal class NativeListRowView(
2658
3145
  index: Int,
2659
3146
  count: Int,
2660
3147
  sizeDp: Int,
3148
+ heightDp: Int,
2661
3149
  tokenPair: Boolean,
2662
3150
  ): FrameLayout.LayoutParams {
2663
3151
  if (count == 1 || tokenPair && index == 0) {
2664
- return FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
3152
+ return FrameLayout.LayoutParams(dp(sizeDp), dp(heightDp))
2665
3153
  }
2666
3154
  if (tokenPair && index == 1) {
2667
3155
  return FrameLayout.LayoutParams(dp(16), dp(16), Gravity.END or Gravity.BOTTOM).apply {
@@ -3003,6 +3491,22 @@ internal class NativeListRowView(
3003
3491
  }
3004
3492
 
3005
3493
  private fun applySize(item: NativeListItem) {
3494
+ if (item.type == "market") {
3495
+ val style = item.json.optJSONObject("style")
3496
+ val imageHeight = style?.optJSONObject("image")?.optDouble(
3497
+ "height",
3498
+ if (item.json.optString("variant") == "stock") 40.0 else 32.0,
3499
+ ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3500
+ val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3501
+ val defaultHeight = if (item.json.optString("variant") == "stock") 72.0 else 68.0
3502
+ minimumHeight = dp(
3503
+ item.json.optDouble(
3504
+ "height",
3505
+ maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3506
+ ).roundToInt(),
3507
+ )
3508
+ return
3509
+ }
3006
3510
  val isWalletSidebar =
3007
3511
  item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
3008
3512
  val isAccountSelectorIdentity =
@@ -3155,9 +3659,14 @@ internal class NativeListRowView(
3155
3659
  else -> 36
3156
3660
  }
3157
3661
  "system" -> when (item.json.optString("variant")) {
3662
+ "loading" -> when (item.json.optString("loadingStyle")) {
3663
+ "skeleton" -> 56
3664
+ "spinner" -> 52
3665
+ else -> if (item.json.optString("presentation") == "market") 68 else 56
3666
+ }
3667
+ "noMatch", "retry" -> if (item.json.optString("presentation") == "market") 44 else if (item.json.optString("variant") == "noMatch") 36 else 44
3158
3668
  "warning" -> 0
3159
- "noMatch", "end" -> 36
3160
- "retry" -> 44
3669
+ "end" -> if (item.json.optString("presentation") == "market") 44 else 36
3161
3670
  else -> 56
3162
3671
  }
3163
3672
  "action" -> when {
@@ -3263,6 +3772,12 @@ internal class NativeListRowView(
3263
3772
  }
3264
3773
  }
3265
3774
 
3775
+ private fun roundedOutlineProvider(radiusDp: Float) = object : ViewOutlineProvider() {
3776
+ override fun getOutline(view: View, outline: Outline) {
3777
+ outline.setRoundRect(0, 0, view.width, view.height, scaledDp(radiusDp))
3778
+ }
3779
+ }
3780
+
3266
3781
  private fun JSONArray?.hasAccessory(kind: String): Boolean {
3267
3782
  if (this == null) return false
3268
3783
  return (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
@@ -3459,6 +3974,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3459
3974
  "CrossedSmallSolid" to listOf(Path.FillType.WINDING),
3460
3975
  "AccountErrorCustom" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3461
3976
  "Circle" to listOf(Path.FillType.WINDING),
3977
+ "BadgeVerifiedSolid" to listOf(Path.FillType.EVEN_ODD),
3462
3978
  "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
3463
3979
  "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
3464
3980
  "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
@@ -3486,6 +4002,7 @@ private class OneKeyIconView(context: android.content.Context) : View(context) {
3486
4002
  "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"),
3487
4003
  "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"),
3488
4004
  "Circle" to listOf("M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0"),
4005
+ "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"),
3489
4006
  "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
3490
4007
  "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
3491
4008
  "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),