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

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.
@@ -10,6 +10,8 @@ import com.facebook.react.uimanager.ThemedReactContext
10
10
  import org.json.JSONObject
11
11
  import java.util.Collections
12
12
  import java.util.WeakHashMap
13
+ import kotlin.math.ceil
14
+ import kotlin.math.roundToInt
13
15
 
14
16
  internal class NativeListAdapter(
15
17
  private val context: ThemedReactContext,
@@ -39,6 +41,8 @@ internal class NativeListAdapter(
39
41
  )
40
42
  val currentList: List<NativeListItem>
41
43
  get() = differ.currentList
44
+ private var marketSourceItems: List<NativeListItem>? = null
45
+ private var marketSourceEdges = DoubleArray(0)
42
46
  private val createdRows = Collections.newSetFromMap(
43
47
  WeakHashMap<NativeListRowView, Boolean>(),
44
48
  )
@@ -122,6 +126,30 @@ internal class NativeListAdapter(
122
126
  fun itemAt(position: Int): NativeListItem? =
123
127
  reorderItems?.getOrNull(position) ?: differ.currentList.getOrNull(position)
124
128
 
129
+ // OneKey patch: Yoga rounds badge dimensions from unrounded cumulative row edges.
130
+ fun marketSourceEdgePx(position: Int): Double {
131
+ val items = reorderItems ?: differ.currentList
132
+ if (marketSourceItems !== items) {
133
+ val density = context.resources.displayMetrics.density.toDouble()
134
+ val edges = DoubleArray(items.size + 1)
135
+ items.forEachIndexed { index, item ->
136
+ val style = item.json.optJSONObject("style")
137
+ val height = item.json.optDouble("height", 0.0)
138
+ val sourceHeight = if (item.type == "market" && style != null) {
139
+ val titleHeight = ceil((style.optJSONObject("title")?.optDouble("lineHeight", 24.0) ?: 24.0) * density) / density
140
+ val subtitleHeight = if (item.json.optString("subtitle").isNotEmpty() || item.json.optJSONObject("subtitlePrefix") != null) {
141
+ ceil((style.optJSONObject("subtitle")?.optDouble("lineHeight", 20.0) ?: 20.0) * density) / density + style.optDouble("lineGap", 0.0)
142
+ } else 0.0
143
+ maxOf(height.roundToInt().toDouble(), titleHeight + subtitleHeight + style.optDouble("verticalPadding", 12.0) * 2).toFloat().toDouble()
144
+ } else height
145
+ edges[index + 1] = edges[index] + sourceHeight * density
146
+ }
147
+ marketSourceItems = items
148
+ marketSourceEdges = edges
149
+ }
150
+ return marketSourceEdges.getOrElse(position) { 0.0 }
151
+ }
152
+
125
153
  fun positionOfKey(key: String): Int =
126
154
  (reorderItems ?: differ.currentList).indexOfFirst { it.key == key }
127
155
 
@@ -169,6 +197,8 @@ internal class NativeListAdapter(
169
197
  }
170
198
 
171
199
  fun dispose() {
200
+ marketSourceItems = null
201
+ marketSourceEdges = DoubleArray(0)
172
202
  reorderItems = null
173
203
  suppressDifferUpdates = false
174
204
  createdRows.forEach(NativeListRowView::dispose)
@@ -45,6 +45,7 @@ import com.facebook.react.uimanager.style.LogicalEdge
45
45
  import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
46
46
  import androidx.core.graphics.PathParser
47
47
  import androidx.core.widget.TextViewCompat
48
+ import androidx.recyclerview.widget.RecyclerView
48
49
  import org.json.JSONArray
49
50
  import org.json.JSONObject
50
51
  import kotlin.math.roundToInt
@@ -119,6 +120,7 @@ internal data class NativeListActionOrigin(
119
120
  val source: String,
120
121
  val slot: Int? = null,
121
122
  val anchorInsetPixels: Int = 0,
123
+ val windowPointPixels: android.graphics.PointF? = null,
122
124
  )
123
125
 
124
126
  /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
@@ -209,6 +211,8 @@ private class DottedUnderlineTextView(context: android.content.Context) : TextVi
209
211
 
210
212
  private class PackedTitleLineLayout(context: android.content.Context) : LinearLayout(context) {
211
213
  var packsChildrenAtStart = false
214
+ // OneKey patch: optional cap for the Market subtitle's localized name.
215
+ var leadingTextMaxWidth = Int.MAX_VALUE
212
216
 
213
217
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
214
218
  if (!packsChildrenAtStart || childCount < 2) {
@@ -236,7 +240,7 @@ private class PackedTitleLineLayout(context: android.content.Context) : LinearLa
236
240
  val titleMargins = title.layoutParams as MarginLayoutParams
237
241
  if (widthMode != MeasureSpec.UNSPECIFIED) {
238
242
  (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - accessoryWidth -
239
- titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0)
243
+ titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0).coerceAtMost(leadingTextMaxWidth)
240
244
  }
241
245
  (title.layoutParams as LayoutParams).apply {
242
246
  width = LayoutParams.WRAP_CONTENT
@@ -393,7 +397,16 @@ private class NativeListTableColumnView(context: android.content.Context) : Line
393
397
  internal class NativeListRowView(
394
398
  private val reactContext: ThemedReactContext,
395
399
  ) : LinearLayout(reactContext) {
396
- 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
+ }
397
410
  // OneKey patch: reset selector fragments and corner decorations on every bind.
398
411
  private val selectorViews = mutableListOf<View>()
399
412
  private var selectorUsesSourceScale = false
@@ -405,6 +418,7 @@ internal class NativeListRowView(
405
418
  }
406
419
  private val selectorOriginalFontFeatures = mutableMapOf<TextView, String?>()
407
420
  private val selectorOriginalPaintFlags = mutableMapOf<TextView, Int>()
421
+ private val marketOriginalPaintFlags = mutableMapOf<TextView, Int>()
408
422
  private val selectorLineHeights = mutableMapOf<TextView, Int>()
409
423
  private val selectorFontSizes = mutableMapOf<TextView, Float>()
410
424
  private val selectorImages = mutableListOf<OneKeyImageReusableView>()
@@ -427,6 +441,8 @@ internal class NativeListRowView(
427
441
  private val titleLine = PackedTitleLineLayout(context)
428
442
  private val title = DottedUnderlineTextView(context)
429
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)
430
446
  private val tertiary = TextView(context)
431
447
  private val status = TextView(context)
432
448
  private val metricSubtitle = TextView(context)
@@ -479,6 +495,8 @@ internal class NativeListRowView(
479
495
  private var marketLongPressRunnable: Runnable? = null
480
496
  private var marketTouchStartX = 0f
481
497
  private var marketTouchStartY = 0f
498
+ private var marketTouchX = 0f
499
+ private var marketTouchY = 0f
482
500
  private var marketLongPressFired = false
483
501
  private var reorderActive = false
484
502
  private var checkboxCheckedColor = Color.rgb(32, 32, 32)
@@ -610,6 +628,8 @@ internal class NativeListRowView(
610
628
  if (item?.type == "market") {
611
629
  marketTouchStartX = event.x
612
630
  marketTouchStartY = event.y
631
+ marketTouchX = event.x
632
+ marketTouchY = event.y
613
633
  item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
614
634
  emitAction(item, actionKey, null, this, "row")
615
635
  }
@@ -620,7 +640,11 @@ internal class NativeListRowView(
620
640
  if (touchPressed && current?.key == expectedKey && current.type == "market") {
621
641
  marketLongPressRunnable = null
622
642
  marketLongPressFired = true
623
- emitAction(current, actionKey, null, this, "row")
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
+ ))
624
648
  }
625
649
  }
626
650
  marketLongPressRunnable = runnable
@@ -634,6 +658,8 @@ internal class NativeListRowView(
634
658
  }
635
659
  }
636
660
  MotionEvent.ACTION_MOVE -> {
661
+ marketTouchX = event.x
662
+ marketTouchY = event.y
637
663
  val outside = event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
638
664
  val movedMarket = (tag as? NativeListItem)?.type == "market" &&
639
665
  (kotlin.math.abs(event.x - marketTouchStartX) > dp(10) ||
@@ -713,6 +739,22 @@ internal class NativeListRowView(
713
739
  }
714
740
 
715
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
+ }
716
758
  if (isMediaTile) {
717
759
  val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
718
760
  .coerceAtLeast(0)
@@ -737,6 +779,63 @@ internal class NativeListRowView(
737
779
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
738
780
  super.onLayout(changed, left, top, right, bottom)
739
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
+ }
740
839
  val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
741
840
  if (item.type == "identity" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && accessory?.optString("kind") == "icon" && accessory.optString("name") == "PlusSmallOutline") {
742
841
  // OneKey patch: the borderless Plus retains the source's fixed top18/negative7 slot.
@@ -785,7 +884,7 @@ internal class NativeListRowView(
785
884
  touchPressed = shouldRestorePressed
786
885
  currentLayout = layout
787
886
  tag = item
788
- selectorUsesSourceScale = item.type == "market" || item.usesSelectorSourceScale || useSourceScale
887
+ selectorUsesSourceScale = item.type == "market" || item.type == "system" && item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry" || item.usesSelectorSourceScale || useSourceScale
789
888
  reorderActive = false
790
889
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
791
890
  secondaryImage.prepareForReuse()
@@ -1079,6 +1178,8 @@ internal class NativeListRowView(
1079
1178
  selectorOriginalFontFeatures.clear()
1080
1179
  selectorOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1081
1180
  selectorOriginalPaintFlags.clear()
1181
+ marketOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1182
+ marketOriginalPaintFlags.clear()
1082
1183
  selectorLineHeights.clear()
1083
1184
  selectorFontSizes.clear()
1084
1185
  // OneKey patch: selector-only views cannot survive a recycled binding.
@@ -1095,7 +1196,17 @@ internal class NativeListRowView(
1095
1196
  badge.setOnClickListener(null)
1096
1197
  badge.contentDescription = null
1097
1198
  marketBadgeLabels[index].apply {
1199
+ // OneKey patch: restore every optional Market badge typography property on reuse.
1098
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
1099
1210
  setTextColor(color(null, "secondaryText", "#0000009B"))
1100
1211
  }
1101
1212
  marketBadgeImages[index].prepareForReuse()
@@ -1140,6 +1251,9 @@ internal class NativeListRowView(
1140
1251
  boundCheckboxData = null
1141
1252
  mainColumn.orientation = VERTICAL
1142
1253
  mainColumn.gravity = Gravity.CENTER_VERTICAL
1254
+ // OneKey patch: remove the Market group before restoring shared labels.
1255
+ marketSubtitleLine.removeAllViews()
1256
+ mainColumn.removeView(marketSubtitleLine)
1143
1257
  mediaMetadataRow.removeView(subtitle)
1144
1258
  mediaMetadataRow.removeView(mediaNetworkImage)
1145
1259
  mainColumn.removeView(mediaMetadataRow)
@@ -1165,6 +1279,10 @@ internal class NativeListRowView(
1165
1279
  badgeLine.text = ""
1166
1280
  title.setLineSpacing(0f, 1f)
1167
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)
1168
1286
  title.letterSpacing = 0f
1169
1287
  title.showsDottedUnderline = false
1170
1288
  title.setPadding(0, 0, 0, 0)
@@ -1218,6 +1336,7 @@ internal class NativeListRowView(
1218
1336
  spinner.visibility = GONE
1219
1337
  spinner.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
1220
1338
  spinner.alpha = 1f
1339
+ marketLeadingUsesSourceClip = false
1221
1340
  leadingFrame.visibility = GONE
1222
1341
  leadingFrame.background = null
1223
1342
  leadingFrame.clipChildren = false
@@ -1248,6 +1367,8 @@ internal class NativeListRowView(
1248
1367
  leadingActionIcon.iconName = ""
1249
1368
  leadingActionIcon.glyphSizeDp = 24
1250
1369
  leadingActionIcon.setOnClickListener(null)
1370
+ leadingActionIcon.setTag(com.facebook.react.R.id.react_test_id, null)
1371
+ leadingActionIcon.contentDescription = null
1251
1372
  mainColumn.visibility = VISIBLE
1252
1373
  dataContainer.visibility = GONE
1253
1374
  dataColumns.forEach { it.visibility = GONE }
@@ -1932,6 +2053,25 @@ internal class NativeListRowView(
1932
2053
  )
1933
2054
  }
1934
2055
 
2056
+ // OneKey patch: opt-in Market text uses the same pixel rounding and line box as RN.
2057
+ private fun applyMarketTextMetrics(view: TextView, style: JSONObject?) {
2058
+ if (style == null || view.visibility != VISIBLE || view.text.isEmpty()) return
2059
+ marketOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
2060
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
2061
+ if (style.has("fontSize")) {
2062
+ val sourceSize = sp(style.optDouble("fontSize").toFloat()) * resources.displayMetrics.density
2063
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
2064
+ }
2065
+ val text = SpannableStringBuilder(view.text)
2066
+ text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
2067
+ if (style.has("lineHeight")) {
2068
+ val lineHeight = kotlin.math.ceil(style.optDouble("lineHeight") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toInt()
2069
+ text.setSpan(SelectorLineHeightSpan(lineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2070
+ view.setLineSpacing(0f, 1f)
2071
+ }
2072
+ view.text = text
2073
+ }
2074
+
1935
2075
  private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
1936
2076
  if (segments == null || segments.length() == 0) return value
1937
2077
  val result = SpannableStringBuilder()
@@ -1968,6 +2108,33 @@ internal class NativeListRowView(
1968
2108
  ?: if (variant == "perp") 8 else 14
1969
2109
  setPadding(dp(horizontalPadding), dp(verticalPadding), dp(horizontalPadding), dp(verticalPadding))
1970
2110
 
2111
+ item.json.optJSONObject("leadingAction")?.let { action ->
2112
+ leadingActionIcon.visibility = VISIBLE
2113
+ leadingActionIcon.iconName = action.optString("name")
2114
+ leadingActionIcon.glyphSizeDp = 24
2115
+ leadingActionIcon.tintColor = safeColor(
2116
+ action.optString("tintColor"),
2117
+ color(theme, "icon", "#0000009B"),
2118
+ )
2119
+ leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
2120
+ leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
2121
+ leadingActionIcon.setTag(
2122
+ com.facebook.react.R.id.react_test_id,
2123
+ action.optString("testID").takeIf(String::isNotEmpty),
2124
+ )
2125
+ leadingActionIcon.contentDescription = action.optString("accessibilityLabel")
2126
+ leadingActionIcon.setOnClickListener {
2127
+ emitAction(
2128
+ item,
2129
+ action.optString("actionKey"),
2130
+ null,
2131
+ leadingActionIcon,
2132
+ "leadingAction",
2133
+ )
2134
+ }
2135
+ addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
2136
+ }
2137
+
1971
2138
  val leading = JSONObject(item.json.getJSONObject("leading").toString())
1972
2139
  imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
1973
2140
  imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
@@ -1989,7 +2156,11 @@ internal class NativeListRowView(
1989
2156
  cornerRadiusDp = cornerRadius,
1990
2157
  )
1991
2158
 
1992
- addView(mainColumn, weighted())
2159
+ // OneKey patch: retain the source gap without changing other templates.
2160
+ // addView(mainColumn, weighted())
2161
+ addView(mainColumn, weighted().apply {
2162
+ marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
2163
+ })
1993
2164
  titleLine.packsChildrenAtStart = true
1994
2165
  showText(title, item.json.optString("title"), style?.optJSONObject("title")?.optInt("lines", 1) ?: 1)
1995
2166
  applyMarketTextStyle(
@@ -2008,6 +2179,7 @@ internal class NativeListRowView(
2008
2179
  val badge = badges.getJSONObject(index)
2009
2180
  val badgeView = marketBadgeViews[index]
2010
2181
  val badgeLabel = marketBadgeLabels[index]
2182
+ val badgeStyle = badge.optJSONObject("style")
2011
2183
  val hasGlyph = badge.optString("iconName") == "verified"
2012
2184
  val remoteIcon = badge.optJSONObject("icon")
2013
2185
  val hasIcon = hasGlyph || remoteIcon != null
@@ -2022,15 +2194,28 @@ internal class NativeListRowView(
2022
2194
  val foreground = safeColor(badge.optString("textColor"), toneColor)
2023
2195
  badgeView.visibility = VISIBLE
2024
2196
  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)
2197
+ // OneKey patch: Market callers can request the original badge padding.
2198
+ val padding = badgeStyle?.optDouble("horizontalPadding", 5.0)?.roundToInt() ?: 5
2199
+ val leftPadding = if (badgeStyle?.has("horizontalPadding") == true) padding else if (hasIcon) 2 else 5
2200
+ // badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2201
+ badgeView.setPadding(dp(if (iconOnly) 0 else leftPadding), 0, dp(if (iconOnly) 0 else padding), 0)
2026
2202
  badgeView.background = roundedFill(
2027
2203
  safeColor(
2028
2204
  badge.optString("backgroundColor"),
2029
2205
  if (hasIcon && text.isEmpty()) Color.TRANSPARENT else color(theme, "strongBackground", "#0000000F"),
2030
2206
  ),
2031
2207
  4f,
2032
- )
2208
+ ).apply {
2209
+ if (badgeStyle != null) this.cornerRadius = 4f * resources.displayMetrics.density
2210
+ }
2033
2211
  badgeLabel.text = text
2212
+ // OneKey patch: match SizableText's tabular numerals only for explicit Market metrics.
2213
+ badgeLabel.fontFeatureSettings = if (badgeStyle != null) "tnum" else null
2214
+ badgeLabel.textSize = sp(badgeStyle?.optDouble("fontSize", 11.0)?.toFloat() ?: 11f)
2215
+ badgeLabel.typeface = marketTypeface(badgeStyle?.optString("fontWeight").orEmpty(), "medium")
2216
+ if (badgeStyle?.has("lineHeight") == true) {
2217
+ TextViewCompat.setLineHeight(badgeLabel, dp(badgeStyle.optDouble("lineHeight").roundToInt()))
2218
+ }
2034
2219
  badgeLabel.setTextColor(foreground)
2035
2220
  badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
2036
2221
  if (hasGlyph) {
@@ -2056,7 +2241,9 @@ internal class NativeListRowView(
2056
2241
  badgeView.contentDescription = badge.optString("accessibilityLabel", text)
2057
2242
  titleLine.addView(
2058
2243
  badgeView,
2059
- LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2244
+ // OneKey patch: preserve 18dp unless the Market row opts into source metrics.
2245
+ // LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2246
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeStyle?.optDouble("height", 18.0)?.roundToInt() ?: 18)).apply { marginStart = dp(titleBadgeGap) },
2060
2247
  )
2061
2248
  }
2062
2249
  }
@@ -2082,6 +2269,31 @@ internal class NativeListRowView(
2082
2269
  style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2083
2270
  )
2084
2271
  }
2272
+ // OneKey patch: preserve independent name metrics, truncation, and volume.
2273
+ val subtitlePrefix = item.json.optJSONObject("subtitlePrefix")
2274
+ val subtitlePadding = style?.optDouble("subtitleTrailingPadding", 0.0)?.roundToInt() ?: 0
2275
+ if (subtitlePrefix != null || subtitlePadding > 0) {
2276
+ mainColumn.removeView(subtitle)
2277
+ mainColumn.removeView(tertiary)
2278
+ marketSubtitleLine.orientation = HORIZONTAL
2279
+ marketSubtitleLine.gravity = Gravity.CENTER_VERTICAL
2280
+ marketSubtitleLine.packsChildrenAtStart = true
2281
+ marketSubtitleLine.leadingTextMaxWidth = subtitlePrefix?.takeIf { it.has("maxWidth") }
2282
+ ?.optDouble("maxWidth")?.roundToInt()?.let(::dp) ?: Int.MAX_VALUE
2283
+ marketSubtitleLine.setPadding(0, 0, dp(subtitlePadding), 0)
2284
+ showText(tertiary, subtitlePrefix?.optString("text") ?: "", 1)
2285
+ applyMarketTextStyle(tertiary, subtitlePrefix?.optJSONObject("style"), 12f, 16, "regular", color(theme, "secondaryText", "#646464"), "start")
2286
+ marketSubtitleLine.addView(tertiary, wrap())
2287
+ marketSubtitleLine.addView(subtitle, wrap().apply {
2288
+ if (tertiary.visibility == VISIBLE && subtitle.visibility == VISIBLE) {
2289
+ marginStart = dp(subtitlePrefix?.optDouble("gap", 4.0)?.roundToInt() ?: 4)
2290
+ }
2291
+ })
2292
+ mainColumn.addView(marketSubtitleLine, 1, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2293
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2294
+ })
2295
+ marketSubtitleLine.visibility = if (tertiary.visibility == VISIBLE || subtitle.visibility == VISIBLE) VISIBLE else GONE
2296
+ }
2085
2297
  trailingColumn.orientation = HORIZONTAL
2086
2298
  trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2087
2299
  addView(trailingColumn, wrap())
@@ -2150,6 +2362,24 @@ internal class NativeListRowView(
2150
2362
  dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
2151
2363
  dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
2152
2364
  )
2365
+ applyMarketTextMetrics(title, style?.optJSONObject("title"))
2366
+ applyMarketTextMetrics(subtitle, style?.optJSONObject("subtitle"))
2367
+ applyMarketTextMetrics(tertiary, item.json.optJSONObject("subtitlePrefix")?.optJSONObject("style"))
2368
+ applyMarketTextMetrics(price, priceStyle)
2369
+ applyMarketTextMetrics(change, changeStyle)
2370
+ val badges = item.json.optJSONArray("badges")
2371
+ marketBadgeLabels.forEachIndexed { index, label ->
2372
+ val badge = badges?.optJSONObject(index)
2373
+ val badgeStyle = badge?.optJSONObject("style")
2374
+ applyMarketTextMetrics(label, badgeStyle)
2375
+ if (badge != null && label.visibility == VISIBLE && badgeStyle?.has("horizontalPadding") == true &&
2376
+ badge.optJSONObject("icon") == null && badge.optString("iconName").isEmpty()) {
2377
+ // OneKey patch: match RN's intrinsic text width and one rounded padding pair.
2378
+ label.layoutParams = wrap().apply { width = label.paint.measureText(label.text.toString()).roundToInt() }
2379
+ val padding = badgeStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
2380
+ marketBadgeViews[index].setPadding(kotlin.math.floor(padding).toInt(), 0, (padding * 2).roundToInt() - kotlin.math.floor(padding).toInt(), 0)
2381
+ }
2382
+ }
2153
2383
  contentDescription = item.json.optString(
2154
2384
  "accessibilityLabel",
2155
2385
  listOf(
@@ -2817,16 +3047,41 @@ internal class NativeListRowView(
2817
3047
  )
2818
3048
  return
2819
3049
  }
3050
+ if (isMarket && variant == "retry") {
3051
+ val message = item.json.optString("message")
3052
+ orientation = VERTICAL
3053
+ gravity = Gravity.CENTER
3054
+ setPadding(dp(32), dp(if (message.isEmpty()) 11 else 32), dp(32), dp(if (message.isEmpty()) 11 else 27))
3055
+ if (message.isNotEmpty()) {
3056
+ showText(title, message, 2)
3057
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3058
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3059
+ applyMarketTextMetrics(title, style)
3060
+ addView(mainColumn, wrap().apply { bottomMargin = kotlin.math.ceil(7.0 * resources.displayMetrics.density).toInt() })
3061
+ }
3062
+ showTrailing(0, item.json.optString("actionText", "Retry"), true, item.json.optString("actionKey"))
3063
+ val button = trailingViews[0]
3064
+ val style = JSONObject().put("fontSize", 14).put("lineHeight", 20)
3065
+ applyMarketTextStyle(button, style, 14f, 20, "medium", color(theme, "secondaryText", "#0000009B"), "center")
3066
+ applyMarketTextMetrics(button, style)
3067
+ button.background = null
3068
+ // Match Yoga's text rounding before adding the tertiary Button's border/padding.
3069
+ val density = resources.displayMetrics.density
3070
+ val buttonWidth = (kotlin.math.ceil(button.paint.measureText(button.text.toString()).toDouble()) + 18 * density).roundToInt()
3071
+ val buttonHeight = kotlin.math.ceil(kotlin.math.ceil(20.0 * density) + 10 * density).toInt()
3072
+ button.setPadding(0, 0, 0, 0)
3073
+ button.layoutParams = LayoutParams(buttonWidth, buttonHeight)
3074
+ addView(trailingColumn, wrap())
3075
+ return
3076
+ }
2820
3077
  if (isMarket && variant == "noMatch") {
2821
3078
  gravity = Gravity.CENTER
2822
3079
  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
3080
  showText(title, item.json.optString("message"), 1)
2829
- addView(mainColumn, weighted())
3081
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3082
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3083
+ applyMarketTextMetrics(title, style)
3084
+ addView(mainColumn, wrap())
2830
3085
  return
2831
3086
  }
2832
3087
  // OneKey patch: warning title/description wrap inside the actual scroll content.
@@ -2972,12 +3227,19 @@ internal class NativeListRowView(
2972
3227
  if (tokenPair) {
2973
3228
  leadingOverlayBackground.visibility = VISIBLE
2974
3229
  leadingOverlayBackground.background = roundedFill(visualBackdropColor, 10f)
3230
+ val item = tag as? NativeListItem
3231
+ val marketPadding = if (item?.type == "market") item.json.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0 else null
3232
+ val density = resources.displayMetrics.density
3233
+ // Yoga rounds the absolute horizontal edges, including the avatar's fractional origin.
3234
+ val badgeRight = marketPadding?.let { ((it + sizeDp + 4) * density).roundToInt() }
3235
+ val badgeLeft = marketPadding?.let { ((it + sizeDp - 16) * density).roundToInt() }
3236
+ val avatarRight = marketPadding?.let { ((it + sizeDp) * density).roundToInt() }
2975
3237
  leadingOverlayBackground.layoutParams = FrameLayout.LayoutParams(
2976
- dp(20),
3238
+ if (badgeRight != null && badgeLeft != null) badgeRight - badgeLeft else dp(20),
2977
3239
  dp(20),
2978
3240
  Gravity.END or Gravity.BOTTOM,
2979
3241
  ).apply {
2980
- marginEnd = -dp(4)
3242
+ marginEnd = if (badgeRight != null && avatarRight != null) avatarRight - badgeRight else -dp(4)
2981
3243
  bottomMargin = -dp(4)
2982
3244
  }
2983
3245
  }
@@ -3019,21 +3281,26 @@ internal class NativeListRowView(
3019
3281
  image.clipToOutline = true
3020
3282
  if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3021
3283
  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
3284
+ val density = resources.displayMetrics.density
3285
+ val sourceRadius = cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp))
3286
+ // OneKey patch: retain the source's fractional border and background rendering.
3287
+ leadingFrame.background = null
3288
+ BackgroundStyleApplicator.setBackgroundColor(leadingFrame, visualBackground)
3289
+ // Match the source's physical edges: React Native rasterizes ALL as a different stroke path.
3290
+ for (edge in arrayOf(LogicalEdge.LEFT, LogicalEdge.TOP, LogicalEdge.RIGHT, LogicalEdge.BOTTOM)) {
3291
+ BackgroundStyleApplicator.setBorderWidth(leadingFrame, edge, 1f)
3292
+ BackgroundStyleApplicator.setBorderColor(leadingFrame, edge, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3027
3293
  }
3028
- image.layoutParams = FrameLayout.LayoutParams(dp(sizeDp) - inset * 2, dp(heightDp) - inset * 2).apply {
3294
+ BackgroundStyleApplicator.setBorderRadius(leadingFrame, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(sourceRadius, LengthPercentageType.POINT))
3295
+ // OneKey patch: round the source image's absolute edges inside its border.
3296
+ val sourceLeft = ((tag as? NativeListItem)?.json?.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0) * density
3297
+ val imageWidth = (sourceLeft + (sizeDp - 1) * density).roundToInt() - (sourceLeft + density).roundToInt()
3298
+ image.layoutParams = FrameLayout.LayoutParams(imageWidth, dp(heightDp - 2)).apply {
3029
3299
  leftMargin = inset
3030
3300
  topMargin = inset
3031
3301
  }
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
- }
3302
+ marketLeadingUsesSourceClip = true
3303
+ image.clipToOutline = false
3037
3304
  }
3038
3305
  val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
3039
3306
  val expectedEpoch = bindingEpoch
@@ -3497,6 +3764,13 @@ internal class NativeListRowView(
3497
3764
  }
3498
3765
 
3499
3766
  private fun applySize(item: NativeListItem) {
3767
+ if (item.type == "system" && item.json.optString("presentation") == "market" &&
3768
+ item.json.optString("variant") in setOf("retry", "noMatch")) {
3769
+ val defaultHeight = if (item.json.optString("variant") == "noMatch") 88.0 else if (item.json.optString("message").isEmpty()) 52.0 else 120.0
3770
+ minimumHeight = (item.json.optDouble("height", defaultHeight).toFloat() * resources.displayMetrics.density).roundToInt()
3771
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3772
+ return
3773
+ }
3500
3774
  if (item.type == "market") {
3501
3775
  val style = item.json.optJSONObject("style")
3502
3776
  val imageHeight = style?.optJSONObject("image")?.optDouble(
@@ -3505,12 +3779,15 @@ internal class NativeListRowView(
3505
3779
  ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3506
3780
  val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3507
3781
  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(),
3782
+ // OneKey patch: preserve fractional DP until the final physical pixel edge.
3783
+ val rowHeight = item.json.optDouble(
3784
+ "height",
3785
+ maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3513
3786
  )
3787
+ val sourceScale = if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)
3788
+ minimumHeight = (rowHeight * sourceScale * resources.displayMetrics.density).roundToInt()
3789
+ // OneKey patch: explicit Market row heights must not grow after child pixel rounding.
3790
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3514
3791
  return
3515
3792
  }
3516
3793
  val isWalletSidebar =