@onekeyfe/react-native-native-list 3.0.105

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.
Files changed (181) hide show
  1. package/README.md +342 -0
  2. package/android/CMakeLists.txt +20 -0
  3. package/android/build.gradle +105 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  7. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +125 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +165 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +147 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListPackage.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +2939 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1794 -0
  13. package/common/fonts/Roobert-Bold.ttf +0 -0
  14. package/common/fonts/Roobert-Medium.ttf +0 -0
  15. package/common/fonts/Roobert-Regular.ttf +0 -0
  16. package/common/fonts/Roobert-SemiBold.ttf +0 -0
  17. package/docs/DESIGN.md +129 -0
  18. package/ios/HybridNativeList.swift +131 -0
  19. package/ios/NativeListCell.swift +2858 -0
  20. package/ios/NativeListDesignAssets.swift +110 -0
  21. package/ios/NativeListModels.swift +215 -0
  22. package/ios/RNCNativeListView.swift +1737 -0
  23. package/ios/Resources/NativeListIcons.xcassets/Contents.json +1 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/Contents.json +1 -0
  25. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/icon.svg +1 -0
  26. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/Contents.json +1 -0
  27. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/icon.svg +1 -0
  28. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/Contents.json +1 -0
  29. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/icon.svg +1 -0
  30. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/Contents.json +1 -0
  31. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/icon.svg +1 -0
  32. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/Contents.json +1 -0
  33. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/icon.svg +1 -0
  34. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/Contents.json +1 -0
  35. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/icon.svg +1 -0
  36. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/Contents.json +1 -0
  37. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/icon.svg +1 -0
  38. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/Contents.json +1 -0
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/icon.svg +1 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/Contents.json +1 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/icon.svg +1 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/Contents.json +1 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/icon.svg +1 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/Contents.json +1 -0
  45. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/icon.svg +1 -0
  46. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/Contents.json +1 -0
  47. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/icon.svg +1 -0
  48. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/Contents.json +1 -0
  49. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/icon.svg +1 -0
  50. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/Contents.json +1 -0
  51. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/icon.svg +1 -0
  52. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/Contents.json +1 -0
  53. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/icon.svg +1 -0
  54. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/Contents.json +1 -0
  55. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/icon.svg +1 -0
  56. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/Contents.json +1 -0
  57. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/icon.svg +1 -0
  58. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/Contents.json +1 -0
  59. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/icon.svg +1 -0
  60. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/Contents.json +1 -0
  61. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/icon.svg +1 -0
  62. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/Contents.json +1 -0
  63. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/icon.svg +1 -0
  64. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/Contents.json +1 -0
  65. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/icon.svg +1 -0
  66. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/Contents.json +1 -0
  67. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/icon.svg +1 -0
  68. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/Contents.json +1 -0
  69. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/icon.svg +1 -0
  70. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/Contents.json +1 -0
  71. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/icon.svg +1 -0
  72. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/Contents.json +1 -0
  73. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/icon.svg +1 -0
  74. package/lib/module/NativeList.js +183 -0
  75. package/lib/module/NativeList.nitro.js +4 -0
  76. package/lib/module/NativeList.types.js +4 -0
  77. package/lib/module/NativeList.web.js +167 -0
  78. package/lib/module/index.js +6 -0
  79. package/lib/module/models.js +2 -0
  80. package/lib/module/package.json +1 -0
  81. package/lib/module/scrolling.js +121 -0
  82. package/lib/module/selection.js +85 -0
  83. package/lib/module/validation.js +562 -0
  84. package/lib/module/web/NativeListWebEngine.js +2238 -0
  85. package/lib/nitrogen/generated/.gitattributes +1 -0
  86. package/lib/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  87. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  88. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  89. package/lib/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  90. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  91. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  92. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  93. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  94. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  95. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  96. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  97. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  98. package/lib/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  99. package/lib/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  100. package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  101. package/lib/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  102. package/lib/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  103. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  104. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  105. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  106. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  107. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  108. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  109. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  110. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  111. package/lib/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  112. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  113. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  114. package/lib/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  115. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  116. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  117. package/lib/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  118. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  119. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  120. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  121. package/lib/typescript/package.json +1 -0
  122. package/lib/typescript/src/NativeList.d.ts +18 -0
  123. package/lib/typescript/src/NativeList.nitro.d.ts +29 -0
  124. package/lib/typescript/src/NativeList.types.d.ts +87 -0
  125. package/lib/typescript/src/NativeList.web.d.ts +19 -0
  126. package/lib/typescript/src/index.d.ts +7 -0
  127. package/lib/typescript/src/models.d.ts +456 -0
  128. package/lib/typescript/src/scrolling.d.ts +32 -0
  129. package/lib/typescript/src/selection.d.ts +27 -0
  130. package/lib/typescript/src/validation.d.ts +7 -0
  131. package/lib/typescript/src/web/NativeListWebEngine.d.ts +203 -0
  132. package/nitro.json +23 -0
  133. package/nitrogen/generated/.gitattributes +1 -0
  134. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  136. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  137. package/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  138. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  139. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  146. package/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  147. package/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  148. package/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  149. package/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  150. package/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  151. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  152. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  153. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  154. package/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  155. package/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  156. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  157. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  158. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  159. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  160. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  161. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  162. package/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  163. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  164. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  165. package/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  167. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  168. package/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  169. package/package.json +126 -0
  170. package/react-native-native-list.podspec +38 -0
  171. package/react-native.config.js +19 -0
  172. package/src/NativeList.nitro.ts +48 -0
  173. package/src/NativeList.tsx +298 -0
  174. package/src/NativeList.types.ts +125 -0
  175. package/src/NativeList.web.tsx +258 -0
  176. package/src/index.ts +35 -0
  177. package/src/models.ts +604 -0
  178. package/src/scrolling.ts +189 -0
  179. package/src/selection.ts +159 -0
  180. package/src/validation.ts +877 -0
  181. package/src/web/NativeListWebEngine.ts +3767 -0
