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