@@ -0,0 +1,2939 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.TimeInterpolator
6
+ import android.animation.ValueAnimator
7
+ import android.graphics.Color
8
+ import android.graphics.Canvas
9
+ import android.graphics.Outline
10
+ import android.graphics.Paint
11
+ import android.graphics.Path
12
+ import android.graphics.Typeface
13
+ import android.graphics.drawable.Drawable
14
+ import android.graphics.drawable.GradientDrawable
15
+ import android.text.Spannable
16
+ import android.text.SpannableStringBuilder
17
+ import android.text.TextUtils
18
+ import android.text.style.AbsoluteSizeSpan
19
+ import android.text.style.BackgroundColorSpan
20
+ import android.text.style.ForegroundColorSpan
21
+ import android.util.TypedValue
22
+ import android.view.Gravity
23
+ import android.view.MotionEvent
24
+ import android.view.View
25
+ import android.view.ViewGroup
26
+ import android.view.ViewOutlineProvider
27
+ import android.widget.FrameLayout
28
+ import android.widget.LinearLayout
29
+ import android.widget.ProgressBar
30
+ import android.widget.TextView
31
+ import com.facebook.react.uimanager.ThemedReactContext
32
+ import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
33
+ import androidx.core.graphics.PathParser
34
+ import androidx.core.widget.TextViewCompat
35
+ import org.json.JSONArray
36
+ import org.json.JSONObject
37
+ import kotlin.math.roundToInt
38
+
39
+ internal data class NativeListActionOrigin(
40
+ val sourceView: View,
41
+ val ownerRowView: NativeListRowView,
42
+ val bindingEpoch: Long,
43
+ val source: String,
44
+ val slot: Int? = null,
45
+ )
46
+
47
+ /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
48
+ internal fun parseNativeListColor(value: String): Int {
49
+ val normalized = value.trim()
50
+ val androidValue = if (
51
+ normalized.length == 9 && normalized[0] == '#' &&
52
+ normalized.drop(1).all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
53
+ ) {
54
+ "#${normalized.takeLast(2)}${normalized.substring(1, 7)}"
55
+ } else {
56
+ normalized
57
+ }
58
+ return Color.parseColor(androidValue)
59
+ }
60
+
61
+ internal object NativeListScale {
62
+ private const val STANDARD_DP_WIDTH_THRESHOLD = 400
63
+
64
+ fun factor(resources: android.content.res.Resources): Float {
65
+ val widthDp = resources.configuration.screenWidthDp
66
+ return if (widthDp in 1 until STANDARD_DP_WIDTH_THRESHOLD) 0.9f else 1f
67
+ }
68
+
69
+ fun dp(resources: android.content.res.Resources, value: Int): Int =
70
+ (value * factor(resources) * resources.displayMetrics.density).roundToInt()
71
+
72
+ fun dp(resources: android.content.res.Resources, value: Float): Float =
73
+ (value * factor(resources) * resources.displayMetrics.density).roundToInt().toFloat()
74
+
75
+ fun font(resources: android.content.res.Resources, value: Float): Float =
76
+ if (factor(resources) == 1f) value else (value * factor(resources)).roundToInt().toFloat()
77
+ }
78
+
79
+ internal object NativeListFonts {
80
+ private val cache = mutableMapOf<String, Typeface>()
81
+
82
+ private fun load(context: android.content.Context, weight: String, fallback: String): Typeface =
83
+ cache.getOrPut(weight) {
84
+ try {
85
+ Typeface.createFromAsset(context.assets, "fonts/Roobert-$weight.ttf")
86
+ } catch (_: RuntimeException) {
87
+ Typeface.create(fallback, Typeface.NORMAL)
88
+ }
89
+ }
90
+
91
+ fun regular(context: android.content.Context) = load(context, "Regular", "sans-serif")
92
+ fun medium(context: android.content.Context) = load(context, "Medium", "sans-serif-medium")
93
+ fun semibold(context: android.content.Context) = load(context, "SemiBold", "sans-serif-medium")
94
+ fun bold(context: android.content.Context) = load(context, "Bold", "sans-serif")
95
+ }
96
+
97
+ internal data class NativeSelectionTarget(val scope: String, val key: String?)
98
+
99
+ private class DottedUnderlineTextView(context: android.content.Context) : TextView(context) {
100
+ var showsDottedUnderline = false
101
+ var dottedUnderlineColor = Color.TRANSPARENT
102
+ private val dottedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
103
+
104
+ override fun onDraw(canvas: Canvas) {
105
+ super.onDraw(canvas)
106
+ if (!showsDottedUnderline || text.isEmpty()) return
107
+ dottedPaint.color = dottedUnderlineColor
108
+ val radius = NativeListScale.dp(resources, 0.75f)
109
+ val spacing = NativeListScale.dp(resources, 4f)
110
+ val lineWidth = paint.measureText(text.toString()).coerceAtMost(width.toFloat())
111
+ val y = height - radius
112
+ var x = NativeListScale.dp(resources, 1f)
113
+ while (x <= lineWidth - radius) {
114
+ canvas.drawCircle(x, y, radius, dottedPaint)
115
+ x += spacing
116
+ }
117
+ }
118
+ }
119
+
120
+ private class PackedTitleLineLayout(context: android.content.Context) : LinearLayout(context) {
121
+ var packsChildrenAtStart = false
122
+
123
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
124
+ if (!packsChildrenAtStart || childCount < 2) {
125
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
126
+ return
127
+ }
128
+
129
+ val title = getChildAt(0)
130
+ val badge = getChildAt(1)
131
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
132
+ val widthSize = MeasureSpec.getSize(widthMeasureSpec)
133
+ if (badge.visibility != GONE) {
134
+ measureChildWithMargins(
135
+ badge,
136
+ widthMeasureSpec,
137
+ paddingLeft + paddingRight,
138
+ heightMeasureSpec,
139
+ paddingTop + paddingBottom,
140
+ )
141
+ }
142
+
143
+ val badgeMargins = badge.layoutParams as MarginLayoutParams
144
+ val badgeWidth = if (badge.visibility == GONE) 0 else {
145
+ badge.measuredWidth + badgeMargins.leftMargin + badgeMargins.rightMargin
146
+ }
147
+ val titleMargins = title.layoutParams as MarginLayoutParams
148
+ if (widthMode != MeasureSpec.UNSPECIFIED) {
149
+ (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - badgeWidth -
150
+ titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0)
151
+ }
152
+ (title.layoutParams as LayoutParams).apply {
153
+ width = LayoutParams.WRAP_CONTENT
154
+ weight = 0f
155
+ }
156
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
157
+ if (widthMode != MeasureSpec.UNSPECIFIED) {
158
+ setMeasuredDimension(widthSize, measuredHeight)
159
+ }
160
+ }
161
+ }
162
+
163
+ private class NativeListTableColumnView(context: android.content.Context) : LinearLayout(context) {
164
+ private val primaryLine = LinearLayout(context)
165
+ private val primary = TextView(context)
166
+ private val badges = LinearLayout(context)
167
+ private val secondaryLine = LinearLayout(context)
168
+ private val secondaryLeading = TextView(context)
169
+ private val secondary = TextView(context)
170
+
171
+ init {
172
+ orientation = VERTICAL
173
+ primaryLine.orientation = HORIZONTAL
174
+ primaryLine.gravity = Gravity.CENTER_VERTICAL
175
+ badges.orientation = HORIZONTAL
176
+ badges.gravity = Gravity.CENTER_VERTICAL
177
+ primary.includeFontPadding = false
178
+ primary.fontFeatureSettings = "tnum"
179
+ primary.maxLines = 1
180
+ primary.ellipsize = TextUtils.TruncateAt.END
181
+ primary.textSize = sp(14f)
182
+ primary.typeface = NativeListFonts.medium(context)
183
+ TextViewCompat.setLineHeight(primary, dp(20))
184
+ secondaryLine.orientation = HORIZONTAL
185
+ secondaryLine.gravity = Gravity.CENTER_VERTICAL
186
+ listOf(secondaryLeading, secondary).forEach { label ->
187
+ label.includeFontPadding = false
188
+ label.fontFeatureSettings = "tnum"
189
+ label.maxLines = 1
190
+ label.ellipsize = TextUtils.TruncateAt.END
191
+ label.textSize = sp(12f)
192
+ label.typeface = NativeListFonts.regular(context)
193
+ TextViewCompat.setLineHeight(label, dp(16))
194
+ }
195
+ secondaryLeading.maxWidth = dp(120)
196
+ secondaryLine.addView(secondaryLeading, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)))
197
+ secondaryLine.addView(secondary, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
198
+ marginStart = dp(4)
199
+ })
200
+ primaryLine.addView(primary, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
201
+ primaryLine.addView(badges, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
202
+ marginStart = dp(6)
203
+ })
204
+ addView(primaryLine, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
205
+ addView(secondaryLine, LayoutParams(LayoutParams.MATCH_PARENT, dp(16)).apply {
206
+ topMargin = dp(4)
207
+ })
208
+ }
209
+
210
+ fun reset() {
211
+ primary.text = ""
212
+ secondaryLeading.text = ""
213
+ secondary.text = ""
214
+ secondaryLeading.visibility = GONE
215
+ secondary.visibility = GONE
216
+ secondaryLine.visibility = GONE
217
+ badges.removeAllViews()
218
+ badges.visibility = GONE
219
+ }
220
+
221
+ fun bind(
222
+ column: JSONObject,
223
+ rowBadges: JSONArray?,
224
+ primaryColor: Int,
225
+ secondaryColor: Int,
226
+ infoColor: Int,
227
+ ) {
228
+ reset()
229
+ gravity = when (column.optString("alignment", "start")) {
230
+ "center" -> Gravity.CENTER_HORIZONTAL
231
+ "end" -> Gravity.END
232
+ else -> Gravity.START
233
+ }
234
+ primary.gravity = gravity
235
+ secondaryLeading.gravity = gravity
236
+ secondary.gravity = gravity
237
+ primary.text = column.optString("text")
238
+ primary.setTextColor(primaryColor)
239
+ if (rowBadges != null && rowBadges.length() > 0) {
240
+ badges.visibility = VISIBLE
241
+ for (index in 0 until minOf(2, rowBadges.length())) {
242
+ val badge = TextView(context).apply {
243
+ includeFontPadding = false
244
+ fontFeatureSettings = "tnum"
245
+ gravity = Gravity.CENTER
246
+ text = rowBadges.getJSONObject(index).optString("text")
247
+ textSize = sp(10f)
248
+ typeface = NativeListFonts.regular(context)
249
+ setTextColor(infoColor)
250
+ setPadding(dp(6), 0, dp(6), 0)
251
+ background = GradientDrawable().apply {
252
+ setColor(parseNativeListColor("#008FF519"))
253
+ cornerRadius = dp(4).toFloat()
254
+ }
255
+ }
256
+ badges.addView(badge, LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply {
257
+ if (index > 0) marginStart = dp(4)
258
+ })
259
+ }
260
+ }
261
+ val secondaryLeadingText = column.optString("secondaryLeadingText")
262
+ if (secondaryLeadingText.isNotEmpty()) {
263
+ secondaryLeading.text = secondaryLeadingText
264
+ secondaryLeading.setTextColor(secondaryColor)
265
+ secondaryLeading.visibility = VISIBLE
266
+ secondaryLine.visibility = VISIBLE
267
+ }
268
+ val secondaryText = column.optString("secondaryText")
269
+ if (secondaryText.isNotEmpty()) {
270
+ secondary.text = secondaryText
271
+ secondary.setTextColor(secondaryColor)
272
+ secondary.visibility = VISIBLE
273
+ secondaryLine.visibility = VISIBLE
274
+ }
275
+ }
276
+
277
+ private fun dp(value: Int): Int = NativeListScale.dp(resources, value)
278
+ private fun sp(value: Float): Float = NativeListScale.font(resources, value)
279
+ }
280
+
281
+ internal class NativeListRowView(
282
+ private val reactContext: ThemedReactContext,
283
+ ) : LinearLayout(reactContext) {
284
+ private val leadingFrame = FrameLayout(context)
285
+ private val leadingImages = List(3) { OneKeyImageReusableView(reactContext) }
286
+ private val leadingOverlayBackground = View(context)
287
+ private val leadingCornerIconFrame = FrameLayout(context)
288
+ private val leadingCornerIcon = OneKeyIconView(context)
289
+ private val leadingFallback = TextView(context)
290
+ private val leadingIcon = OneKeyIconView(context)
291
+ private val favoriteIcon = OneKeyIconView(context)
292
+ private val headerTitleIcon = OneKeyIconView(context)
293
+ private val headerValueIcon = OneKeyIconView(context)
294
+ private val leadingActionIcon = OneKeyIconView(context)
295
+ private val secondaryImage = OneKeyImageReusableView(reactContext)
296
+ private val mediaNetworkImage = OneKeyImageReusableView(reactContext)
297
+ private val metricVisualImages = List(5) { OneKeyImageReusableView(reactContext) }
298
+ private val mainColumn = LinearLayout(context)
299
+ private val mediaMetadataRow = LinearLayout(context)
300
+ private val titleLine = PackedTitleLineLayout(context)
301
+ private val title = DottedUnderlineTextView(context)
302
+ private val subtitle = TextView(context)
303
+ private val tertiary = TextView(context)
304
+ private val status = TextView(context)
305
+ private val metricSubtitle = TextView(context)
306
+ private val badgeLine = TextView(context)
307
+ private val activityContentRow = LinearLayout(context)
308
+ private val actionLine = LinearLayout(context)
309
+ private val actionViews = List(3) { TextView(context) }
310
+ private val trailingColumn = LinearLayout(context)
311
+ private val trailingViews = List(2) { TextView(context) }
312
+ private val trailingIcons = List(2) { OneKeyIconView(context) }
313
+ private val checkbox = OneKeyCheckboxView(context)
314
+ private val spinner = ProgressBar(context)
315
+ private val dataContainer = LinearLayout(context)
316
+ private val dataColumns = List(4) { TextView(context) }
317
+ private val tableDataContainer = LinearLayout(context)
318
+ private val tableDataColumns = List(4) { NativeListTableColumnView(context) }
319
+ private val unreadDot = View(context)
320
+ private val mediaBadge = TextView(context)
321
+ private val skeletonPrimary = View(context)
322
+ private val skeletonSecondary = View(context)
323
+ private val walletGroupRows = mutableListOf<NativeListRowView>()
324
+ private val walletGroupDragBadgeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
325
+ private val walletGroupDragBadgeBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
326
+ style = Paint.Style.STROKE
327
+ }
328
+ private val walletGroupDragBadgeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
329
+ textAlign = Paint.Align.CENTER
330
+ typeface = NativeListFonts.semibold(context)
331
+ }
332
+ private var walletGroupDragChildCount = 0
333
+ private var walletGroupExpandedHeightPx = 0
334
+ private var walletGroupExpandAnimator: ValueAnimator? = null
335
+ private var isMediaTile = false
336
+ private var boundKey: String? = null
337
+ var bindingEpoch: Long = 0
338
+ private set
339
+ private var boundCheckboxData: JSONObject? = null
340
+ private var currentLayout = "linear"
341
+ private var restingRowBackground: Drawable? = null
342
+ private var pressedRowBackground: Drawable? = null
343
+ private var reorderActive = false
344
+ private var checkboxCheckedColor = Color.rgb(32, 32, 32)
345
+ private var checkboxUncheckedColor = Color.rgb(252, 252, 252)
346
+ private var checkboxBorderColor = Color.rgb(206, 206, 206)
347
+ private var iconSubduedColor = Color.rgb(141, 141, 141)
348
+ private var visualBackdropColor = Color.WHITE
349
+ private val circleOutlineProvider = object : ViewOutlineProvider() {
350
+ override fun getOutline(view: View, outline: Outline) {
351
+ outline.setOval(0, 0, view.width, view.height)
352
+ }
353
+ }
354
+ private val separatorPaint = Paint(Paint.ANTI_ALIAS_FLAG)
355
+ private var showsSeparator = false
356
+
357
+ var onRowPress: ((NativeListItem, NativeListActionOrigin) -> Unit)? = null
358
+ var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Unit)? = null
359
+ var onBindingInvalidated: ((NativeListRowView, Long) -> Unit)? = null
360
+
361
+ init {
362
+ gravity = Gravity.CENTER_VERTICAL
363
+ isClickable = true
364
+ isFocusable = true
365
+
366
+ leadingFallback.gravity = Gravity.CENTER
367
+ leadingFallback.typeface = NativeListFonts.bold(context)
368
+ leadingFrame.addView(leadingFallback)
369
+ leadingFrame.addView(leadingIcon)
370
+ leadingFrame.addView(leadingImages[0])
371
+ leadingFrame.addView(leadingOverlayBackground)
372
+ leadingFrame.addView(leadingImages[1])
373
+ leadingFrame.addView(leadingImages[2])
374
+ leadingCornerIconFrame.addView(leadingCornerIcon)
375
+ leadingFrame.addView(leadingCornerIconFrame)
376
+ leadingFrame.addView(unreadDot)
377
+ leadingFrame.addView(mediaBadge)
378
+ leadingFrame.clipChildren = false
379
+ leadingFrame.clipToPadding = false
380
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
381
+ favoriteIcon.layoutParams = LayoutParams(dp(24), dp(24))
382
+ leadingActionIcon.layoutParams = LayoutParams(dp(24), dp(24))
383
+ leadingCornerIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
384
+ unreadDot.layoutParams = FrameLayout.LayoutParams(dp(8), dp(8), Gravity.TOP or Gravity.END)
385
+ mainColumn.orientation = VERTICAL
386
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
387
+ title.typeface = NativeListFonts.regular(context)
388
+ subtitle.typeface = NativeListFonts.regular(context)
389
+ tertiary.typeface = NativeListFonts.regular(context)
390
+ status.typeface = NativeListFonts.regular(context)
391
+ metricSubtitle.typeface = NativeListFonts.regular(context)
392
+ badgeLine.typeface = NativeListFonts.medium(context)
393
+ listOf(title, subtitle, tertiary, status, metricSubtitle, badgeLine).forEach {
394
+ it.includeFontPadding = false
395
+ it.fontFeatureSettings = "tnum"
396
+ }
397
+ subtitle.maxLines = 2
398
+ status.maxLines = 1
399
+ metricSubtitle.maxLines = 1
400
+ badgeLine.maxLines = 1
401
+ titleLine.orientation = HORIZONTAL
402
+ titleLine.gravity = Gravity.CENTER_VERTICAL
403
+ titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
404
+ titleLine.addView(badgeLine, wrap())
405
+ mainColumn.addView(titleLine)
406
+ mainColumn.addView(subtitle)
407
+ mainColumn.addView(tertiary)
408
+ mainColumn.addView(status)
409
+ mainColumn.addView(metricSubtitle)
410
+
411
+ actionLine.orientation = HORIZONTAL
412
+ actionViews.forEach { action ->
413
+ action.typeface = NativeListFonts.semibold(context)
414
+ action.includeFontPadding = false
415
+ action.textSize = sp(14f)
416
+ TextViewCompat.setLineHeight(action, dp(20))
417
+ action.setPadding(dp(8), dp(4), dp(8), dp(4))
418
+ actionLine.addView(action)
419
+ }
420
+ mainColumn.addView(actionLine)
421
+
422
+ trailingColumn.orientation = VERTICAL
423
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
424
+ trailingViews.forEach { trailingColumn.addView(it) }
425
+ trailingIcons.forEach { trailingColumn.addView(it) }
426
+ trailingColumn.addView(checkbox)
427
+ trailingColumn.addView(spinner)
428
+
429
+ dataContainer.orientation = HORIZONTAL
430
+ dataContainer.gravity = Gravity.CENTER_VERTICAL
431
+ dataColumns.forEach { dataContainer.addView(it) }
432
+ tableDataContainer.orientation = HORIZONTAL
433
+ tableDataContainer.gravity = Gravity.CENTER_VERTICAL
434
+ tableDataColumns.forEach { tableDataContainer.addView(it) }
435
+
436
+ mediaBadge.gravity = Gravity.CENTER
437
+ mediaBadge.typeface = NativeListFonts.regular(context)
438
+ secondaryImage.outlineProvider = object : ViewOutlineProvider() {
439
+ override fun getOutline(view: View, outline: Outline) {
440
+ outline.setRoundRect(0, 0, view.width, view.height, dp(6).toFloat())
441
+ }
442
+ }
443
+ secondaryImage.clipToOutline = true
444
+
445
+ setOnTouchListener { _, event ->
446
+ when (event.actionMasked) {
447
+ MotionEvent.ACTION_DOWN -> if (isEnabled) {
448
+ if ((tag as? NativeListItem)?.type == "mediaTile") {
449
+ leadingFrame.alpha = 0.8f
450
+ } else {
451
+ background = pressedRowBackground
452
+ }
453
+ }
454
+ MotionEvent.ACTION_MOVE -> if (
455
+ event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
456
+ ) {
457
+ restoreRestingBackground()
458
+ }
459
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> restoreRestingBackground()
460
+ }
461
+ false
462
+ }
463
+ setOnClickListener { view ->
464
+ (view.tag as? NativeListItem)?.let { item ->
465
+ onRowPress?.invoke(item, actionOrigin(view, "row"))
466
+ }
467
+ }
468
+ setWillNotDraw(false)
469
+ }
470
+
471
+ override fun dispatchDraw(canvas: Canvas) {
472
+ super.dispatchDraw(canvas)
473
+ if (showsSeparator) {
474
+ val start = if ((tag as? NativeListItem)?.type == "identity") dp(60).toFloat() else dp(12).toFloat()
475
+ canvas.drawLine(start, height - 1f, width.toFloat(), height - 1f, separatorPaint)
476
+ }
477
+ if (reorderActive && (tag as? NativeListItem)?.type == "walletGroup" && walletGroupDragChildCount > 0) {
478
+ val label = "+$walletGroupDragChildCount"
479
+ val badgeHeight = dp(24).toFloat()
480
+ val badgeWidth = maxOf(
481
+ dp(24).toFloat(),
482
+ walletGroupDragBadgeTextPaint.measureText(label) + dp(12),
483
+ )
484
+ val right = width - dp(4).toFloat()
485
+ val bottom = height - dp(4).toFloat()
486
+ val left = right - badgeWidth
487
+ val top = bottom - badgeHeight
488
+ canvas.drawRoundRect(
489
+ left,
490
+ top,
491
+ right,
492
+ bottom,
493
+ badgeHeight / 2f,
494
+ badgeHeight / 2f,
495
+ walletGroupDragBadgeBackgroundPaint,
496
+ )
497
+ canvas.drawRoundRect(
498
+ left,
499
+ top,
500
+ right,
501
+ bottom,
502
+ dp(12).toFloat(),
503
+ dp(12).toFloat(),
504
+ walletGroupDragBadgeBorderPaint,
505
+ )
506
+ val metrics = walletGroupDragBadgeTextPaint.fontMetrics
507
+ val baseline = (top + bottom - metrics.ascent - metrics.descent) / 2f
508
+ canvas.drawText(label, (left + right) / 2f, baseline, walletGroupDragBadgeTextPaint)
509
+ }
510
+ }
511
+
512
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
513
+ if (isMediaTile) {
514
+ val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
515
+ .coerceAtLeast(0)
516
+ leadingFrame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, availableWidth)
517
+ }
518
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
519
+ }
520
+
521
+ fun bind(
522
+ item: NativeListItem,
523
+ theme: JSONObject?,
524
+ layout: String,
525
+ listOrientation: String,
526
+ itemIndex: Int?,
527
+ selected: Boolean,
528
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
529
+ ) {
530
+ invalidateCurrentBinding()
531
+ bindingEpoch += 1
532
+ boundKey = item.key
533
+ currentLayout = layout
534
+ tag = item
535
+ reorderActive = false
536
+ leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
537
+ secondaryImage.prepareForReuse()
538
+ mediaNetworkImage.prepareForReuse()
539
+ metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
540
+ resetViews()
541
+
542
+ val primary = color(theme, "primaryText", "#000000DF")
543
+ val secondary = color(theme, "secondaryText", "#0000009B")
544
+ val accent = color(theme, "accent", "#0D8200FC")
545
+ checkboxCheckedColor = primary
546
+ checkboxUncheckedColor = color(theme, "inverseText", "#FCFCFC")
547
+ checkboxBorderColor = Color.argb(
548
+ 0x31,
549
+ 0,
550
+ 0,
551
+ 0,
552
+ )
553
+ iconSubduedColor = color(theme, "iconSubdued", "#00000072")
554
+ visualBackdropColor = color(theme, "rowBackground", "#FFFFFF")
555
+ unreadDot.background = roundedFill(
556
+ parseNativeListColor("#E5484D"),
557
+ 4f,
558
+ )
559
+ applySelectionState(item, theme, layout, itemIndex, selected)
560
+ pressedRowBackground = groupedBackground(
561
+ when (item.type) {
562
+ "rail" -> "rail"
563
+ "mediaTile" -> "mediaTile"
564
+ else -> "single"
565
+ },
566
+ color(
567
+ theme,
568
+ if (item.type == "rail") "strongBackground" else "rowPressedBackground",
569
+ if (item.type == "rail") "#0000000F" else "#00000017",
570
+ ),
571
+ )
572
+ background = restingRowBackground
573
+ if (layout == "table") {
574
+ if (item.type == "dataRow") {
575
+ setPadding(dp(20), dp(10), dp(20), dp(10))
576
+ } else {
577
+ setPadding(dp(16), dp(8), dp(16), dp(8))
578
+ }
579
+ }
580
+ title.setTextColor(primary)
581
+ subtitle.setTextColor(secondary)
582
+ tertiary.setTextColor(secondary)
583
+ status.setTextColor(secondary)
584
+ metricSubtitle.setTextColor(secondary)
585
+ badgeLine.setTextColor(accent)
586
+ separatorPaint.color = color(theme, "separator", "#0000001F")
587
+ separatorPaint.strokeWidth = 1f
588
+ showsSeparator = item.json.optBoolean("separator", false) &&
589
+ !item.key.startsWith("token-") &&
590
+ !item.key.startsWith("balance-token-") &&
591
+ item.key != "linear-custom-token"
592
+ invalidate()
593
+ trailingViews.forEach { it.setTextColor(primary) }
594
+ isEnabled = !item.json.optBoolean("disabled", false)
595
+ alpha = if (isEnabled) 1f else 0.5f
596
+ contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
597
+
598
+ when (item.type) {
599
+ "walletGroup" -> bindWalletGroup(item, theme, layout, listOrientation, checkboxState)
600
+ "identity" -> bindIdentity(item, theme, selected, checkboxState)
601
+ "rail" -> bindRail(item, theme)
602
+ "activity" -> bindActivity(item, theme)
603
+ "message" -> bindMessage(item, theme)
604
+ "dataRow" -> bindDataRow(item, theme, checkboxState)
605
+ "mediaTile" -> bindMediaTile(item, theme)
606
+ "metricCard" -> bindMetricCard(item, theme)
607
+ "sectionHeader" -> bindSectionHeader(item, theme, checkboxState)
608
+ "action" -> bindAction(item, theme, checkboxState)
609
+ "system" -> bindSystem(item, theme)
610
+ }
611
+ applySize(item)
612
+ applyListOrientation(item, listOrientation)
613
+ }
614
+
615
+ fun recycle() {
616
+ restoreRestingBackground()
617
+ invalidateCurrentBinding()
618
+ boundKey = null
619
+ leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
620
+ secondaryImage.prepareForReuse()
621
+ mediaNetworkImage.prepareForReuse()
622
+ metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
623
+ walletGroupRows.forEach { row ->
624
+ row.visibility = VISIBLE
625
+ row.alpha = 1f
626
+ row.recycle()
627
+ }
628
+ }
629
+
630
+ fun bindSelection(
631
+ item: NativeListItem,
632
+ theme: JSONObject?,
633
+ layout: String,
634
+ itemIndex: Int?,
635
+ selected: Boolean,
636
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
637
+ ) {
638
+ if (boundKey != item.key) return
639
+ if (item.type == "walletGroup") {
640
+ val members = buildList {
641
+ add(item.json.getJSONObject("parent"))
642
+ item.json.getJSONArray("children").let { children ->
643
+ for (index in 0 until children.length()) add(children.getJSONObject(index))
644
+ }
645
+ }
646
+ members.forEachIndexed { index, memberJson ->
647
+ walletGroupRows.getOrNull(index)?.bindSelection(
648
+ NativeListItem.parse(memberJson),
649
+ theme,
650
+ layout,
651
+ null,
652
+ memberJson.optBoolean("selected", false),
653
+ checkboxState,
654
+ )
655
+ }
656
+ // The group owns the subdued backdrop. Selection belongs to a member;
657
+ // applying the translucent selected color to both levels produces dark
658
+ // bands in the 12dp gaps and double-composites the parent highlight.
659
+ background = restingRowBackground
660
+ return
661
+ }
662
+ applySelectionState(item, theme, layout, itemIndex, selected)
663
+ if (item.type == "identity" && item.json.optString("presentation") == "walletSidebar") {
664
+ title.setTextColor(
665
+ color(
666
+ theme,
667
+ if (selected) "primaryText" else "secondaryText",
668
+ if (selected) "#FFFFFFED" else "#FFFFFFAF",
669
+ ),
670
+ )
671
+ }
672
+ boundCheckboxData?.let { bindCheckbox(item, it, checkboxState) }
673
+ }
674
+
675
+ fun bindStableSummary(item: NativeListItem) {
676
+ if (
677
+ boundKey != item.key ||
678
+ item.type != "sectionHeader" ||
679
+ item.json.optString("variant") != "summary"
680
+ ) {
681
+ return
682
+ }
683
+ tag = item
684
+ contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
685
+ title.text = item.json.optString("title")
686
+ trailingViews[0].text = item.json.optString("value")
687
+ }
688
+
689
+ fun dispose() {
690
+ restoreRestingBackground()
691
+ leadingImages.forEach(OneKeyImageReusableView::dispose)
692
+ secondaryImage.dispose()
693
+ mediaNetworkImage.dispose()
694
+ metricVisualImages.forEach(OneKeyImageReusableView::dispose)
695
+ walletGroupRows.forEach(NativeListRowView::dispose)
696
+ }
697
+
698
+ private fun actionOrigin(
699
+ sourceView: View,
700
+ source: String,
701
+ slot: Int? = null,
702
+ ) = NativeListActionOrigin(sourceView, this, bindingEpoch, source, slot)
703
+
704
+ private fun emitAction(
705
+ item: NativeListItem,
706
+ actionKey: String,
707
+ target: NativeSelectionTarget?,
708
+ sourceView: View,
709
+ source: String,
710
+ slot: Int? = null,
711
+ ) {
712
+ onAction?.invoke(item, actionKey, target, actionOrigin(sourceView, source, slot))
713
+ }
714
+
715
+ private fun invalidateCurrentBinding() {
716
+ if (boundKey == null) return
717
+ onBindingInvalidated?.invoke(this, bindingEpoch)
718
+ bindingEpoch += 1
719
+ }
720
+
721
+ private fun resetViews() {
722
+ walletGroupRows.forEach { it.invalidateCurrentBinding() }
723
+ walletGroupExpandAnimator?.removeAllListeners()
724
+ walletGroupExpandAnimator?.cancel()
725
+ walletGroupExpandAnimator = null
726
+ layoutParams?.takeIf { it.height != ViewGroup.LayoutParams.WRAP_CONTENT }?.let { params ->
727
+ params.height = ViewGroup.LayoutParams.WRAP_CONTENT
728
+ layoutParams = params
729
+ }
730
+ restoreRestingBackground()
731
+ walletGroupRows.forEach { row ->
732
+ row.visibility = VISIBLE
733
+ row.alpha = 1f
734
+ row.recycle()
735
+ }
736
+ activityContentRow.removeAllViews()
737
+ (actionLine.parent as? ViewGroup)?.removeView(actionLine)
738
+ removeAllViews()
739
+ orientation = HORIZONTAL
740
+ gravity = Gravity.CENTER_VERTICAL
741
+ minimumHeight = 0
742
+ setPadding(dp(12), dp(8), dp(12), dp(8))
743
+ isMediaTile = false
744
+ walletGroupDragChildCount = 0
745
+ walletGroupExpandedHeightPx = 0
746
+ boundCheckboxData = null
747
+ mainColumn.orientation = VERTICAL
748
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
749
+ mediaMetadataRow.removeView(subtitle)
750
+ mediaMetadataRow.removeView(mediaNetworkImage)
751
+ mainColumn.removeView(mediaMetadataRow)
752
+ mainColumn.removeView(titleLine)
753
+ mainColumn.removeView(subtitle)
754
+ mainColumn.removeView(tertiary)
755
+ mainColumn.addView(titleLine, 0)
756
+ mainColumn.addView(subtitle, 1)
757
+ mainColumn.addView(tertiary, 2)
758
+ mainColumn.addView(actionLine)
759
+ titleLine.packsChildrenAtStart = false
760
+ titleLine.removeView(headerTitleIcon)
761
+ trailingColumn.removeView(headerValueIcon)
762
+ title.gravity = Gravity.START
763
+ title.ellipsize = null
764
+ title.maxWidth = Int.MAX_VALUE
765
+ title.setHorizontallyScrolling(false)
766
+ title.text = ""
767
+ subtitle.text = ""
768
+ tertiary.text = ""
769
+ status.text = ""
770
+ metricSubtitle.text = ""
771
+ badgeLine.text = ""
772
+ title.setLineSpacing(0f, 1f)
773
+ subtitle.setLineSpacing(0f, 1f)
774
+ title.letterSpacing = 0f
775
+ title.showsDottedUnderline = false
776
+ title.setPadding(0, 0, 0, 0)
777
+ subtitle.letterSpacing = 0f
778
+ status.letterSpacing = 0f
779
+ badgeLine.letterSpacing = 0f
780
+ status.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
781
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
782
+ title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
783
+ badgeLine.layoutParams = wrap()
784
+ titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
785
+ title.visibility = GONE
786
+ subtitle.visibility = GONE
787
+ tertiary.visibility = GONE
788
+ status.visibility = GONE
789
+ metricSubtitle.visibility = GONE
790
+ badgeLine.visibility = GONE
791
+ badgeLine.background = null
792
+ badgeLine.setPadding(0, 0, 0, 0)
793
+ actionLine.visibility = GONE
794
+ actionViews.forEach {
795
+ it.visibility = GONE
796
+ it.background = null
797
+ it.setOnClickListener(null)
798
+ }
799
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
800
+ trailingColumn.orientation = VERTICAL
801
+ trailingColumn.layoutParams = wrap()
802
+ trailingViews.forEach {
803
+ it.visibility = GONE
804
+ it.gravity = Gravity.END
805
+ it.maxLines = 1
806
+ it.layoutParams = wrap()
807
+ it.background = null
808
+ it.setPadding(0, 0, 0, 0)
809
+ it.setOnClickListener(null)
810
+ }
811
+ trailingIcons.forEach {
812
+ it.visibility = GONE
813
+ it.layoutParams = LayoutParams(dp(24), dp(24)).apply { gravity = Gravity.END }
814
+ it.glyphSizeDp = null
815
+ it.alpha = 1f
816
+ it.isEnabled = true
817
+ it.setOnClickListener(null)
818
+ }
819
+ checkbox.visibility = GONE
820
+ checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
821
+ checkbox.alpha = 1f
822
+ checkbox.setOnClickListener(null)
823
+ spinner.visibility = GONE
824
+ spinner.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
825
+ spinner.alpha = 1f
826
+ leadingFrame.visibility = GONE
827
+ leadingFrame.background = null
828
+ leadingFrame.clipChildren = false
829
+ leadingFrame.clipToPadding = false
830
+ secondaryImage.visibility = GONE
831
+ secondaryImage.foreground = null
832
+ mediaNetworkImage.visibility = GONE
833
+ metricVisualImages.forEach {
834
+ (it.parent as? ViewGroup)?.removeView(it)
835
+ it.visibility = GONE
836
+ it.background = null
837
+ it.clipToOutline = false
838
+ it.outlineProvider = ViewOutlineProvider.BACKGROUND
839
+ }
840
+ unreadDot.visibility = GONE
841
+ mediaBadge.visibility = GONE
842
+ mediaBadge.text = ""
843
+ leadingIcon.visibility = GONE
844
+ leadingIcon.iconName = ""
845
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
846
+ favoriteIcon.visibility = GONE
847
+ favoriteIcon.iconName = ""
848
+ headerTitleIcon.visibility = GONE
849
+ headerTitleIcon.iconName = ""
850
+ headerValueIcon.visibility = GONE
851
+ headerValueIcon.iconName = ""
852
+ leadingActionIcon.visibility = GONE
853
+ leadingActionIcon.iconName = ""
854
+ leadingActionIcon.glyphSizeDp = 24
855
+ leadingActionIcon.setOnClickListener(null)
856
+ mainColumn.visibility = VISIBLE
857
+ dataContainer.visibility = GONE
858
+ dataColumns.forEach { it.visibility = GONE }
859
+ tableDataContainer.visibility = GONE
860
+ tableDataColumns.forEach {
861
+ it.reset()
862
+ it.visibility = GONE
863
+ }
864
+ leadingOverlayBackground.visibility = GONE
865
+ leadingCornerIconFrame.visibility = GONE
866
+ leadingCornerIcon.iconName = ""
867
+ leadingImages.forEach {
868
+ it.visibility = GONE
869
+ it.clipToOutline = false
870
+ it.outlineProvider = ViewOutlineProvider.BACKGROUND
871
+ }
872
+ leadingFallback.text = ""
873
+ leadingFallback.textSize = sp(13f)
874
+ leadingFallback.typeface = NativeListFonts.bold(context)
875
+ leadingFallback.visibility = GONE
876
+ leadingFallback.background = null
877
+ showsSeparator = false
878
+ mainColumn.removeView(skeletonPrimary)
879
+ mainColumn.removeView(skeletonSecondary)
880
+ setOnClickListener { view ->
881
+ (view.tag as? NativeListItem)?.let { item ->
882
+ onRowPress?.invoke(item, actionOrigin(view, "row"))
883
+ }
884
+ }
885
+ }
886
+
887
+ private fun restoreRestingBackground() {
888
+ background = if (reorderActive) pressedRowBackground else restingRowBackground
889
+ leadingFrame.alpha = 1f
890
+ }
891
+
892
+ fun setReorderActive(active: Boolean) {
893
+ if ((tag as? NativeListItem)?.type == "walletGroup" && reorderActive == active) return
894
+ reorderActive = active
895
+ if ((tag as? NativeListItem)?.type == "walletGroup") {
896
+ walletGroupExpandAnimator?.removeAllListeners()
897
+ walletGroupExpandAnimator?.cancel()
898
+ walletGroupExpandAnimator = null
899
+ walletGroupRows.forEachIndexed { index, row ->
900
+ row.alpha = 1f
901
+ row.visibility = if (active && index > 0) GONE else VISIBLE
902
+ if (index == 0) row.setReorderActive(active)
903
+ }
904
+ minimumHeight = if (active) dp(68) else walletGroupExpandedHeightPx
905
+ layoutParams = layoutParams?.apply {
906
+ height = ViewGroup.LayoutParams.WRAP_CONTENT
907
+ }
908
+ background = restingRowBackground
909
+ requestLayout()
910
+ invalidate()
911
+ return
912
+ }
913
+ restoreRestingBackground()
914
+ }
915
+
916
+ fun finishWalletGroupReorder(
917
+ durationMs: Long,
918
+ interpolator: TimeInterpolator,
919
+ completion: (() -> Unit)? = null,
920
+ ) {
921
+ if ((tag as? NativeListItem)?.type != "walletGroup") {
922
+ setReorderActive(false)
923
+ completion?.invoke()
924
+ return
925
+ }
926
+ reorderActive = false
927
+ walletGroupRows.firstOrNull()?.setReorderActive(false)
928
+ val startHeight = height.coerceAtLeast(dp(68))
929
+ val targetHeight = walletGroupExpandedHeightPx
930
+ minimumHeight = startHeight
931
+ layoutParams = layoutParams?.apply { height = startHeight }
932
+ walletGroupRows.drop(1).forEach { row ->
933
+ row.visibility = VISIBLE
934
+ row.alpha = 0f
935
+ }
936
+ background = restingRowBackground
937
+ if (targetHeight <= startHeight) {
938
+ walletGroupRows.forEach { row ->
939
+ row.visibility = VISIBLE
940
+ row.alpha = 1f
941
+ }
942
+ minimumHeight = targetHeight
943
+ layoutParams = layoutParams?.apply { height = ViewGroup.LayoutParams.WRAP_CONTENT }
944
+ requestLayout()
945
+ invalidate()
946
+ completion?.invoke()
947
+ return
948
+ }
949
+ walletGroupExpandAnimator = ValueAnimator.ofInt(startHeight, targetHeight).apply {
950
+ duration = durationMs
951
+ this.interpolator = interpolator
952
+ addUpdateListener { animator ->
953
+ val progress = animator.animatedFraction
954
+ layoutParams = layoutParams?.apply { height = animator.animatedValue as Int }
955
+ walletGroupRows.drop(1).forEach { it.alpha = progress }
956
+ requestLayout()
957
+ invalidate()
958
+ }
959
+ addListener(object : AnimatorListenerAdapter() {
960
+ override fun onAnimationEnd(animation: Animator) {
961
+ walletGroupExpandAnimator = null
962
+ walletGroupRows.forEach { row ->
963
+ row.visibility = VISIBLE
964
+ row.alpha = 1f
965
+ }
966
+ minimumHeight = targetHeight
967
+ layoutParams = layoutParams?.apply { height = ViewGroup.LayoutParams.WRAP_CONTENT }
968
+ requestLayout()
969
+ invalidate()
970
+ postOnAnimation {
971
+ if (!reorderActive) {
972
+ walletGroupRows.forEach { row ->
973
+ row.visibility = VISIBLE
974
+ row.alpha = 1f
975
+ }
976
+ requestLayout()
977
+ invalidate()
978
+ }
979
+ }
980
+ completion?.invoke()
981
+ }
982
+ })
983
+ start()
984
+ }
985
+ }
986
+
987
+ private fun bindWalletGroup(
988
+ item: NativeListItem,
989
+ theme: JSONObject?,
990
+ layout: String,
991
+ listOrientation: String,
992
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
993
+ ) {
994
+ orientation = VERTICAL
995
+ gravity = Gravity.CENTER_HORIZONTAL
996
+ setPadding(0, 0, 0, 0)
997
+ isClickable = false
998
+ setOnClickListener(null)
999
+ val members = mutableListOf<JSONObject>()
1000
+ members.add(item.json.getJSONObject("parent"))
1001
+ item.json.getJSONArray("children").let { children ->
1002
+ for (index in 0 until children.length()) {
1003
+ members.add(children.getJSONObject(index))
1004
+ }
1005
+ }
1006
+ walletGroupDragChildCount = members.size - 1
1007
+ walletGroupExpandedHeightPx =
1008
+ dp(members.size * 68 + walletGroupDragChildCount * 12)
1009
+ walletGroupDragBadgeBackgroundPaint.color = color(
1010
+ theme,
1011
+ "inverseBackground",
1012
+ "#000000DF",
1013
+ )
1014
+ walletGroupDragBadgeBorderPaint.color = color(
1015
+ theme,
1016
+ "rowBackground",
1017
+ "#FFFFFF",
1018
+ )
1019
+ walletGroupDragBadgeBorderPaint.strokeWidth = dp(1).toFloat()
1020
+ walletGroupDragBadgeTextPaint.color = color(
1021
+ theme,
1022
+ "inverseText",
1023
+ "#FCFCFC",
1024
+ )
1025
+ walletGroupDragBadgeTextPaint.textSize = TypedValue.applyDimension(
1026
+ TypedValue.COMPLEX_UNIT_SP,
1027
+ sp(12f),
1028
+ resources.displayMetrics,
1029
+ )
1030
+ while (walletGroupRows.size < members.size) {
1031
+ walletGroupRows.add(NativeListRowView(reactContext))
1032
+ }
1033
+ members.forEachIndexed { index, memberJson ->
1034
+ val member = NativeListItem.parse(memberJson)
1035
+ val memberRow = walletGroupRows[index]
1036
+ memberRow.onRowPress = { item, origin -> onRowPress?.invoke(item, origin) }
1037
+ memberRow.onAction = { source, actionKey, target, origin ->
1038
+ onAction?.invoke(source, actionKey, target, origin)
1039
+ }
1040
+ memberRow.onBindingInvalidated = { row, epoch ->
1041
+ onBindingInvalidated?.invoke(row, epoch)
1042
+ }
1043
+ memberRow.bind(
1044
+ member,
1045
+ theme,
1046
+ layout,
1047
+ listOrientation,
1048
+ null,
1049
+ memberJson.optBoolean("selected", false),
1050
+ checkboxState,
1051
+ )
1052
+ memberRow.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(68)).apply {
1053
+ if (index > 0) topMargin = dp(12)
1054
+ }
1055
+ addView(memberRow)
1056
+ }
1057
+ val fill = color(theme, "subduedBackground", "#F9F9F9")
1058
+ val stroke = color(theme, "separator", "#0000001F")
1059
+ restingRowBackground = GradientDrawable().apply {
1060
+ setColor(fill)
1061
+ setStroke(dp(1), stroke)
1062
+ cornerRadius = dp(12).toFloat()
1063
+ }
1064
+ pressedRowBackground = restingRowBackground
1065
+ background = restingRowBackground
1066
+ }
1067
+
1068
+ private fun bindIdentity(
1069
+ item: NativeListItem,
1070
+ theme: JSONObject?,
1071
+ selected: Boolean,
1072
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1073
+ ) {
1074
+ if (item.json.optString("presentation") == "walletSidebar") {
1075
+ orientation = VERTICAL
1076
+ gravity = Gravity.CENTER
1077
+ setPadding(dp(4), dp(4), dp(4), dp(4))
1078
+ addLeading(item.json.optJSONObject("leading"), 40, spacingDp = 0)
1079
+ leadingFallback.textSize = sp(28f)
1080
+ leadingFallback.typeface = NativeListFonts.regular(context)
1081
+ mainColumn.gravity = Gravity.CENTER
1082
+ titleLine.gravity = Gravity.CENTER
1083
+ titleLine.packsChildrenAtStart = false
1084
+ titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1085
+ title.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
1086
+ title.gravity = Gravity.CENTER
1087
+ showText(title, item.json.optString("title"), 1)
1088
+ title.setTextColor(
1089
+ color(
1090
+ theme,
1091
+ if (selected) "primaryText" else "secondaryText",
1092
+ if (selected) "#FFFFFFED" else "#FFFFFFAF",
1093
+ ),
1094
+ )
1095
+ addView(
1096
+ mainColumn,
1097
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1098
+ topMargin = dp(4)
1099
+ },
1100
+ )
1101
+ return
1102
+ }
1103
+ if (item.json.optString("presentation") == "networkSelector") {
1104
+ setPadding(dp(12), dp(7), dp(12), dp(8))
1105
+ }
1106
+ val leading = item.json.optJSONObject("leading")
1107
+ item.json.optJSONObject("leadingAction")?.let { action ->
1108
+ leadingActionIcon.visibility = VISIBLE
1109
+ leadingActionIcon.iconName = action.optString("name")
1110
+ leadingActionIcon.glyphSizeDp = 24
1111
+ leadingActionIcon.tintColor = safeColor(
1112
+ action.optString("tintColor"),
1113
+ color(theme, "icon", "#0000009B"),
1114
+ )
1115
+ leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
1116
+ leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
1117
+ leadingActionIcon.setOnClickListener {
1118
+ emitAction(
1119
+ item,
1120
+ action.optString("actionKey"),
1121
+ null,
1122
+ leadingActionIcon,
1123
+ "leadingAction",
1124
+ )
1125
+ }
1126
+ // ListItem.IconButton is 36dp with 6dp inner padding and m=-7. Keep the
1127
+ // full button frame but absorb its leading negative margin into the row
1128
+ // padding and its trailing margin into the following gap.
1129
+ setPadding(dp(5), paddingTop, paddingRight, paddingBottom)
1130
+ addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
1131
+ }
1132
+ addLeading(
1133
+ leading,
1134
+ if (
1135
+ leading?.optString("kind") == "network" ||
1136
+ item.json.optString("presentation") == "accountSelector"
1137
+ ) 32 else 40,
1138
+ )
1139
+ addView(mainColumn, weighted())
1140
+ titleLine.packsChildrenAtStart = true
1141
+ title.ellipsize = TextUtils.TruncateAt.END
1142
+ showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
1143
+ if (item.json.optString("presentation") == "accountSelector") {
1144
+ title.typeface = NativeListFonts.regular(context)
1145
+ }
1146
+ showText(subtitle, item.json.optString("subtitle"), item.json.optInt("subtitleLines", 2))
1147
+ showText(tertiary, item.json.optString("tertiary"), 1)
1148
+ tertiary.setTextColor(
1149
+ color(
1150
+ theme,
1151
+ if (item.json.optString("tertiaryTone") == "info") "info" else "secondaryText",
1152
+ if (item.json.optString("tertiaryTone") == "info") "#006DCBF2" else "#0000009B",
1153
+ ),
1154
+ )
1155
+ val badges = item.json.optJSONArray("badges")
1156
+ if (badges != null && badges.length() > 0) {
1157
+ val texts = (0 until minOf(2, badges.length())).map { badges.getJSONObject(it).optString("text") }
1158
+ showText(badgeLine, texts.joinToString(" "), 1)
1159
+ badgeLine.layoutParams = wrap().apply { marginStart = dp(8) }
1160
+ badgeLine.setTextColor(color(theme, "secondaryText", "#0000009B"))
1161
+ badgeLine.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 4f)
1162
+ badgeLine.setPadding(dp(8), dp(2), dp(8), dp(2))
1163
+ TextViewCompat.setLineHeight(badgeLine, dp(16))
1164
+ }
1165
+ addView(trailingColumn, wrap())
1166
+ val accessories = item.json.optJSONArray("trailing")
1167
+ if (accessories.hasAccessory("checkbox") && accessories.hasAccessory("value")) {
1168
+ trailingColumn.orientation = HORIZONTAL
1169
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1170
+ checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginStart = dp(12) }
1171
+ } else if (accessories.isValuePairMenu()) {
1172
+ trailingColumn.orientation = HORIZONTAL
1173
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1174
+ } else if (accessories.isBookmarkEditActions()) {
1175
+ trailingColumn.orientation = HORIZONTAL
1176
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1177
+ }
1178
+ bindAccessories(item, accessories, theme, checkboxState)
1179
+ }
1180
+
1181
+ private fun bindRail(item: NativeListItem, theme: JSONObject?) {
1182
+ setPadding(dp(4), dp(4), dp(4), dp(4))
1183
+ addLeading(item.json.optJSONObject("visual"), 20, spacingDp = 6)
1184
+ showText(title, item.json.optString("title"), 1)
1185
+ TextViewCompat.setLineHeight(title, dp(16))
1186
+ title.fontFeatureSettings = "tnum"
1187
+ title.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
1188
+ titleLine.layoutParams = wrap()
1189
+ mainColumn.orientation = HORIZONTAL
1190
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
1191
+ mainColumn.layoutParams = wrap()
1192
+ title.setHorizontallyScrolling(true)
1193
+ item.json.optJSONObject("badge")?.let { badge ->
1194
+ showText(badgeLine, badge.optString("text"), 1)
1195
+ badgeLine.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
1196
+ marginStart = dp(6)
1197
+ }
1198
+ badgeLine.setTextColor(
1199
+ when (badge.optString("tone")) {
1200
+ "success" -> color(theme, "positive", "#00713FDE")
1201
+ "danger" -> color(theme, "negative", "#C40006D3")
1202
+ else -> color(theme, "secondaryText", "#0000009B")
1203
+ },
1204
+ )
1205
+ badgeLine.typeface = NativeListFonts.medium(context)
1206
+ badgeLine.fontFeatureSettings = "tnum"
1207
+ TextViewCompat.setLineHeight(badgeLine, dp(16))
1208
+ }
1209
+ item.json.optString("status").takeUnless { it.isEmpty() || it == "none" }
1210
+ ?.let { showText(status, it, 1) }
1211
+ addView(mainColumn, wrap())
1212
+ }
1213
+
1214
+ private fun bindActivity(item: NativeListItem, theme: JSONObject?) {
1215
+ addLeading(
1216
+ item.json.optJSONObject("leading"),
1217
+ secondaryVisual = item.json.optJSONObject("secondaryLeading"),
1218
+ )
1219
+ addView(mainColumn, weighted())
1220
+ showText(title, item.json.optString("title"), 1)
1221
+ showText(subtitle, item.json.optString("description"), 2)
1222
+ TextViewCompat.setLineHeight(title, dp(24))
1223
+ TextViewCompat.setLineHeight(subtitle, dp(20))
1224
+ if (item.json.optString("status") == "Failed") {
1225
+ showText(badgeLine, "Failed", 1)
1226
+ badgeLine.setTextColor(color(theme, "negative", "#C40006D3"))
1227
+ badgeLine.background = roundedFill(
1228
+ color(theme, "criticalBackground", "#F3000D14"),
1229
+ 4f,
1230
+ )
1231
+ badgeLine.setPadding(dp(8), dp(2), dp(8), dp(2))
1232
+ } else {
1233
+ showText(status, item.json.optString("status"), 1)
1234
+ }
1235
+ addView(trailingColumn, wrap())
1236
+ showTrailing(0, item.json.optString("primaryAmount"), true)
1237
+ showTrailing(1, item.json.optString("secondaryAmount"), false)
1238
+ if (item.json.optString("primaryAmount").startsWith("+")) {
1239
+ trailingViews[0].setTextColor(color(theme, "positive", "#00713FDE"))
1240
+ }
1241
+ val actions = item.json.optJSONArray("footerActions")
1242
+ if (actions != null && actions.length() > 0) {
1243
+ actionLine.visibility = VISIBLE
1244
+ for (index in 0 until minOf(3, actions.length())) {
1245
+ val action = actions.getJSONObject(index)
1246
+ val actionView = actionViews[index]
1247
+ actionView.text = action.optString("label")
1248
+ actionView.visibility = VISIBLE
1249
+ actionView.isEnabled = isEnabled && !action.optBoolean("disabled", false)
1250
+ actionView.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 8f)
1251
+ actionView.setTextColor(
1252
+ if (action.optString("tone") == "danger") {
1253
+ color(theme, "negative", "#C40006D3")
1254
+ } else {
1255
+ color(theme, "primaryText", "#000000DF")
1256
+ },
1257
+ )
1258
+ actionView.layoutParams = LayoutParams(
1259
+ LayoutParams.WRAP_CONTENT,
1260
+ LayoutParams.WRAP_CONTENT,
1261
+ ).apply { marginEnd = dp(8) }
1262
+ actionView.setOnClickListener {
1263
+ emitAction(item, action.optString("key"), null, actionView, "footerAction", index)
1264
+ }
1265
+ }
1266
+ // TxActionCommonListView is one column ListItem with an 8dp gap between
1267
+ // its content XStack and the pending action footer.
1268
+ removeView(leadingFrame)
1269
+ removeView(mainColumn)
1270
+ removeView(trailingColumn)
1271
+ mainColumn.removeView(actionLine)
1272
+ activityContentRow.orientation = HORIZONTAL
1273
+ activityContentRow.gravity = Gravity.CENTER_VERTICAL
1274
+ activityContentRow.addView(leadingFrame)
1275
+ activityContentRow.addView(mainColumn)
1276
+ activityContentRow.addView(trailingColumn)
1277
+ orientation = VERTICAL
1278
+ gravity = Gravity.START
1279
+ addView(activityContentRow, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
1280
+ addView(actionLine, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
1281
+ marginStart = if (leadingFrame.visibility == VISIBLE) dp(52) else 0
1282
+ topMargin = dp(8)
1283
+ bottomMargin = dp(4)
1284
+ })
1285
+ }
1286
+ }
1287
+
1288
+ private fun bindMessage(item: NativeListItem, theme: JSONObject?) {
1289
+ gravity = Gravity.TOP
1290
+ setPadding(dp(12), dp(16), dp(12), dp(16))
1291
+ item.json.optJSONObject("leading")?.let { addLeading(it, 28) }
1292
+ unreadDot.visibility = if (item.json.optBoolean("unread", false)) VISIBLE else GONE
1293
+ addView(mainColumn, weighted())
1294
+ showText(title, item.json.optString("title"), 2)
1295
+ showText(subtitle, item.json.optString("body"), item.json.optInt("bodyLines", 3).coerceIn(1, 3))
1296
+ showText(status, item.json.optString("time"), 1)
1297
+ TextViewCompat.setLineHeight(title, dp(20))
1298
+ TextViewCompat.setLineHeight(subtitle, dp(20))
1299
+ TextViewCompat.setLineHeight(status, dp(16))
1300
+ subtitle.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1301
+ topMargin = dp(2)
1302
+ }
1303
+ status.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1304
+ topMargin = dp(4)
1305
+ }
1306
+ status.setTextColor(color(theme, "disabledText", "#00000072"))
1307
+ item.json.optJSONObject("thumbnail")?.let { source ->
1308
+ secondaryImage.visibility = VISIBLE
1309
+ secondaryImage.foreground = roundedHairlineStroke(
1310
+ color(theme, "strongBackground", "#0000000F"),
1311
+ 6f,
1312
+ )
1313
+ addView(secondaryImage, LayoutParams(dp(64), dp(64)).apply { marginStart = dp(12) })
1314
+ bindImage(source, secondaryImage, item.key, 0, "generic")
1315
+ }
1316
+ }
1317
+
1318
+ private fun bindDataRow(
1319
+ item: NativeListItem,
1320
+ theme: JSONObject?,
1321
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1322
+ ) {
1323
+ if (
1324
+ item.json.optBoolean("favorite", false) ||
1325
+ item.json.optBoolean("favoriteActive", false)
1326
+ ) {
1327
+ val favoriteActive = item.json.optBoolean("favoriteActive", false)
1328
+ favoriteIcon.visibility = VISIBLE
1329
+ favoriteIcon.iconName = if (favoriteActive) "StarSolid" else "StarOutline"
1330
+ favoriteIcon.tintColor = color(
1331
+ theme,
1332
+ if (favoriteActive) "icon" else "iconSubdued",
1333
+ if (favoriteActive) "#0000009B" else "#00000072",
1334
+ )
1335
+ addView(favoriteIcon, LayoutParams(dp(20), dp(20)).apply {
1336
+ marginEnd = dp(if (currentLayout == "table") 8 else 12)
1337
+ })
1338
+ }
1339
+ item.json.optJSONObject("leading")?.let {
1340
+ addLeading(it, 40, spacingDp = if (currentLayout == "table") 10 else 12)
1341
+ }
1342
+ var hasLeadingAccessory = false
1343
+ item.json.optJSONObject("checkbox")?.let {
1344
+ bindCheckbox(item, it, checkboxState)
1345
+ hasLeadingAccessory = true
1346
+ }
1347
+ if (item.json.has("index")) {
1348
+ val indexView = trailingViews[0]
1349
+ indexView.text = item.json.optInt("index").toString()
1350
+ indexView.visibility = VISIBLE
1351
+ hasLeadingAccessory = true
1352
+ }
1353
+ if (hasLeadingAccessory) {
1354
+ addView(trailingColumn, LayoutParams(dp(32), LayoutParams.WRAP_CONTENT).apply { marginEnd = dp(10) })
1355
+ }
1356
+ val columns = item.json.getJSONArray("columns")
1357
+ val rowBadges = item.json.optJSONArray("badges")
1358
+ if (currentLayout == "table") {
1359
+ tableDataContainer.visibility = VISIBLE
1360
+ for (index in 0 until minOf(4, columns.length())) {
1361
+ val column = columns.getJSONObject(index)
1362
+ tableDataColumns[index].visibility = VISIBLE
1363
+ tableDataColumns[index].bind(
1364
+ column = column,
1365
+ rowBadges = if (index == 0) rowBadges else null,
1366
+ primaryColor = dataTextColor(column.optString("tone"), theme),
1367
+ secondaryColor = dataTextColor(
1368
+ column.optString("secondaryTone", "secondary"),
1369
+ theme,
1370
+ ),
1371
+ infoColor = color(theme, "info", "#006DCBF2"),
1372
+ )
1373
+ tableDataColumns[index].layoutParams = LayoutParams(
1374
+ 0,
1375
+ dp(40),
1376
+ column.optInt("weight", 1).toFloat(),
1377
+ )
1378
+ }
1379
+ addView(tableDataContainer, weighted())
1380
+ return
1381
+ }
1382
+
1383
+ dataContainer.visibility = VISIBLE
1384
+ for (index in 0 until minOf(4, columns.length())) {
1385
+ val column = columns.getJSONObject(index)
1386
+ val view = dataColumns[index]
1387
+ view.text = styledDataText(column, if (index == 0) rowBadges else null, theme)
1388
+ view.maxLines = if (column.optString("secondaryText").isEmpty()) 1 else 2
1389
+ view.gravity = when (column.optString("alignment", "start")) {
1390
+ "center" -> Gravity.CENTER
1391
+ "end" -> Gravity.END
1392
+ else -> Gravity.START
1393
+ }
1394
+ view.visibility = VISIBLE
1395
+ view.setTextColor(
1396
+ when (column.optString("tone", "primary")) {
1397
+ "secondary" -> color(theme, "secondaryText", "#0000009B")
1398
+ "positive" -> color(theme, "positive", "#00713FDE")
1399
+ "negative" -> color(theme, "negative", "#C40006D3")
1400
+ else -> color(theme, "primaryText", "#000000DF")
1401
+ },
1402
+ )
1403
+ view.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, column.optInt("weight", 1).toFloat())
1404
+ }
1405
+ addView(dataContainer, weighted())
1406
+ }
1407
+
1408
+ private fun styledDataText(
1409
+ column: JSONObject,
1410
+ badges: JSONArray?,
1411
+ theme: JSONObject?,
1412
+ ): CharSequence {
1413
+ val result = SpannableStringBuilder(column.optString("text"))
1414
+ result.setSpan(
1415
+ ForegroundColorSpan(dataTextColor(column.optString("tone"), theme)),
1416
+ 0,
1417
+ result.length,
1418
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
1419
+ )
1420
+ if (badges != null) {
1421
+ for (index in 0 until minOf(2, badges.length())) {
1422
+ val start = result.length
1423
+ result.append(" ${badges.getJSONObject(index).optString("text")} ")
1424
+ result.setSpan(
1425
+ ForegroundColorSpan(color(theme, "info", "#006DCBF2")),
1426
+ start,
1427
+ result.length,
1428
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
1429
+ )
1430
+ result.setSpan(
1431
+ BackgroundColorSpan(parseNativeListColor("#008FF519")),
1432
+ start,
1433
+ result.length,
1434
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
1435
+ )
1436
+ }
1437
+ }
1438
+ column.optString("secondaryText").takeIf(String::isNotEmpty)?.let { secondary ->
1439
+ val start = result.length
1440
+ result.append("\n$secondary")
1441
+ result.setSpan(
1442
+ ForegroundColorSpan(dataTextColor(column.optString("secondaryTone", "secondary"), theme)),
1443
+ start,
1444
+ result.length,
1445
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
1446
+ )
1447
+ }
1448
+ return result
1449
+ }
1450
+
1451
+ private fun dataTextColor(tone: String, theme: JSONObject?): Int = when (tone.ifEmpty { "primary" }) {
1452
+ "secondary" -> color(theme, "secondaryText", "#0000009B")
1453
+ "positive" -> color(theme, "positive", "#00713FDE")
1454
+ "negative" -> color(theme, "negative", "#C40006D3")
1455
+ else -> color(theme, "primaryText", "#000000DF")
1456
+ }
1457
+
1458
+ private fun bindMediaTile(item: NativeListItem, theme: JSONObject?) {
1459
+ isMediaTile = true
1460
+ orientation = VERTICAL
1461
+ gravity = Gravity.START
1462
+ setPadding(dp(10), dp(10), dp(10), dp(10))
1463
+ val imageState = item.json.optString("imageState", "loaded")
1464
+ when (imageState) {
1465
+ "empty" -> {
1466
+ addLeading(null, 160)
1467
+ leadingFrame.background = roundedFill(Color.WHITE, 10f)
1468
+ }
1469
+ "error" -> {
1470
+ addLeading(null, 160)
1471
+ leadingFallback.visibility = VISIBLE
1472
+ leadingFallback.background = roundedFill(
1473
+ color(theme, "strongBackground", "#0000000F"),
1474
+ 10f,
1475
+ )
1476
+ leadingIcon.visibility = VISIBLE
1477
+ leadingIcon.iconName = "ImageSquareWavesOutline"
1478
+ leadingIcon.tintColor = parseNativeListColor("#00000044")
1479
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
1480
+ }
1481
+ else -> {
1482
+ addLeading(
1483
+ JSONObject().put("kind", "image").put("image", item.json.getJSONObject("image")),
1484
+ 160,
1485
+ )
1486
+ }
1487
+ }
1488
+ leadingFrame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(160))
1489
+ leadingFallback.layoutParams = FrameLayout.LayoutParams(
1490
+ LayoutParams.MATCH_PARENT,
1491
+ LayoutParams.MATCH_PARENT,
1492
+ )
1493
+ leadingImages[0].layoutParams = FrameLayout.LayoutParams(
1494
+ LayoutParams.MATCH_PARENT,
1495
+ LayoutParams.MATCH_PARENT,
1496
+ )
1497
+ leadingImages[0].outlineProvider = object : ViewOutlineProvider() {
1498
+ override fun getOutline(view: View, outline: Outline) {
1499
+ outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
1500
+ }
1501
+ }
1502
+ leadingImages[0].clipToOutline = true
1503
+ mainColumn.removeView(titleLine)
1504
+ mainColumn.removeView(subtitle)
1505
+ mediaMetadataRow.orientation = HORIZONTAL
1506
+ mediaMetadataRow.gravity = Gravity.CENTER_VERTICAL
1507
+ mediaMetadataRow.addView(subtitle, weighted().apply { marginEnd = dp(8) })
1508
+ item.json.optJSONObject("networkImage")?.let { networkImage ->
1509
+ mediaNetworkImage.visibility = VISIBLE
1510
+ mediaNetworkImage.outlineProvider = circleOutlineProvider
1511
+ mediaNetworkImage.clipToOutline = true
1512
+ mediaMetadataRow.addView(mediaNetworkImage, LayoutParams(dp(14), dp(14)))
1513
+ bindImage(networkImage, mediaNetworkImage, item.key, 2, "network")
1514
+ }
1515
+ mainColumn.addView(mediaMetadataRow, 0)
1516
+ mainColumn.addView(titleLine, 1)
1517
+ showText(subtitle, item.json.optString("subtitle"), 1)
1518
+ showText(title, item.json.optString("title"), 1)
1519
+ item.json.optJSONObject("badge")?.optString("text")?.let { value ->
1520
+ mediaBadge.text = value
1521
+ mediaBadge.setTextColor(parseNativeListColor("#FCFCFC"))
1522
+ mediaBadge.textSize = sp(14f)
1523
+ mediaBadge.background = roundedStroke(
1524
+ Color.WHITE,
1525
+ parseNativeListColor("#000000DF"),
1526
+ 10f,
1527
+ )
1528
+ mediaBadge.setPadding(dp(8), 0, dp(8), 0)
1529
+ mediaBadge.layoutParams = FrameLayout.LayoutParams(
1530
+ LayoutParams.WRAP_CONTENT,
1531
+ dp(24),
1532
+ Gravity.END or Gravity.BOTTOM,
1533
+ )
1534
+ mediaBadge.visibility = VISIBLE
1535
+ }
1536
+ addView(mainColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
1537
+ topMargin = dp(8)
1538
+ })
1539
+ item.json.optString("closeActionKey").takeIf { it.isNotEmpty() }?.let { actionKey ->
1540
+ val close = trailingViews[0]
1541
+ close.text = "×"
1542
+ close.textSize = sp(22f)
1543
+ close.gravity = Gravity.END
1544
+ close.visibility = VISIBLE
1545
+ close.setOnClickListener {
1546
+ emitAction(item, actionKey, null, close, "mediaClose")
1547
+ }
1548
+ addView(trailingColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
1549
+ }
1550
+ }
1551
+
1552
+ private fun bindMetricCard(item: NativeListItem, theme: JSONObject?) {
1553
+ orientation = VERTICAL
1554
+ gravity = Gravity.START
1555
+ setPadding(dp(14), dp(14), dp(14), dp(14))
1556
+ val variant = item.json.optString("variant", "standard")
1557
+ if (variant == "activity" || variant == "performance") {
1558
+ bindCompositeMetricCard(item, theme, variant)
1559
+ return
1560
+ }
1561
+ item.json.optJSONObject("visual")?.let {
1562
+ addLeading(it, 32)
1563
+ leadingFrame.clipChildren = true
1564
+ leadingFrame.clipToPadding = true
1565
+ }
1566
+ showText(subtitle, item.json.optString("title"), 1)
1567
+ subtitle.setTextColor(color(theme, "disabledText", "#00000072"))
1568
+ showText(title, item.json.optString("value"), 1)
1569
+ title.textSize = sp(if (item.json.optString("size") == "large") 24f else 18f)
1570
+ title.typeface = NativeListFonts.semibold(context)
1571
+ showText(status, item.json.optString("trend"), 1)
1572
+ status.setTextColor(
1573
+ when (item.json.optString("trendTone", "neutral")) {
1574
+ "positive" -> color(theme, "positive", "#00713FDE")
1575
+ "negative" -> color(theme, "negative", "#C40006D3")
1576
+ else -> color(theme, "secondaryText", "#0000009B")
1577
+ },
1578
+ )
1579
+ showText(metricSubtitle, item.json.optString("subtitle"), 1)
1580
+ item.json.optJSONObject("badge")?.optString("text")?.let { showText(badgeLine, it, 1) }
1581
+ addView(mainColumn, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
1582
+ }
1583
+
1584
+ private fun bindCompositeMetricCard(item: NativeListItem, theme: JSONObject?, variant: String) {
1585
+ val metrics = item.json.optJSONArray("metrics") ?: JSONArray()
1586
+ addView(
1587
+ metricText(
1588
+ value = item.json.optString("title").uppercase(),
1589
+ size = 11f,
1590
+ lineHeight = 14,
1591
+ typeface = NativeListFonts.regular(context),
1592
+ textColor = color(theme, "disabledText", "#00000072"),
1593
+ letterSpacingDp = 1.2f,
1594
+ ),
1595
+ weightedWidth(),
1596
+ )
1597
+ if (variant == "activity") {
1598
+ addView(activityHeroMetrics(metrics, theme), weightedWidth().apply {
1599
+ topMargin = dp(14)
1600
+ })
1601
+ addView(View(context).apply {
1602
+ setBackgroundColor(color(theme, "separator", "#0000001F"))
1603
+ }, LayoutParams(LayoutParams.MATCH_PARENT, 1).apply {
1604
+ topMargin = dp(14)
1605
+ bottomMargin = dp(14)
1606
+ })
1607
+ addView(activityCompactMetrics(metrics, theme), weightedWidth())
1608
+ } else {
1609
+ val winRateRow = LinearLayout(context).apply {
1610
+ orientation = HORIZONTAL
1611
+ gravity = Gravity.BOTTOM
1612
+ if (metrics.length() > 0) {
1613
+ addView(
1614
+ makeMetricColumn(
1615
+ metric = metrics.getJSONObject(0),
1616
+ visualSlot = 0,
1617
+ theme = theme,
1618
+ alignment = Gravity.START,
1619
+ valueSize = 18f,
1620
+ valueLineHeight = 24,
1621
+ valueTypeface = NativeListFonts.semibold(context),
1622
+ ),
1623
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
1624
+ )
1625
+ }
1626
+ if (metrics.length() > 1) {
1627
+ addView(
1628
+ makeMetricColumn(
1629
+ metric = metrics.getJSONObject(1),
1630
+ visualSlot = 1,
1631
+ theme = theme,
1632
+ alignment = Gravity.END,
1633
+ valueSize = 14f,
1634
+ valueLineHeight = 20,
1635
+ valueTypeface = NativeListFonts.semibold(context),
1636
+ ),
1637
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
1638
+ )
1639
+ }
1640
+ }
1641
+ addView(winRateRow, weightedWidth().apply { topMargin = dp(14) })
1642
+ val progressValue = item.json.optDouble("progress", 0.0).coerceIn(0.0, 1.0)
1643
+ val progress = LinearLayout(context).apply {
1644
+ orientation = HORIZONTAL
1645
+ clipToOutline = true
1646
+ outlineProvider = object : ViewOutlineProvider() {
1647
+ override fun getOutline(view: View, outline: Outline) {
1648
+ outline.setRoundRect(0, 0, view.width, view.height, dp(2).toFloat())
1649
+ }
1650
+ }
1651
+ if (progressValue > 0.0) {
1652
+ addView(View(context).apply {
1653
+ setBackgroundColor(parseNativeListColor("#22AB15"))
1654
+ }, LayoutParams(0, LayoutParams.MATCH_PARENT, progressValue.toFloat()))
1655
+ }
1656
+ if (progressValue < 1.0) {
1657
+ addView(View(context).apply {
1658
+ setBackgroundColor(parseNativeListColor("#E5484D"))
1659
+ }, LayoutParams(0, LayoutParams.MATCH_PARENT, (1.0 - progressValue).toFloat()))
1660
+ }
1661
+ }
1662
+ addView(progress, LayoutParams(LayoutParams.MATCH_PARENT, dp(4)).apply {
1663
+ topMargin = dp(8)
1664
+ })
1665
+ addView(performanceAverageMetrics(metrics, theme), weightedWidth().apply {
1666
+ topMargin = dp(14)
1667
+ })
1668
+ }
1669
+ }
1670
+
1671
+ private fun activityHeroMetrics(metrics: JSONArray, theme: JSONObject?) =
1672
+ LinearLayout(context).apply {
1673
+ orientation = HORIZONTAL
1674
+ gravity = Gravity.BOTTOM
1675
+ if (metrics.length() > 0) {
1676
+ addView(
1677
+ makeMetricColumn(
1678
+ metric = metrics.getJSONObject(0),
1679
+ visualSlot = 0,
1680
+ theme = theme,
1681
+ alignment = Gravity.START,
1682
+ valueSize = 16f,
1683
+ valueLineHeight = 24,
1684
+ valueTypeface = NativeListFonts.semibold(context),
1685
+ ),
1686
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
1687
+ )
1688
+ }
1689
+ if (metrics.length() > 1) {
1690
+ addView(
1691
+ makeMetricColumn(
1692
+ metric = metrics.getJSONObject(1),
1693
+ visualSlot = 1,
1694
+ theme = theme,
1695
+ alignment = Gravity.END,
1696
+ valueSize = 14f,
1697
+ valueLineHeight = 20,
1698
+ valueTypeface = NativeListFonts.semibold(context),
1699
+ ),
1700
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
1701
+ )
1702
+ }
1703
+ }
1704
+
1705
+ private fun activityCompactMetrics(metrics: JSONArray, theme: JSONObject?) =
1706
+ LinearLayout(context).apply {
1707
+ orientation = HORIZONTAL
1708
+ gravity = Gravity.CENTER_VERTICAL
1709
+ for (index in 2 until metrics.length()) {
1710
+ val alignment = when (index) {
1711
+ 2 -> Gravity.START
1712
+ metrics.length() - 1 -> Gravity.END
1713
+ else -> Gravity.CENTER_HORIZONTAL
1714
+ }
1715
+ addView(
1716
+ makeMetricColumn(
1717
+ metric = metrics.getJSONObject(index),
1718
+ visualSlot = index,
1719
+ theme = theme,
1720
+ alignment = alignment,
1721
+ valueSize = 14f,
1722
+ valueLineHeight = 20,
1723
+ valueTypeface = NativeListFonts.medium(context),
1724
+ ),
1725
+ LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f),
1726
+ )
1727
+ }
1728
+ }
1729
+
1730
+ private fun performanceAverageMetrics(metrics: JSONArray, theme: JSONObject?) =
1731
+ LinearLayout(context).apply {
1732
+ orientation = HORIZONTAL
1733
+ val indices = listOf(2, metrics.length() - 1).distinct().filter { it in 0 until metrics.length() }
1734
+ indices.forEachIndexed { position, index ->
1735
+ val metric = metrics.getJSONObject(index)
1736
+ val card = makeMetricColumn(
1737
+ metric = metric,
1738
+ visualSlot = index,
1739
+ theme = theme,
1740
+ alignment = Gravity.START,
1741
+ valueSize = 14f,
1742
+ valueLineHeight = 20,
1743
+ valueTypeface = NativeListFonts.medium(context),
1744
+ ).apply {
1745
+ setPadding(dp(10), dp(10), dp(10), dp(10))
1746
+ background = roundedFill(color(theme, "strongBackground", "#0000000F"), 8f)
1747
+ }
1748
+ addView(card, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f).apply {
1749
+ if (position > 0) marginStart = dp(8)
1750
+ })
1751
+ }
1752
+ }
1753
+
1754
+ private fun makeMetricColumn(
1755
+ metric: JSONObject,
1756
+ visualSlot: Int,
1757
+ theme: JSONObject?,
1758
+ alignment: Int,
1759
+ valueSize: Float,
1760
+ valueLineHeight: Int,
1761
+ valueTypeface: Typeface,
1762
+ ) = LinearLayout(context).apply {
1763
+ orientation = VERTICAL
1764
+ gravity = alignment
1765
+ addView(
1766
+ metricText(
1767
+ value = metric.optString("label"),
1768
+ size = 11f,
1769
+ lineHeight = 14,
1770
+ typeface = NativeListFonts.regular(context),
1771
+ textColor = color(theme, "disabledText", "#00000072"),
1772
+ gravity = alignment,
1773
+ ),
1774
+ )
1775
+ val valueText = metricText(
1776
+ value = metric.optString("value"),
1777
+ size = valueSize,
1778
+ lineHeight = valueLineHeight,
1779
+ typeface = valueTypeface,
1780
+ textColor = dataTextColor(metric.optString("tone"), theme),
1781
+ gravity = alignment,
1782
+ )
1783
+ val valueVisual = metric.optJSONObject("visual")
1784
+ val valueView = if (valueVisual != null && visualSlot in metricVisualImages.indices) {
1785
+ LinearLayout(context).apply {
1786
+ orientation = HORIZONTAL
1787
+ gravity = Gravity.CENTER_VERTICAL
1788
+ val image = metricVisualImages[visualSlot]
1789
+ image.visibility = VISIBLE
1790
+ image.outlineProvider = circleOutlineProvider
1791
+ image.clipToOutline = true
1792
+ valueVisual.optString("backgroundColor").takeIf(String::isNotEmpty)?.let { backgroundColor ->
1793
+ image.background = roundedFill(
1794
+ safeColor(backgroundColor, Color.WHITE),
1795
+ 8f,
1796
+ )
1797
+ }
1798
+ visualSources(valueVisual).firstOrNull()?.let { (source, variant) ->
1799
+ bindImage(source, image, boundKey ?: "metric", visualSlot, variant)
1800
+ }
1801
+ addView(image, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
1802
+ addView(valueText, wrap())
1803
+ }
1804
+ } else {
1805
+ valueText
1806
+ }
1807
+ addView(
1808
+ valueView,
1809
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
1810
+ topMargin = dp(2)
1811
+ },
1812
+ )
1813
+ }
1814
+
1815
+ private fun metricText(
1816
+ value: String,
1817
+ size: Float,
1818
+ lineHeight: Int,
1819
+ typeface: Typeface,
1820
+ textColor: Int,
1821
+ letterSpacingDp: Float = 0f,
1822
+ gravity: Int = Gravity.START,
1823
+ ) = TextView(context).apply {
1824
+ includeFontPadding = false
1825
+ text = value
1826
+ textSize = sp(size)
1827
+ this.typeface = typeface
1828
+ setTextColor(textColor)
1829
+ this.gravity = gravity
1830
+ maxLines = 1
1831
+ ellipsize = TextUtils.TruncateAt.END
1832
+ fontFeatureSettings = "tnum"
1833
+ TextViewCompat.setLineHeight(this, dp(lineHeight))
1834
+ if (letterSpacingDp > 0f) {
1835
+ letterSpacing = letterSpacingDp / sp(size)
1836
+ }
1837
+ }
1838
+
1839
+ private fun weightedWidth() = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1840
+
1841
+ private fun bindSectionHeader(
1842
+ item: NativeListItem,
1843
+ theme: JSONObject?,
1844
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1845
+ ) {
1846
+ val variant = item.json.optString("variant")
1847
+ val isSummary = variant == "summary"
1848
+ val isGallery = variant == "gallery"
1849
+ val isTable = currentLayout == "table"
1850
+ val isNetworkSelector = item.json.optString("presentation") == "networkSelector"
1851
+ val isHistory = variant == "history" || item.sectionKey?.startsWith("history-") == true
1852
+ val isTokenManager = item.sectionKey in setOf("linear-tokens", "action-tokens")
1853
+ val hasDottedTitle = isSummary || isNetworkSelector ||
1854
+ (item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null)
1855
+ // Linear/sectioned snapshots reserve the ListItem mx=8 at RecyclerView
1856
+ // level, so header-local insets below are source px minus that outer inset.
1857
+ val headerHorizontalInset = 12
1858
+ addView(mainColumn, weighted())
1859
+ showText(title, item.json.optString("title"), 1)
1860
+ if (hasDottedTitle) {
1861
+ title.showsDottedUnderline = true
1862
+ title.dottedUnderlineColor = color(theme, "secondaryText", "#0000009B")
1863
+ title.setPadding(0, 0, 0, dp(3))
1864
+ }
1865
+ title.setTextColor(
1866
+ color(
1867
+ theme,
1868
+ if (isSummary || isGallery) "primaryText" else "secondaryText",
1869
+ if (isSummary || isGallery) "#000000DF" else "#0000009B",
1870
+ ),
1871
+ )
1872
+ if (isNetworkSelector) {
1873
+ setPadding(dp(headerHorizontalInset), dp(12), dp(headerHorizontalInset), dp(12))
1874
+ title.textSize = sp(14f)
1875
+ title.typeface = NativeListFonts.medium(context)
1876
+ TextViewCompat.setLineHeight(title, dp(20))
1877
+ } else if (isHistory) {
1878
+ setPadding(0, 0, 0, 0)
1879
+ title.text = item.json.optString("title").uppercase()
1880
+ title.textSize = sp(12f)
1881
+ title.typeface = NativeListFonts.semibold(context)
1882
+ title.letterSpacing = 0.8f / sp(12f)
1883
+ TextViewCompat.setLineHeight(title, dp(16))
1884
+ } else if (isTokenManager) {
1885
+ setPadding(dp(headerHorizontalInset), dp(10), dp(headerHorizontalInset), 0)
1886
+ title.textSize = sp(14f)
1887
+ title.typeface = NativeListFonts.regular(context)
1888
+ TextViewCompat.setLineHeight(title, dp(20))
1889
+ } else if (!isTable && !isGallery && !isSummary) {
1890
+ // SectionList.SectionHeader: h=36, px=20, headingSm 14/20 semibold.
1891
+ setPadding(dp(headerHorizontalInset), dp(8), dp(headerHorizontalInset), dp(8))
1892
+ TextViewCompat.setLineHeight(title, dp(20))
1893
+ } else if (isTable) {
1894
+ setPadding(dp(16), dp(12), dp(16), dp(2))
1895
+ title.includeFontPadding = false
1896
+ title.layoutParams = wrap()
1897
+ titleLine.layoutParams = wrap()
1898
+ item.json.optJSONObject("titleIcon")?.let { icon ->
1899
+ headerTitleIcon.visibility = VISIBLE
1900
+ headerTitleIcon.iconName = icon.optString("name")
1901
+ headerTitleIcon.tintColor = safeColor(
1902
+ icon.optString("tintColor"),
1903
+ color(theme, "iconSubdued", "#00000072"),
1904
+ )
1905
+ titleLine.addView(headerTitleIcon, LayoutParams(dp(12), dp(12)).apply {
1906
+ marginStart = dp(4)
1907
+ })
1908
+ if (icon.optString("name") != "ChevronGrabberVerOutline") {
1909
+ title.setTextColor(color(theme, "primaryText", "#000000DF"))
1910
+ }
1911
+ }
1912
+ }
1913
+ showText(subtitle, item.json.optString("subtitle"), 1)
1914
+ if (isGallery) {
1915
+ setPadding(dp(12), 0, dp(12), dp(8))
1916
+ } else if (isSummary) {
1917
+ addView(trailingColumn, wrap())
1918
+ // NetworkListHeader: outer mt=16/pb=12, inner XStack px=20/py=8.
1919
+ setPadding(dp(headerHorizontalInset), dp(24), dp(headerHorizontalInset), dp(20))
1920
+ showTrailing(
1921
+ 0,
1922
+ item.json.optString("value"),
1923
+ true,
1924
+ item.json.optString("valueActionKey"),
1925
+ color(theme, "secondaryText", "#0000009B"),
1926
+ )
1927
+ trailingViews[0].textSize = sp(16f)
1928
+ trailingViews[0].typeface = NativeListFonts.medium(context)
1929
+ TextViewCompat.setLineHeight(trailingViews[0], dp(24))
1930
+ trailingViews[0].setPadding(dp(14), dp(6), dp(14), dp(6))
1931
+ trailingViews[0].layoutParams = wrap()
1932
+ } else if (!isGallery && !isHistory && !isTokenManager) {
1933
+ val value = item.json.optString("value")
1934
+ val checkboxData = item.json.optJSONObject("checkbox")
1935
+ if (checkboxData != null && value.isNotEmpty()) {
1936
+ // Value and checkbox share the trailing edge as one compound accessory.
1937
+ trailingColumn.orientation = HORIZONTAL
1938
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1939
+ trailingViews[0].layoutParams = wrap()
1940
+ checkbox.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginStart = dp(12) }
1941
+ }
1942
+ addView(trailingColumn, wrap())
1943
+ showTrailing(0, value, true)
1944
+ if (isTable) {
1945
+ trailingColumn.orientation = HORIZONTAL
1946
+ trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
1947
+ trailingViews[0].includeFontPadding = false
1948
+ trailingViews[0].textSize = sp(11f)
1949
+ trailingViews[0].typeface = NativeListFonts.regular(context)
1950
+ trailingViews[0].setTextColor(color(theme, "secondaryText", "#0000009B"))
1951
+ item.json.optJSONObject("valueIcon")?.let { icon ->
1952
+ headerValueIcon.visibility = VISIBLE
1953
+ headerValueIcon.iconName = icon.optString("name")
1954
+ headerValueIcon.tintColor = safeColor(
1955
+ icon.optString("tintColor"),
1956
+ color(theme, "iconSubdued", "#00000072"),
1957
+ )
1958
+ trailingColumn.addView(
1959
+ headerValueIcon,
1960
+ LayoutParams(dp(12), dp(12)).apply { marginStart = dp(4) },
1961
+ )
1962
+ if (icon.optString("name") != "ChevronGrabberVerOutline") {
1963
+ trailingViews[0].setTextColor(color(theme, "primaryText", "#000000DF"))
1964
+ }
1965
+ }
1966
+ }
1967
+ checkboxData?.let { bindCheckbox(item, it, checkboxState) }
1968
+ }
1969
+ }
1970
+
1971
+ private fun bindAction(
1972
+ item: NativeListItem,
1973
+ theme: JSONObject?,
1974
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
1975
+ ) {
1976
+ val isAccountSelector = item.json.optString("presentation") == "accountSelector"
1977
+ item.json.optJSONObject("icon")?.let { icon ->
1978
+ addLeading(icon, if (isAccountSelector) 32 else 40)
1979
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
1980
+ if (!icon.has("backgroundColor")) leadingFrame.background = null
1981
+ }
1982
+ addView(mainColumn, weighted())
1983
+ showText(title, item.json.optString("title"), 1)
1984
+ if (isAccountSelector) {
1985
+ title.typeface = NativeListFonts.regular(context)
1986
+ title.setTextColor(color(theme, "secondaryText", "#0000009B"))
1987
+ } else if (item.json.optString("tone") == "danger") {
1988
+ title.setTextColor(color(theme, "negative", "#C40006D3"))
1989
+ }
1990
+ item.json.optJSONObject("checkbox")?.let {
1991
+ bindCheckbox(item, it, checkboxState)
1992
+ addView(trailingColumn, wrap())
1993
+ } ?: item.json.optJSONArray("trailing")?.let { accessories ->
1994
+ if (accessories.length() > 0) {
1995
+ addView(trailingColumn, wrap())
1996
+ bindAccessories(item, accessories, theme, checkboxState)
1997
+ }
1998
+ }
1999
+ setOnClickListener {
2000
+ emitAction(item, item.json.optString("actionKey"), null, this, "row")
2001
+ }
2002
+ }
2003
+
2004
+ private fun bindSystem(item: NativeListItem, theme: JSONObject?) {
2005
+ val variant = item.json.optString("variant")
2006
+ if (variant == "spacer") {
2007
+ minimumHeight = dp(item.json.optInt("height", 0))
2008
+ return
2009
+ }
2010
+ if (variant == "end") {
2011
+ // components/ListEndIndicator: py=16, gap=8, 80x1 lines and a 4dp dot.
2012
+ gravity = Gravity.CENTER
2013
+ setPadding(0, dp(16), 0, dp(16))
2014
+ val indicatorColor = color(theme, "separator", "#0000001F")
2015
+ addView(View(context).apply { setBackgroundColor(indicatorColor) }, LayoutParams(dp(80), 1))
2016
+ addView(
2017
+ View(context).apply { background = roundedFill(indicatorColor, 2f) },
2018
+ LayoutParams(dp(4), dp(4)).apply {
2019
+ marginStart = dp(8)
2020
+ marginEnd = dp(8)
2021
+ },
2022
+ )
2023
+ addView(View(context).apply { setBackgroundColor(indicatorColor) }, LayoutParams(dp(80), 1))
2024
+ return
2025
+ }
2026
+ gravity = Gravity.CENTER
2027
+ if (variant == "loading") {
2028
+ addLeading(JSONObject().put("kind", "skeleton"), 40)
2029
+ leadingFallback.text = ""
2030
+ leadingFallback.background = roundedFill(
2031
+ color(theme, "strongBackground", "#0000000F"),
2032
+ 20f,
2033
+ )
2034
+ addSkeleton(skeletonPrimary, 120, 12, theme, bottomMarginDp = 8)
2035
+ addSkeleton(skeletonSecondary, 80, 12, theme)
2036
+ }
2037
+ showText(title, item.json.optString("message"), 2)
2038
+ title.gravity = if (variant == "retry" || variant == "noMatch") {
2039
+ Gravity.START
2040
+ } else {
2041
+ Gravity.CENTER
2042
+ }
2043
+ mainColumn.gravity = Gravity.CENTER_VERTICAL
2044
+ if (variant == "noMatch") {
2045
+ setPadding(dp(12), 0, dp(12), 0)
2046
+ title.setTextColor(color(theme, "secondaryText", "#0000009B"))
2047
+ } else if (variant == "retry") {
2048
+ setPadding(dp(12), dp(8), dp(12), dp(8))
2049
+ title.setTextColor(color(theme, "secondaryText", "#0000009B"))
2050
+ }
2051
+ addView(
2052
+ mainColumn,
2053
+ if (variant == "retry" || variant == "noMatch") weighted() else wrap(),
2054
+ )
2055
+ if (variant == "retry") {
2056
+ setOnClickListener {
2057
+ emitAction(item, item.json.optString("actionKey"), null, this, "row")
2058
+ }
2059
+ showTrailing(0, "Retry", true, item.json.optString("actionKey"))
2060
+ trailingViews[0].textSize = sp(14f)
2061
+ trailingViews[0].background = roundedFill(color(theme, "strongBackground", "#0000000F"), 16f)
2062
+ trailingViews[0].setPadding(dp(10), dp(4), dp(10), dp(4))
2063
+ TextViewCompat.setLineHeight(trailingViews[0], dp(20))
2064
+ addView(trailingColumn, wrap().apply { marginStart = dp(12) })
2065
+ }
2066
+ }
2067
+
2068
+ private fun addLeading(
2069
+ visual: JSONObject?,
2070
+ sizeDp: Int = 40,
2071
+ secondaryVisual: JSONObject? = null,
2072
+ spacingDp: Int = 12,
2073
+ ) {
2074
+ leadingFrame.visibility = VISIBLE
2075
+ leadingFrame.layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)).apply { marginEnd = dp(spacingDp) }
2076
+ addView(leadingFrame)
2077
+ leadingFallback.layoutParams = FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
2078
+ if (visual == null) return
2079
+ val kind = visual.optString("kind")
2080
+ val shape = visual.optString(
2081
+ "shape",
2082
+ if (kind == "image" || isMediaTile) "rounded" else "circle",
2083
+ )
2084
+ val sources = visualSources(visual).toMutableList()
2085
+ secondaryVisual?.let { sources.addAll(visualSources(it).take(1)) }
2086
+ val isIcon = kind == "icon"
2087
+ val cornerIconData = visual.optJSONObject("cornerIcon")
2088
+ val fallback = visual.optString("fallbackText").take(2)
2089
+ leadingFallback.text = fallback
2090
+ leadingFallback.setTextColor(parseNativeListColor("#00000072"))
2091
+ val visualBackground = safeColor(
2092
+ visual.optString("backgroundColor"),
2093
+ parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
2094
+ )
2095
+ if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
2096
+ leadingFrame.background = roundedFill(
2097
+ visualBackground,
2098
+ leadingCornerRadius(shape, sizeDp),
2099
+ )
2100
+ }
2101
+ leadingFallback.background = roundedFill(visualBackground, leadingCornerRadius(shape, sizeDp))
2102
+ leadingFallback.visibility = if (!isIcon && sources.isEmpty()) VISIBLE else GONE
2103
+ if (isIcon) {
2104
+ leadingFrame.background = GradientDrawable().apply {
2105
+ setColor(visualBackground)
2106
+ setStroke(1, parseNativeListColor("#0000001F"))
2107
+ cornerRadius = NativeListScale.dp(resources, leadingCornerRadius(shape, sizeDp))
2108
+ }
2109
+ leadingIcon.iconName = visual.optString("name")
2110
+ leadingIcon.tintColor = safeColor(
2111
+ visual.optString("tintColor"),
2112
+ parseNativeListColor("#0000009B"),
2113
+ )
2114
+ leadingIcon.visibility = VISIBLE
2115
+ }
2116
+ val visibleSources = sources.take(leadingImages.size)
2117
+ val tokenPair = kind == "token" && visibleSources.size > 1
2118
+ if (tokenPair) {
2119
+ leadingOverlayBackground.visibility = VISIBLE
2120
+ leadingOverlayBackground.background = roundedFill(visualBackdropColor, 10f)
2121
+ leadingOverlayBackground.layoutParams = FrameLayout.LayoutParams(
2122
+ dp(20),
2123
+ dp(20),
2124
+ Gravity.END or Gravity.BOTTOM,
2125
+ ).apply {
2126
+ marginEnd = -dp(4)
2127
+ bottomMargin = -dp(4)
2128
+ }
2129
+ }
2130
+ if (cornerIconData != null) {
2131
+ leadingCornerIconFrame.visibility = VISIBLE
2132
+ leadingCornerIconFrame.background = roundedFill(
2133
+ safeColor(cornerIconData.optString("backgroundColor"), visualBackdropColor),
2134
+ 10f,
2135
+ )
2136
+ leadingCornerIconFrame.layoutParams = FrameLayout.LayoutParams(
2137
+ dp(20),
2138
+ dp(20),
2139
+ Gravity.END or Gravity.BOTTOM,
2140
+ ).apply {
2141
+ marginEnd = -dp(4)
2142
+ bottomMargin = -dp(4)
2143
+ }
2144
+ leadingCornerIcon.iconName = cornerIconData.optString("name")
2145
+ leadingCornerIcon.tintColor = safeColor(
2146
+ cornerIconData.optString("tintColor"),
2147
+ parseNativeListColor("#0000009B"),
2148
+ )
2149
+ }
2150
+ visibleSources.forEachIndexed { index, (source, variant) ->
2151
+ val image = leadingImages[index]
2152
+ image.visibility = VISIBLE
2153
+ image.layoutParams = leadingImageLayout(
2154
+ index = index,
2155
+ count = visibleSources.size,
2156
+ sizeDp = sizeDp,
2157
+ tokenPair = tokenPair,
2158
+ )
2159
+ image.outlineProvider = when {
2160
+ tokenPair && index == 1 -> circleOutlineProvider
2161
+ else -> leadingOutlineProvider(shape)
2162
+ }
2163
+ image.clipToOutline = true
2164
+ bindImage(source, image, boundKey ?: "", index, variant)
2165
+ }
2166
+ }
2167
+
2168
+ private fun visualSources(visual: JSONObject): List<Pair<JSONObject, String>> {
2169
+ return when (val kind = visual.optString("kind")) {
2170
+ "stackedImages" -> {
2171
+ val images = visual.optJSONArray("images") ?: return emptyList()
2172
+ (0 until minOf(3, images.length())).mapNotNull { index ->
2173
+ images.optJSONObject(index)?.let { it to "generic" }
2174
+ }
2175
+ }
2176
+ "icon" -> emptyList()
2177
+ else -> buildList {
2178
+ visual.optJSONObject("image")?.let {
2179
+ val variant = when (kind) {
2180
+ "token" -> "token"
2181
+ "network" -> "network"
2182
+ "account", "wallet" -> "avatar"
2183
+ else -> "generic"
2184
+ }
2185
+ add(it to variant)
2186
+ }
2187
+ if (kind == "token") {
2188
+ visual.optJSONObject("networkImage")?.let { add(it to "network") }
2189
+ }
2190
+ }
2191
+ }
2192
+ }
2193
+
2194
+ private fun leadingImageLayout(
2195
+ index: Int,
2196
+ count: Int,
2197
+ sizeDp: Int,
2198
+ tokenPair: Boolean,
2199
+ ): FrameLayout.LayoutParams {
2200
+ if (count == 1 || tokenPair && index == 0) {
2201
+ return FrameLayout.LayoutParams(dp(sizeDp), dp(sizeDp))
2202
+ }
2203
+ if (tokenPair && index == 1) {
2204
+ return FrameLayout.LayoutParams(dp(16), dp(16), Gravity.END or Gravity.BOTTOM).apply {
2205
+ marginEnd = -dp(2)
2206
+ bottomMargin = -dp(2)
2207
+ }
2208
+ }
2209
+ val imageSize = (sizeDp * 0.72f).toInt()
2210
+ return FrameLayout.LayoutParams(dp(imageSize), dp(imageSize), Gravity.START or Gravity.CENTER_VERTICAL).apply {
2211
+ marginStart = dp(index * (sizeDp - imageSize) / maxOf(1, count - 1))
2212
+ }
2213
+ }
2214
+
2215
+ private fun bindAccessories(
2216
+ item: NativeListItem,
2217
+ accessories: JSONArray?,
2218
+ theme: JSONObject?,
2219
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
2220
+ ) {
2221
+ if (accessories == null) return
2222
+ var textIndex = 0
2223
+ for (index in 0 until minOf(2, accessories.length())) {
2224
+ val accessory = accessories.getJSONObject(index)
2225
+ when (accessory.optString("kind")) {
2226
+ "value" -> showTrailing(textIndex++, accessory.optString("text"), !accessory.optBoolean("secondary", false))
2227
+ "valuePair" -> showTrailingValuePair(textIndex++, accessory, theme)
2228
+ "checkbox" -> bindCheckbox(item, accessory, checkboxState)
2229
+ "radio" -> showTrailing(
2230
+ textIndex++,
2231
+ if (accessory.optBoolean("checked")) "●" else "○",
2232
+ true,
2233
+ accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
2234
+ )
2235
+ "switch" -> showTrailing(
2236
+ textIndex++,
2237
+ if (accessory.optBoolean("value")) "ON" else "OFF",
2238
+ true,
2239
+ accessory.optString("actionKey").takeUnless { accessory.optBoolean("disabled", false) },
2240
+ )
2241
+ "chevron" -> showTrailingIcon(
2242
+ textIndex++,
2243
+ item,
2244
+ JSONObject(accessory.toString()).apply {
2245
+ put("name", "ChevronRightSmallOutline")
2246
+ if (optString("actionKey").isEmpty()) put("actionKey", "press")
2247
+ },
2248
+ )
2249
+ "menu" -> showTrailingMenu(textIndex++, accessory.optString("actionKey"))
2250
+ "drag" -> showTrailingIcon(
2251
+ textIndex++,
2252
+ item,
2253
+ JSONObject(accessory.toString()).apply { put("name", "DragOutline") },
2254
+ )
2255
+ "icon" -> showTrailingIcon(textIndex++, item, accessory)
2256
+ "spinner" -> spinner.visibility = VISIBLE
2257
+ "progress" -> showTrailing(textIndex++, "${(accessory.optDouble("value") * 100).toInt()}%", false)
2258
+ }
2259
+ }
2260
+ }
2261
+
2262
+ private fun bindCheckbox(
2263
+ item: NativeListItem,
2264
+ json: JSONObject,
2265
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
2266
+ ) {
2267
+ boundCheckboxData = json
2268
+ val targetJson = json.optJSONObject("target")
2269
+ val scope = targetJson?.optString("scope", "row") ?: "row"
2270
+ val target = NativeSelectionTarget(
2271
+ scope,
2272
+ when (scope) {
2273
+ "section" -> targetJson?.optString("sectionKey")
2274
+ "row" -> item.key
2275
+ else -> null
2276
+ },
2277
+ )
2278
+ val state = checkboxState(item, target, json.optString("state", "unchecked"))
2279
+ val accessoryDisabled = json.optBoolean("disabled", false)
2280
+ if (json.optBoolean("loading", false)) {
2281
+ checkbox.visibility = GONE
2282
+ checkbox.setOnClickListener(null)
2283
+ spinner.visibility = VISIBLE
2284
+ spinner.alpha = if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
2285
+ return
2286
+ }
2287
+ checkbox.visibility = VISIBLE
2288
+ checkbox.setState(state, checkboxUncheckedColor)
2289
+ checkbox.background = if (state == "unchecked") {
2290
+ roundedStroke(checkboxBorderColor, checkboxUncheckedColor, 4f)
2291
+ } else {
2292
+ roundedFill(checkboxCheckedColor, 4f)
2293
+ }
2294
+ // Row-level disabled opacity already applies to this child. Only apply a
2295
+ // local 0.5 when the accessory alone is disabled, never 0.5 * 0.5.
2296
+ checkbox.alpha = if (!item.json.optBoolean("disabled", false) && accessoryDisabled) 0.5f else 1f
2297
+ checkbox.isEnabled =
2298
+ !item.json.optBoolean("disabled", false) &&
2299
+ !accessoryDisabled &&
2300
+ !json.optBoolean("loading", false)
2301
+ checkbox.setOnClickListener {
2302
+ emitAction(
2303
+ item,
2304
+ json.optString("actionKey", "selection"),
2305
+ target,
2306
+ checkbox,
2307
+ "trailingAccessory",
2308
+ )
2309
+ }
2310
+ }
2311
+
2312
+ private fun applySelectionState(
2313
+ item: NativeListItem,
2314
+ theme: JSONObject?,
2315
+ layout: String,
2316
+ itemIndex: Int?,
2317
+ selected: Boolean,
2318
+ ) {
2319
+ var rowBackground = color(
2320
+ theme,
2321
+ if (selected) "rowSelectedBackground" else "rowBackground",
2322
+ if (selected) "#0000000F" else "#FFFFFF",
2323
+ )
2324
+ if (item.type == "rail" || item.type == "mediaTile") {
2325
+ // The source FavoriteTokenItem has no active/resting fill, and the NFT
2326
+ // tile changes only the image opacity while pressed.
2327
+ rowBackground = color(theme, "rowBackground", "#FFFFFF")
2328
+ } else if (item.type == "metricCard" && !selected) {
2329
+ rowBackground = color(theme, "subduedBackground", "#F9F9F9")
2330
+ }
2331
+ val groupPosition = when {
2332
+ item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> "single"
2333
+ else -> when (item.type) {
2334
+ "metricCard" -> "single"
2335
+ "rail" -> "rail"
2336
+ else -> item.json.optString("groupPosition")
2337
+ }
2338
+ }
2339
+ var backgroundGroupPosition = if (item.type == "mediaTile") "mediaTile" else groupPosition
2340
+ if (layout == "sectioned") {
2341
+ // Selection in sectioned lists is represented by the OneKey checkbox,
2342
+ // matching iOS and the app-monorepo network selector.
2343
+ rowBackground = color(theme, "rowBackground", "#FFFFFF")
2344
+ if (item.type == "sectionHeader") backgroundGroupPosition = ""
2345
+ } else if (
2346
+ layout == "table" &&
2347
+ item.type == "dataRow" &&
2348
+ (if (item.json.has("index")) item.json.optInt("index") else itemIndex)?.rem(2) == 0 &&
2349
+ !selected
2350
+ ) {
2351
+ rowBackground = color(theme, "subduedBackground", "#F9F9F9")
2352
+ backgroundGroupPosition = ""
2353
+ }
2354
+ restingRowBackground = groupedBackground(backgroundGroupPosition, rowBackground)
2355
+ background = restingRowBackground
2356
+ }
2357
+
2358
+ private fun showText(view: TextView, value: String, lines: Int) {
2359
+ if (value.isEmpty()) return
2360
+ view.text = value
2361
+ view.maxLines = lines
2362
+ view.visibility = VISIBLE
2363
+ }
2364
+
2365
+ private fun showTrailing(
2366
+ index: Int,
2367
+ value: String,
2368
+ primary: Boolean,
2369
+ actionKey: String? = null,
2370
+ textColor: Int? = null,
2371
+ ) {
2372
+ if (index !in trailingViews.indices || value.isEmpty()) return
2373
+ val view = trailingViews[index]
2374
+ view.maxLines = 1
2375
+ view.text = value
2376
+ view.textSize = sp(if (primary) 16f else 14f)
2377
+ view.typeface = if (primary) NativeListFonts.medium(context) else NativeListFonts.regular(context)
2378
+ view.includeFontPadding = false
2379
+ view.fontFeatureSettings = "tnum"
2380
+ TextViewCompat.setLineHeight(view, dp(if (primary) 24 else 20))
2381
+ textColor?.let(view::setTextColor)
2382
+ view.visibility = VISIBLE
2383
+ if (!actionKey.isNullOrEmpty()) {
2384
+ view.setOnClickListener {
2385
+ (tag as? NativeListItem)
2386
+ ?.takeUnless { item -> item.json.optBoolean("disabled", false) }
2387
+ ?.let { item ->
2388
+ emitAction(item, actionKey, null, view, "trailingAccessory", index)
2389
+ }
2390
+ }
2391
+ }
2392
+ }
2393
+
2394
+ private fun showTrailingValuePair(index: Int, accessory: JSONObject, theme: JSONObject?) {
2395
+ if (index !in trailingViews.indices) return
2396
+ val primary = accessory.optString("primary")
2397
+ val secondary = accessory.optString("secondary")
2398
+ if (primary.isEmpty() && secondary.isEmpty()) return
2399
+ val value = SpannableStringBuilder()
2400
+ val primaryStart = value.length
2401
+ value.append(primary)
2402
+ value.setSpan(
2403
+ ForegroundColorSpan(accessoryTextColor(accessory.optString("primaryTone"), "primary", theme)),
2404
+ primaryStart,
2405
+ value.length,
2406
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2407
+ )
2408
+ value.setSpan(
2409
+ AbsoluteSizeSpan(
2410
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(16f), resources.displayMetrics).roundToInt(),
2411
+ ),
2412
+ primaryStart,
2413
+ value.length,
2414
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2415
+ )
2416
+ if (primary.isNotEmpty() && secondary.isNotEmpty()) value.append('\n')
2417
+ val secondaryStart = value.length
2418
+ value.append(secondary)
2419
+ value.setSpan(
2420
+ ForegroundColorSpan(accessoryTextColor(accessory.optString("secondaryTone"), "secondary", theme)),
2421
+ secondaryStart,
2422
+ value.length,
2423
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2424
+ )
2425
+ value.setSpan(
2426
+ AbsoluteSizeSpan(
2427
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp(14f), resources.displayMetrics).roundToInt(),
2428
+ ),
2429
+ secondaryStart,
2430
+ value.length,
2431
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
2432
+ )
2433
+ val view = trailingViews[index]
2434
+ view.text = value
2435
+ view.maxLines = 2
2436
+ view.gravity = Gravity.END
2437
+ view.includeFontPadding = false
2438
+ view.typeface = NativeListFonts.regular(context)
2439
+ view.fontFeatureSettings = "tnum"
2440
+ TextViewCompat.setLineHeight(view, dp(20))
2441
+ view.visibility = VISIBLE
2442
+ }
2443
+
2444
+ private fun showTrailingMenu(index: Int, actionKey: String) {
2445
+ showTrailing(index, "⋮", true, actionKey)
2446
+ if (index !in trailingViews.indices) return
2447
+ trailingViews[index].apply {
2448
+ gravity = Gravity.CENTER
2449
+ textSize = sp(20f)
2450
+ layoutParams = LayoutParams(dp(24), dp(24)).apply {
2451
+ if (trailingColumn.orientation == HORIZONTAL && index > 0) marginStart = dp(8)
2452
+ }
2453
+ }
2454
+ }
2455
+
2456
+ private fun accessoryTextColor(tone: String, defaultTone: String, theme: JSONObject?): Int =
2457
+ when (tone.ifEmpty { defaultTone }) {
2458
+ "positive" -> color(theme, "positive", "#00713FDE")
2459
+ "negative" -> color(theme, "negative", "#C40006D3")
2460
+ "secondary" -> color(theme, "secondaryText", "#0000009B")
2461
+ else -> color(theme, "primaryText", "#000000DF")
2462
+ }
2463
+
2464
+ private fun showTrailingIcon(index: Int, item: NativeListItem, data: JSONObject) {
2465
+ if (index !in trailingIcons.indices) return
2466
+ val icon = trailingIcons[index]
2467
+ icon.iconName = data.optString("name")
2468
+ icon.tintColor = safeColor(data.optString("tintColor"), iconSubduedColor)
2469
+ if (icon.iconName == "ChevronRightSmallOutline") {
2470
+ icon.glyphSizeDp = null
2471
+ // ListItem.DrillIn is a 24dp icon with mx=-6, for a 12dp layout footprint.
2472
+ icon.layoutParams = LayoutParams(dp(24), dp(24)).apply {
2473
+ marginStart = -dp(6)
2474
+ marginEnd = -dp(6)
2475
+ }
2476
+ } else {
2477
+ // ListItem.IconButton medium keeps a 24dp glyph in a 36dp frame. Its
2478
+ // m=-7 moves the last frame 7dp through the row's trailing padding. For
2479
+ // the Bookmark XStack, gap=$6 plus both negative margins leaves 10dp
2480
+ // between the physical frames (46dp between glyph centers).
2481
+ icon.glyphSizeDp = 24
2482
+ setPadding(paddingLeft, paddingTop, dp(5), paddingBottom)
2483
+ icon.layoutParams = LayoutParams(dp(36), dp(36)).apply {
2484
+ gravity = Gravity.END
2485
+ if (trailingColumn.orientation == HORIZONTAL && index > 0) {
2486
+ marginStart = dp(10)
2487
+ }
2488
+ }
2489
+ }
2490
+ icon.visibility = VISIBLE
2491
+ icon.isEnabled = !data.optBoolean("disabled", false)
2492
+ icon.alpha = if (icon.isEnabled) 1f else 0.4f
2493
+ val actionKey = data.optString("actionKey")
2494
+ if (icon.isEnabled && actionKey.isNotEmpty()) {
2495
+ icon.setOnClickListener {
2496
+ emitAction(item, actionKey, null, icon, "trailingAccessory", index)
2497
+ }
2498
+ }
2499
+ }
2500
+
2501
+ private fun applySize(item: NativeListItem) {
2502
+ val isWalletSidebar =
2503
+ item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
2504
+ val isAccountSelectorIdentity =
2505
+ item.type == "identity" && item.json.optString("presentation") == "accountSelector"
2506
+ val isNetworkSelectorIdentity =
2507
+ item.type == "identity" && item.json.optString("presentation") == "networkSelector"
2508
+ val isAccountSelectorAction =
2509
+ item.type == "action" && item.json.optString("presentation") == "accountSelector"
2510
+ val isNetworkSelectorSection =
2511
+ item.type == "sectionHeader" && item.json.optString("presentation") == "networkSelector"
2512
+ title.textSize = sp(
2513
+ if (isWalletSidebar) {
2514
+ 12f
2515
+ } else {
2516
+ when (item.type) {
2517
+ "message" -> 14f
2518
+ "sectionHeader" -> when {
2519
+ isNetworkSelectorSection -> 14f
2520
+ item.json.optString("variant") == "gallery" -> 18f
2521
+ item.json.optString("variant") == "summary" -> 16f
2522
+ currentLayout == "table" -> 11f
2523
+ item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true -> 12f
2524
+ else -> 14f
2525
+ }
2526
+ "rail" -> 12f
2527
+ "system" -> 14f
2528
+ "metricCard" -> if (item.json.optString("size") == "large") 24f else 18f
2529
+ else -> 16f
2530
+ }
2531
+ },
2532
+ )
2533
+ title.typeface = if (isWalletSidebar || isAccountSelectorIdentity || isAccountSelectorAction) {
2534
+ NativeListFonts.regular(context)
2535
+ } else {
2536
+ when (item.type) {
2537
+ "sectionHeader" -> when {
2538
+ isNetworkSelectorSection -> NativeListFonts.medium(context)
2539
+ currentLayout == "table" -> NativeListFonts.regular(context)
2540
+ item.json.optString("variant") == "summary" -> NativeListFonts.medium(context)
2541
+ item.sectionKey in setOf("linear-tokens", "action-tokens") -> NativeListFonts.regular(context)
2542
+ else -> NativeListFonts.semibold(context)
2543
+ }
2544
+ "system" -> NativeListFonts.regular(context)
2545
+ "message", "metricCard" -> NativeListFonts.semibold(context)
2546
+ else -> NativeListFonts.medium(context)
2547
+ }
2548
+ }
2549
+ subtitle.textSize = sp(when (item.type) {
2550
+ "mediaTile" -> 12f
2551
+ "metricCard" -> 11f
2552
+ else -> 14f
2553
+ })
2554
+ tertiary.textSize = sp(14f)
2555
+ if (isNetworkSelectorSection) {
2556
+ TextViewCompat.setLineHeight(title, dp(20))
2557
+ } else if (item.type == "sectionHeader" && item.json.optString("variant") == "gallery") {
2558
+ TextViewCompat.setLineHeight(title, dp(24))
2559
+ } else if (item.type == "sectionHeader" && item.json.optString("variant") == "summary") {
2560
+ TextViewCompat.setLineHeight(title, dp(24))
2561
+ } else if (item.type == "sectionHeader" && currentLayout == "table") {
2562
+ TextViewCompat.setLineHeight(title, dp(14))
2563
+ TextViewCompat.setLineHeight(trailingViews[0], dp(14))
2564
+ } else if (
2565
+ item.type == "sectionHeader" &&
2566
+ (item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true)
2567
+ ) {
2568
+ TextViewCompat.setLineHeight(title, dp(16))
2569
+ } else if (item.type == "sectionHeader" && item.sectionKey in setOf("linear-tokens", "action-tokens")) {
2570
+ TextViewCompat.setLineHeight(title, dp(20))
2571
+ } else if (item.type == "identity" && item.json.optString("presentation") == "walletSidebar") {
2572
+ TextViewCompat.setLineHeight(title, dp(16))
2573
+ } else if (item.type == "identity") {
2574
+ TextViewCompat.setLineHeight(title, dp(24))
2575
+ TextViewCompat.setLineHeight(subtitle, dp(20))
2576
+ TextViewCompat.setLineHeight(tertiary, dp(20))
2577
+ } else if (item.type == "action") {
2578
+ TextViewCompat.setLineHeight(title, dp(24))
2579
+ } else if (item.type == "system") {
2580
+ TextViewCompat.setLineHeight(title, dp(20))
2581
+ } else if (item.type == "mediaTile") {
2582
+ TextViewCompat.setLineHeight(title, dp(24))
2583
+ TextViewCompat.setLineHeight(subtitle, dp(16))
2584
+ }
2585
+ status.textSize = sp(12f)
2586
+ badgeLine.textSize = sp(12f)
2587
+ dataColumns.forEach { column ->
2588
+ column.textSize = sp(16f)
2589
+ column.typeface = NativeListFonts.medium(context)
2590
+ column.fontFeatureSettings = "tnum"
2591
+ }
2592
+ val baseHeight = when {
2593
+ item.type == "system" && item.json.optString("variant") == "spacer" -> item.json.optInt("height", 0)
2594
+ item.type == "walletGroup" -> {
2595
+ val childCount = item.json.optJSONArray("children")?.length() ?: 0
2596
+ (childCount + 1) * 68 + childCount * 12
2597
+ }
2598
+ isNetworkSelectorIdentity -> 47
2599
+ else -> when (item.type) {
2600
+ "rail" -> 28
2601
+ "activity" -> if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
2602
+ "message" -> 0
2603
+ "mediaTile" -> 0
2604
+ "metricCard" -> when (item.json.optString("variant")) {
2605
+ "activity" -> 0
2606
+ "performance" -> 0
2607
+ else -> 132
2608
+ }
2609
+ "sectionHeader" -> when {
2610
+ isNetworkSelectorSection -> 44
2611
+ currentLayout == "table" -> 28
2612
+ item.json.optString("variant") == "summary" -> 80
2613
+ item.json.optString("variant") == "gallery" -> 32
2614
+ item.json.optString("variant") == "history" || item.sectionKey?.startsWith("history-") == true -> 16
2615
+ item.sectionKey in setOf("linear-tokens", "action-tokens") -> 30
2616
+ item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null -> 40
2617
+ else -> 36
2618
+ }
2619
+ "system" -> when (item.json.optString("variant")) {
2620
+ "noMatch", "end" -> 36
2621
+ "retry" -> 44
2622
+ else -> 56
2623
+ }
2624
+ "action" -> when {
2625
+ item.json.optString("presentation") == "accountSelector" -> 48
2626
+ item.json.has("icon") -> 60
2627
+ else -> 44
2628
+ }
2629
+ "dataRow" -> if (currentLayout == "table") {
2630
+ 60
2631
+ } else if ((0 until item.json.getJSONArray("columns").length()).any {
2632
+ item.json.getJSONArray("columns").getJSONObject(it).optString("secondaryText").isNotEmpty()
2633
+ }) {
2634
+ 64
2635
+ } else {
2636
+ 56
2637
+ }
2638
+ else -> when {
2639
+ item.type == "identity" && item.json.optString("presentation") == "walletSidebar" -> 68
2640
+ item.type == "identity" && item.json.optString("tertiary").isNotEmpty() -> 72
2641
+ item.type == "identity" && item.json.optString("subtitle").isNotEmpty() -> 60
2642
+ else -> 56
2643
+ }
2644
+ }
2645
+ }
2646
+ val modifier = if (isNetworkSelectorIdentity) {
2647
+ 0
2648
+ } else if (
2649
+ item.type == "sectionHeader" && item.json.optString("variant") in listOf("summary", "gallery")
2650
+ ) {
2651
+ 0
2652
+ } else {
2653
+ when (item.json.optString("size", "medium")) { "small" -> -8; "large" -> 12; else -> 0 }
2654
+ }
2655
+ val sectionSpacing = 0
2656
+ val hasSecondaryColumn = item.type == "dataRow" &&
2657
+ (0 until item.json.getJSONArray("columns").length()).any {
2658
+ item.json.getJSONArray("columns").getJSONObject(it).optString("secondaryText").isNotEmpty()
2659
+ }
2660
+ val tableAdjustment = if (item.type == "dataRow" && currentLayout == "table" && !hasSecondaryColumn) -8 else 0
2661
+ minimumHeight = dp((baseHeight + modifier + sectionSpacing + tableAdjustment).coerceAtLeast(0))
2662
+ }
2663
+
2664
+ private fun addSkeleton(
2665
+ view: View,
2666
+ widthDp: Int,
2667
+ heightDp: Int,
2668
+ theme: JSONObject?,
2669
+ bottomMarginDp: Int = 0,
2670
+ ) {
2671
+ view.background = roundedFill(color(theme, "strongBackground", "#0000000F"), 6f)
2672
+ mainColumn.addView(
2673
+ view,
2674
+ LayoutParams(dp(widthDp), dp(heightDp)).apply { bottomMargin = dp(bottomMarginDp) },
2675
+ )
2676
+ }
2677
+
2678
+ private fun applyListOrientation(item: NativeListItem, listOrientation: String) {
2679
+ val params = layoutParams ?: return
2680
+ if (listOrientation == "horizontal") {
2681
+ params.width = when (item.type) {
2682
+ "rail" -> railWidth()
2683
+ "mediaTile" -> dp(200)
2684
+ else -> dp(280)
2685
+ }
2686
+ params.height = ViewGroup.LayoutParams.WRAP_CONTENT
2687
+ } else {
2688
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT
2689
+ params.height = ViewGroup.LayoutParams.WRAP_CONTENT
2690
+ }
2691
+ layoutParams = params
2692
+ }
2693
+
2694
+ private fun railWidth(): Int {
2695
+ var width = dp(4 + 20 + 6).toFloat() + title.paint.measureText(title.text.toString())
2696
+ if (badgeLine.visibility == VISIBLE) {
2697
+ width += dp(6) + badgeLine.paint.measureText(badgeLine.text.toString())
2698
+ }
2699
+ if (status.visibility == VISIBLE) {
2700
+ width += dp(6) + status.paint.measureText(status.text.toString())
2701
+ }
2702
+ width += dp(4)
2703
+ return width.roundToInt()
2704
+ }
2705
+
2706
+ private fun leadingCornerRadius(shape: String, sizeDp: Int): Float = when (shape) {
2707
+ "square" -> 0f
2708
+ "rounded" -> minOf(10f, sizeDp / 4f)
2709
+ else -> sizeDp / 2f
2710
+ }
2711
+
2712
+ private fun leadingOutlineProvider(shape: String) = object : ViewOutlineProvider() {
2713
+ override fun getOutline(view: View, outline: Outline) {
2714
+ when (shape) {
2715
+ "square" -> outline.setRect(0, 0, view.width, view.height)
2716
+ "rounded" -> outline.setRoundRect(0, 0, view.width, view.height, dp(10).toFloat())
2717
+ else -> outline.setOval(0, 0, view.width, view.height)
2718
+ }
2719
+ }
2720
+ }
2721
+
2722
+ private fun JSONArray?.hasAccessory(kind: String): Boolean {
2723
+ if (this == null) return false
2724
+ return (0 until length()).any { optJSONObject(it)?.optString("kind") == kind }
2725
+ }
2726
+
2727
+ private fun JSONArray?.isBookmarkEditActions(): Boolean =
2728
+ this != null && length() == 2 &&
2729
+ optJSONObject(0)?.optString("kind") == "icon" &&
2730
+ optJSONObject(0)?.optString("name") == "PencilOutline" &&
2731
+ optJSONObject(1)?.optString("kind") == "icon" &&
2732
+ optJSONObject(1)?.optString("name") == "DragOutline"
2733
+
2734
+ private fun JSONArray?.isValuePairMenu(): Boolean =
2735
+ this != null && length() == 2 &&
2736
+ optJSONObject(0)?.optString("kind") == "valuePair" &&
2737
+ optJSONObject(1)?.optString("kind") == "menu"
2738
+
2739
+ private fun bindImage(
2740
+ source: JSONObject,
2741
+ imageView: OneKeyImageReusableView,
2742
+ token: String,
2743
+ slot: Int,
2744
+ variant: String,
2745
+ ) {
2746
+ val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
2747
+ imageView.configure(
2748
+ sourceUri = uri,
2749
+ sourceHeadersJson = source.optJSONObject("headers")?.toString(),
2750
+ variant = variant,
2751
+ contentFit = source.optString("contentFit", "cover"),
2752
+ cachePolicy = source.optString("cachePolicy", "memory-disk"),
2753
+ autoplay = source.optBoolean("autoplay", false),
2754
+ recyclingKey = "$token:$slot",
2755
+ optimizeTos = source.optBoolean("optimizeTos", true),
2756
+ overscan = source.optDouble("overscan", 1.1),
2757
+ loadingStrategy = source.optString("loadingStrategy", "static"),
2758
+ )
2759
+ }
2760
+
2761
+ private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
2762
+ private fun wrap() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
2763
+ private fun dp(value: Int): Int = NativeListScale.dp(resources, value)
2764
+ private fun sp(value: Float): Float = NativeListScale.font(resources, value)
2765
+
2766
+ private fun color(theme: JSONObject?, key: String, fallback: String): Int =
2767
+ safeColor(theme?.optString(key, fallback), parseNativeListColor(fallback))
2768
+
2769
+ private fun safeColor(value: String?, fallback: Int): Int = try {
2770
+ if (value.isNullOrEmpty()) fallback else parseNativeListColor(value)
2771
+ } catch (_: IllegalArgumentException) {
2772
+ fallback
2773
+ }
2774
+
2775
+ private fun roundedFill(color: Int, radiusDp: Float) = GradientDrawable().apply {
2776
+ setColor(color)
2777
+ cornerRadius = NativeListScale.dp(resources, radiusDp)
2778
+ }
2779
+
2780
+ private fun roundedStroke(stroke: Int, fill: Int, radiusDp: Float) = GradientDrawable().apply {
2781
+ setColor(fill)
2782
+ setStroke(dp(2), stroke)
2783
+ cornerRadius = NativeListScale.dp(resources, radiusDp)
2784
+ }
2785
+
2786
+ private fun roundedHairlineStroke(stroke: Int, radiusDp: Float) = GradientDrawable().apply {
2787
+ setColor(Color.TRANSPARENT)
2788
+ setStroke(1, stroke)
2789
+ cornerRadius = NativeListScale.dp(resources, radiusDp)
2790
+ }
2791
+
2792
+ private fun groupedBackground(position: String, color: Int) = GradientDrawable().apply {
2793
+ setColor(color)
2794
+ val radius = NativeListScale.dp(resources, 12f)
2795
+ cornerRadii = when (position) {
2796
+ "first" -> floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f)
2797
+ "last" -> floatArrayOf(0f, 0f, 0f, 0f, radius, radius, radius, radius)
2798
+ "single" -> FloatArray(8) { radius }
2799
+ "rail" -> FloatArray(8) { NativeListScale.dp(resources, 8f) }
2800
+ "mediaTile" -> FloatArray(8) { NativeListScale.dp(resources, 16f) }
2801
+ else -> FloatArray(8)
2802
+ }
2803
+ }
2804
+
2805
+ }
2806
+
2807
+ private class OneKeyIconView(context: android.content.Context) : View(context) {
2808
+ var iconName: String = ""
2809
+ set(value) {
2810
+ field = value
2811
+ invalidate()
2812
+ }
2813
+ var tintColor: Int = Color.rgb(100, 100, 100)
2814
+ set(value) {
2815
+ field = value
2816
+ invalidate()
2817
+ }
2818
+ var glyphSizeDp: Int? = null
2819
+ set(value) {
2820
+ field = value
2821
+ invalidate()
2822
+ }
2823
+
2824
+ private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
2825
+
2826
+ override fun onDraw(canvas: Canvas) {
2827
+ super.onDraw(canvas)
2828
+ val pathData = iconPaths[iconName] ?: return
2829
+ val drawSize = minOf(
2830
+ minOf(width, height).toFloat(),
2831
+ glyphSizeDp?.let { NativeListScale.dp(resources, it).toFloat() } ?: Float.MAX_VALUE,
2832
+ )
2833
+ val scale = drawSize / 24f
2834
+ fill.color = tintColor
2835
+ canvas.save()
2836
+ canvas.translate((width - drawSize) / 2f, (height - drawSize) / 2f)
2837
+ canvas.scale(scale, scale)
2838
+ val sourceFillTypes = actionIconFillTypes[iconName]
2839
+ pathData.forEachIndexed { index, data ->
2840
+ PathParser.createPathFromPathData(data)?.let { path ->
2841
+ path.fillType = sourceFillTypes?.getOrNull(index) ?: Path.FillType.EVEN_ODD
2842
+ canvas.drawPath(path, fill)
2843
+ }
2844
+ }
2845
+ canvas.restore()
2846
+ }
2847
+
2848
+ companion object {
2849
+ // Keep the SVG fill-rule used by each Action-row source path. React Native
2850
+ // SVG defaults to nonzero (WINDING); only paths declaring fillRule="evenodd"
2851
+ // use EVEN_ODD. Other existing icons retain their prior rendering behavior.
2852
+ private val actionIconFillTypes = mapOf(
2853
+ "ChevronRightSmallOutline" to listOf(Path.FillType.WINDING),
2854
+ "MinusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
2855
+ "PlusCircleOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
2856
+ "PlusSmallOutline" to listOf(Path.FillType.WINDING),
2857
+ "DotHorOutline" to listOf(Path.FillType.WINDING),
2858
+ "MinusCircleSolid" to listOf(Path.FillType.EVEN_ODD),
2859
+ "PencilOutline" to listOf(Path.FillType.EVEN_ODD),
2860
+ "DragOutline" to listOf(Path.FillType.WINDING),
2861
+ "StarSolid" to listOf(Path.FillType.WINDING),
2862
+ "ChevronGrabberVerOutline" to listOf(Path.FillType.WINDING),
2863
+ "ChevronBottomOutline" to listOf(Path.FillType.WINDING),
2864
+ "ChevronTopOutline" to listOf(Path.FillType.WINDING),
2865
+ "ImageSquareWavesOutline" to listOf(Path.FillType.WINDING, Path.FillType.EVEN_ODD),
2866
+ )
2867
+
2868
+ // Exact 24x24 paths from app-monorepo packages/components Icon sources.
2869
+ private val iconPaths = mapOf(
2870
+ "ArrowBottomOutline" to listOf("m13 17.586 5-5L19.414 14 12 21.414 4.586 14 6 12.586l5 5V3h2z"),
2871
+ "ArrowTopOutline" to listOf("M19.414 10 18 11.414l-5-5V21h-2V6.414l-5 5L4.586 10 12 2.586z"),
2872
+ "ChartTrendingUpOutline" to listOf("M22 13h-2V9.414l-7 7-4-4-6 6L1.586 17 9 9.586l4 4L18.586 8H15V6h7z"),
2873
+ "SwapHorOutline" to listOf("M21 16H6.914l2.293 2.293-1.414 1.414L2.086 14H21zm.914-6H3V8h14.086l-2.293-2.293 1.414-1.414z"),
2874
+ "ShieldExclamationOutline" to listOf(
2875
+ "M12 12.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5m0-4.75a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1",
2876
+ "M11.352 2.223c.42-.145.876-.145 1.296 0l6.98 2.4a1.995 1.995 0 0 1 1.347 1.886v5.432c0 2.799-1.146 4.817-2.805 6.387-1.61 1.525-3.735 2.652-5.696 3.71a1 1 0 0 1-.947 0c-1.961-1.058-4.086-2.185-5.697-3.71-1.659-1.57-2.805-3.588-2.805-6.386V6.508c0-.852.542-1.61 1.347-1.887l6.98-2.4ZM5.02 6.509v5.432c0 2.16.848 3.676 2.18 4.938 1.272 1.204 2.957 2.149 4.799 3.145 1.842-.996 3.527-1.94 4.799-3.145 1.332-1.262 2.181-2.778 2.181-4.938V6.51L12 4.109z",
2877
+ ),
2878
+ "InfoCircleOutline" to listOf(
2879
+ "M13 17h-2v-5h-1v-2h3zm0-8h-2V7h2z",
2880
+ "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
2881
+ ),
2882
+ "SpeakerPromoteOutline" to listOf("M21.996 11a4 4 0 0 1-3 3.874v5.495l-5.361-1.711A3.998 3.998 0 0 1 5.996 17v-.779l-4-1.276v-7.89l17-5.424v5.495c1.725.444 3 2.01 3 3.874m-2 0c0-.74-.402-1.385-1-1.731v3.46c.597-.345 1-.989 1-1.729M7.998 7.24v7.521l8.998 2.871V4.368l-8.998 2.87ZM3.996 8.516v4.967l2.002.638V7.877zm4 8.484a2 2 0 0 0 3.706 1.041L7.996 16.86z"),
2883
+ "PlusCircleOutline" to listOf(
2884
+ "M13 11h4v2h-4v4h-2v-4l-4 .001v-2L11 11V7h2z",
2885
+ "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
2886
+ ),
2887
+ "PlusSmallOutline" to listOf("M13 11h5v2h-5v5h-2v-5H6v-2h5V6h2z"),
2888
+ "DotHorOutline" to listOf("M6 14H2v-4h4zm8 0h-4v-4h4zm8 0h-4v-4h4z"),
2889
+ "MinusCircleOutline" to listOf(
2890
+ "M17 13H7v-2h10z",
2891
+ "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16",
2892
+ ),
2893
+ "MinusCircleSolid" to listOf("M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2M7 13h10v-2H7z"),
2894
+ "ErrorSolid" to listOf("M23.256 20H.742L12 1.041zM11 15v2h2v-2zm0-6v5h2V9z"),
2895
+ "QuestionmarkSolid" to listOf("M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m-1 15h2v-2h-2zM9 7v3h2V9h2v1l-2 1.5V14h2v-1.5l2-1.5V7z"),
2896
+ "PencilOutline" to listOf("M22.414 7.5 7.914 22H2v-5.914l14.5-14.5zM4 16.914V20h3.086l9.5-9.5L13.5 7.414zM14.914 6 18 9.086 19.586 7.5 16.5 4.414z"),
2897
+ "ChevronRightSmallOutline" to listOf("M15.414 12 10 17.414 8.586 16l4-4-4-4L10 6.586z"),
2898
+ "DragOutline" to listOf("M11 21H7v-4h4zm6 0h-4v-4h4zm-6-7H7v-4h4zm6 0h-4v-4h4zm-6-7H7V3h4zm6 0h-4V3h4z"),
2899
+ "StarOutline" to listOf("m15.455 7.243 7.729 1.123-5.592 5.45 1.32 7.698L12 17.879l-6.911 3.635 1.32-7.698-5.592-5.45 7.728-1.123L12 .24zM9.872 9.071l-4.759.69 3.444 3.358-.814 4.738L12 15.62l.465.245 3.791 1.993-.813-4.739 3.443-3.357-4.758-.69L12 4.758z"),
2900
+ "StarSolid" to listOf("m15.405 7.313 7.84 1.034-5.735 5.443 1.44 7.774L12 17.793l-6.948 3.771 1.44-7.774L.756 8.347l7.839-1.034L12 .178z"),
2901
+ "ChevronGrabberVerOutline" to listOf("M17.414 15 12 20.414 6.586 15 8 13.586l4 4 4-4zm0-6L16 10.414l-4-4-4 4L6.586 9 12 3.586z"),
2902
+ "ChevronBottomOutline" to listOf("M20.707 9.707 12 18.414 3.293 9.707l1.414-1.414L12 15.586l7.293-7.293z"),
2903
+ "ChevronTopOutline" to listOf("m20.707 14.293-1.414 1.414L12 8.414l-7.293 7.293-1.414-1.414L12 5.586z"),
2904
+ "ImageSquareWavesOutline" to listOf(
2905
+ "M14.25 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4",
2906
+ "M21 21H3V3h18zM5 16.414V19h12.586L14 15.414l-2 2-4-4zm0-2.828 3-3 4 4 2-2 5 5V5H5z",
2907
+ ),
2908
+ )
2909
+ }
2910
+ }
2911
+
2912
+ private class OneKeyCheckboxView(context: android.content.Context) : View(context) {
2913
+ private val glyphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
2914
+ private var state = "unchecked"
2915
+
2916
+ fun setState(value: String, color: Int) {
2917
+ state = value
2918
+ glyphPaint.color = color
2919
+ invalidate()
2920
+ }
2921
+
2922
+ override fun onDraw(canvas: Canvas) {
2923
+ super.onDraw(canvas)
2924
+ val pathData = when (state) {
2925
+ "checked" -> "M12.204 5.043a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414 0l-2-2a1 1 0 1 1 1.414-1.414l1.293 1.293 3.793-3.793a1 1 0 0 1 1.414 0"
2926
+ "indeterminate" -> "M4 8a1 1 0 0 1 1-1h6a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1"
2927
+ else -> return
2928
+ }
2929
+ val drawSize = minOf(width, height) * 0.8f
2930
+ canvas.save()
2931
+ canvas.translate((width - drawSize) / 2f, (height - drawSize) / 2f)
2932
+ canvas.scale(drawSize / 16f, drawSize / 16f)
2933
+ PathParser.createPathFromPathData(pathData)?.let { canvas.drawPath(it, glyphPaint) }
2934
+ canvas.restore()
2935
+ }
2936
+ }
2937
+
2938
+ internal class NativeListViewHolder(val rowView: NativeListRowView) :
2939
+ androidx.recyclerview.widget.RecyclerView.ViewHolder(rowView)