@onekeyfe/react-native-native-list 3.0.105 → 3.0.106

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 (45) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
  2. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
  4. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
  5. package/ios/HybridNativeList.swift +8 -2
  6. package/ios/NativeListCell.swift +630 -31
  7. package/ios/NativeListDesignAssets.swift +24 -1
  8. package/ios/RNCNativeListView.swift +216 -42
  9. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
  10. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
  11. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
  12. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
  13. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
  14. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
  25. package/lib/module/NativeList.js +102 -8
  26. package/lib/module/NativeList.web.js +20 -2
  27. package/lib/module/avatarPrefetch.js +174 -0
  28. package/lib/module/validation.js +45 -2
  29. package/lib/module/web/NativeListAvatarWorker.js +542 -0
  30. package/lib/module/web/NativeListWebAvatarCache.js +267 -0
  31. package/lib/module/web/NativeListWebEngine.js +958 -47
  32. package/lib/typescript/src/NativeList.web.d.ts +2 -2
  33. package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
  34. package/lib/typescript/src/models.d.ts +72 -4
  35. package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
  36. package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
  37. package/package.json +2 -2
  38. package/src/NativeList.tsx +149 -13
  39. package/src/NativeList.web.tsx +30 -3
  40. package/src/avatarPrefetch.ts +236 -0
  41. package/src/models.ts +98 -7
  42. package/src/validation.ts +131 -2
  43. package/src/web/NativeListAvatarWorker.js +567 -0
  44. package/src/web/NativeListWebAvatarCache.ts +314 -0
  45. package/src/web/NativeListWebEngine.ts +1409 -60
@@ -14,6 +14,7 @@ import android.view.Choreographer
14
14
  import android.view.Gravity
15
15
  import android.view.MotionEvent
16
16
  import android.view.View
17
+ import android.view.ViewConfiguration
17
18
  import android.view.accessibility.AccessibilityEvent
18
19
  import android.view.accessibility.AccessibilityNodeInfo
19
20
  import android.widget.FrameLayout
@@ -31,6 +32,7 @@ import org.json.JSONArray
31
32
  import org.json.JSONObject
32
33
  import java.lang.ref.WeakReference
33
34
  import java.util.UUID
35
+ import kotlin.math.abs
34
36
  import kotlin.math.ceil
35
37
  import kotlin.math.cos
36
38
  import kotlin.math.exp
@@ -87,6 +89,36 @@ class NativeListView(
87
89
  private val contentContainer = FrameLayout(context)
88
90
  private val adapter = NativeListAdapter(reactContext)
89
91
  private val layoutManager = GridLayoutManager(context, 1)
92
+ // OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
93
+ private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
94
+ private val pagerGestureTouchListener = object : RecyclerView.SimpleOnItemTouchListener() {
95
+ private var downX = 0f
96
+ private var downY = 0f
97
+ private var directionResolved = false
98
+
99
+ override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean {
100
+ if (layoutManager.orientation != RecyclerView.VERTICAL) return false
101
+ when (event.actionMasked) {
102
+ MotionEvent.ACTION_DOWN -> {
103
+ downX = event.x
104
+ downY = event.y
105
+ directionResolved = false
106
+ }
107
+ MotionEvent.ACTION_MOVE -> if (!directionResolved) {
108
+ val deltaX = abs(event.x - downX)
109
+ val deltaY = abs(event.y - downY)
110
+ if (max(deltaX, deltaY) > pagerGestureTouchSlop) {
111
+ directionResolved = true
112
+ recyclerView.parent?.requestDisallowInterceptTouchEvent(deltaY >= deltaX)
113
+ }
114
+ }
115
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
116
+ recyclerView.parent?.requestDisallowInterceptTouchEvent(false)
117
+ }
118
+ }
119
+ return false
120
+ }
121
+ }
90
122
  private val reorderPlaceholderDecoration = ReorderPlaceholderDecoration(
91
123
  adapter = adapter,
92
124
  insetPx = dp(REORDER_PLACEHOLDER_INSET_DP),
@@ -102,6 +134,7 @@ class NativeListView(
102
134
  private val sectionIndexView = NativeListSectionIndexView(context)
103
135
  private val sectionIndexPreview = TextView(context)
104
136
  private var config: NativeListConfig? = null
137
+ private var usesSelectorSourceScale = false
105
138
  private var stickyDecoration: StickySectionHeaderDecoration? = null
106
139
  private var spacingDecoration: ItemSpacingDecoration? = null
107
140
  private var itemTouchHelper: ItemTouchHelper? = null
@@ -118,7 +151,6 @@ class NativeListView(
118
151
  private var sectionIndexProgrammaticScroll = false
119
152
  private var sectionIndexHapticsEnabled = true
120
153
  private var pendingScrollRequest: ScrollRequest? = null
121
- private var contentScrollOffsetPx = 0
122
154
  private val actionAnchorInstanceId = UUID.randomUUID().toString()
123
155
  private var actionAnchorCounter = 0L
124
156
  private var actionAnchor: ActionAnchorRecord? = null
@@ -132,7 +164,10 @@ class NativeListView(
132
164
  recyclerView.adapter = adapter
133
165
  recyclerView.layoutManager = layoutManager
134
166
  recyclerView.itemAnimator = null
167
+ recyclerView.addOnItemTouchListener(pagerGestureTouchListener)
135
168
  recyclerView.addItemDecoration(reorderPlaceholderDecoration)
169
+ // OneKey patch: full-width selector header backgrounds do not change row content insets.
170
+ recyclerView.addItemDecoration(SelectorBackgroundDecoration(adapter))
136
171
  recyclerView.setHasFixedSize(false)
137
172
  layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
138
173
  override fun getSpanSize(position: Int): Int {
@@ -158,11 +193,15 @@ class NativeListView(
158
193
  )
159
194
  contentContainer.addView(
160
195
  sectionIndexView,
161
- FrameLayout.LayoutParams(dp(48), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.END),
196
+ FrameLayout.LayoutParams(
197
+ dp(SECTION_INDEX_RAIL_WIDTH_DP),
198
+ FrameLayout.LayoutParams.MATCH_PARENT,
199
+ Gravity.END,
200
+ ),
162
201
  )
163
202
  sectionIndexPreview.apply {
164
203
  gravity = Gravity.CENTER
165
- textSize = NativeListScale.font(resources, 28f)
204
+ textSize = NativeListScale.font(resources, 22f)
166
205
  typeface = NativeListFonts.semibold(context)
167
206
  visibility = GONE
168
207
  alpha = 0f
@@ -170,7 +209,13 @@ class NativeListView(
170
209
  }
171
210
  contentContainer.addView(
172
211
  sectionIndexPreview,
173
- FrameLayout.LayoutParams(dp(72), dp(72), Gravity.CENTER),
212
+ FrameLayout.LayoutParams(
213
+ dp(SECTION_INDEX_PREVIEW_SIZE_DP),
214
+ dp(SECTION_INDEX_PREVIEW_SIZE_DP),
215
+ Gravity.CENTER_VERTICAL or Gravity.END,
216
+ ).apply {
217
+ marginEnd = dp(SECTION_INDEX_PREVIEW_END_MARGIN_DP)
218
+ },
174
219
  )
175
220
  addView(contentContainer, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f))
176
221
  footerView.visibility = GONE
@@ -208,10 +253,6 @@ class NativeListView(
208
253
 
209
254
  override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
210
255
  if (dx != 0 || dy != 0) invalidateActionAnchor("scroll")
211
- contentScrollOffsetPx = (
212
- contentScrollOffsetPx +
213
- if (layoutManager.orientation == RecyclerView.VERTICAL) dy else dx
214
- ).coerceAtLeast(0)
215
256
  syncSectionIndexToVisibleRows()
216
257
  scheduleVisibleEvent()
217
258
  checkEndReached()
@@ -244,17 +285,29 @@ class NativeListView(
244
285
  invalidateActionAnchor("snapshot")
245
286
  val previous = config
246
287
  if (previous != null && canApplyStableContentUpdate(previous, next)) {
288
+ // OneKey patch: authorize a diff payload only against this exact validated baseline.
289
+ next.items.forEachIndexed { index, item ->
290
+ if (item.content != previous.items[index].content &&
291
+ adapter.currentList.getOrNull(index) === previous.items[index]
292
+ ) {
293
+ item.selectionUpdateFromContent = previous.items[index].content
294
+ }
295
+ }
247
296
  val changedSummaryKeys = previous.items.indices.mapNotNull { index ->
248
297
  previous.items[index].key.takeIf {
249
298
  previous.items[index].content != next.items[index].content
250
299
  }
251
300
  }.toSet()
252
301
  config = next
302
+ usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
303
+ adapter.usesSelectorSourceScale = usesSelectorSourceScale
253
304
  adapter.theme = next.theme
254
305
  adapter.layout = next.layout
255
306
  adapter.orientation = next.orientation
256
307
  adapter.selectedKeys = next.selectedKeys
257
308
  adapter.submitList(next.items) {
309
+ // OneKey patch: DiffUtil has dispatched its payloads; release the old serialized rows.
310
+ next.items.forEach { it.selectionUpdateFromContent = null }
258
311
  recyclerView.post {
259
312
  bindVisibleSelection(changedSummaryKeys)
260
313
  bindFooterSelection()
@@ -264,7 +317,9 @@ class NativeListView(
264
317
  return
265
318
  }
266
319
  config = next
267
- endReachedGeneration = null
320
+ usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
321
+ adapter.usesSelectorSourceScale = usesSelectorSourceScale
322
+ if (previous?.generation != next.generation) endReachedGeneration = null
268
323
  pendingReorder = null
269
324
  adapter.theme = next.theme
270
325
  adapter.layout = next.layout
@@ -327,30 +382,98 @@ class NativeListView(
327
382
  val newItem = next.items[index]
328
383
  oldItem.key == newItem.key &&
329
384
  oldItem.type == newItem.type &&
330
- (oldItem.content == newItem.content || isStableSummaryUpdate(oldItem, newItem))
385
+ // OneKey patch: controlled echoes can also carry row and checkbox selection state.
386
+ // (oldItem.content == newItem.content || isStableSummaryUpdate(oldItem, newItem))
387
+ (oldItem.content == newItem.content || isStableSelectionUpdate(
388
+ oldItem,
389
+ newItem,
390
+ next.selectionMode == "single" || next.selectionMode == "multiple",
391
+ ))
331
392
  }
332
393
  }
333
394
 
334
- private fun isStableSummaryUpdate(
395
+ // OneKey patch: preserve the original summary-only comparison for upstream reference.
396
+ // private fun isStableSummaryUpdate(
397
+ // previous: NativeListItem,
398
+ // next: NativeListItem,
399
+ // ): Boolean {
400
+ // if (
401
+ // previous.type != "sectionHeader" ||
402
+ // previous.json.optString("variant") != "summary" ||
403
+ // next.json.optString("variant") != "summary"
404
+ // ) {
405
+ // return false
406
+ // }
407
+ // val previousStructure = JSONObject(previous.content).apply {
408
+ // remove("title")
409
+ // remove("value")
410
+ // }
411
+ // val nextStructure = JSONObject(next.content).apply {
412
+ // remove("title")
413
+ // remove("value")
414
+ // }
415
+ // return previousStructure.toString() == nextStructure.toString()
416
+ // }
417
+
418
+ private fun isStableSelectionUpdate(
335
419
  previous: NativeListItem,
336
420
  next: NativeListItem,
421
+ controlled: Boolean,
337
422
  ): Boolean {
338
- if (
339
- previous.type != "sectionHeader" ||
340
- previous.json.optString("variant") != "summary" ||
341
- next.json.optString("variant") != "summary"
342
- ) {
343
- return false
423
+ val previousStructure = selectionComparisonData(previous.json, controlled) ?: return false
424
+ val nextStructure = selectionComparisonData(next.json, controlled) ?: return false
425
+ return previousStructure.toString() == nextStructure.toString()
426
+ }
427
+
428
+ // OneKey patch: ignore only fields refreshed by the lightweight selection binder.
429
+ private fun selectionComparisonData(data: JSONObject, controlled: Boolean): JSONObject? {
430
+ val type = data.opt("type") as? String ?: return null
431
+ val result = JSONObject(data.toString())
432
+ if (result.has("selected")) {
433
+ if (result.opt("selected") !is Boolean) return null
434
+ result.remove("selected")
344
435
  }
345
- val previousStructure = JSONObject(previous.content).apply {
346
- remove("title")
347
- remove("value")
436
+ if (type == "walletGroup") {
437
+ val parent = data.optJSONObject("parent") ?: return null
438
+ if (parent.optString("type") != "identity") return null
439
+ val children = data.optJSONArray("children") ?: return null
440
+ result.put("parent", selectionComparisonData(parent, false) ?: return null)
441
+ val normalizedChildren = JSONArray()
442
+ for (index in 0 until children.length()) {
443
+ val child = children.optJSONObject(index) ?: return null
444
+ if (child.optString("type") != "identity") return null
445
+ normalizedChildren.put(selectionComparisonData(child, false) ?: return null)
446
+ }
447
+ result.put("children", normalizedChildren)
348
448
  }
349
- val nextStructure = JSONObject(next.content).apply {
350
- remove("title")
351
- remove("value")
449
+ if (type == "sectionHeader" && data.optString("variant") == "summary") {
450
+ result.remove("title")
451
+ result.remove("value")
352
452
  }
353
- return previousStructure.toString() == nextStructure.toString()
453
+ if (!controlled) return result
454
+ fun checkboxData(value: Any?): JSONObject? {
455
+ val checkbox = value as? JSONObject ?: return null
456
+ if (checkbox.optString("kind") != "checkbox") return null
457
+ if (checkbox.has("state") && checkbox.opt("state") !in setOf("checked", "unchecked", "indeterminate")) return null
458
+ checkbox.remove("state")
459
+ return checkbox
460
+ }
461
+ if (type in setOf("dataRow", "sectionHeader", "action") && result.has("checkbox")) {
462
+ result.put("checkbox", checkboxData(result.opt("checkbox")) ?: return null)
463
+ }
464
+ if (type == "identity" && result.has("trailing")) {
465
+ val accessories = result.optJSONArray("trailing") ?: return null
466
+ if (accessories.length() > 2) return null
467
+ var checkboxCount = 0
468
+ for (index in 0 until accessories.length()) {
469
+ val accessory = accessories.optJSONObject(index) ?: return null
470
+ if (accessory.optString("kind") == "checkbox") {
471
+ if (++checkboxCount > 1) return null
472
+ accessories.put(index, checkboxData(accessory) ?: return null)
473
+ }
474
+ }
475
+ }
476
+ return result
354
477
  }
355
478
 
356
479
  fun applyPatches(patchesJson: String) {
@@ -389,6 +512,8 @@ class NativeListView(
389
512
  invalidateActionAnchor("snapshot")
390
513
  val next = current.copy(items = nextItems, selectedKeys = selected)
391
514
  config = next
515
+ usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
516
+ adapter.usesSelectorSourceScale = usesSelectorSourceScale
392
517
  adapter.selectedKeys = selected
393
518
  adapter.submitList(nextItems) { relayoutContents() }
394
519
  bindFooter(next)
@@ -574,16 +699,26 @@ class NativeListView(
574
699
  private fun performOffsetScroll(offset: Double, animated: Boolean) {
575
700
  val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
576
701
  val target = (offset * density).roundToInt().coerceAtLeast(0)
702
+ if (!animated) {
703
+ recyclerView.stopScroll()
704
+ manager.scrollToPositionWithOffset(0, -target)
705
+ relayoutRecyclerView()
706
+ return
707
+ }
577
708
  if (target == 0) {
578
- if (animated) recyclerView.smoothScrollToPosition(0)
579
- else manager.scrollToPositionWithOffset(0, 0)
709
+ recyclerView.smoothScrollToPosition(0)
580
710
  return
581
711
  }
582
- val delta = target - contentScrollOffsetPx
712
+ val currentOffset = if (manager.orientation == RecyclerView.VERTICAL) {
713
+ recyclerView.computeVerticalScrollOffset()
714
+ } else {
715
+ recyclerView.computeHorizontalScrollOffset()
716
+ }
717
+ val delta = target - currentOffset
583
718
  if (manager.orientation == RecyclerView.VERTICAL) {
584
- if (animated) recyclerView.smoothScrollBy(0, delta) else recyclerView.scrollBy(0, delta)
719
+ recyclerView.smoothScrollBy(0, delta)
585
720
  } else {
586
- if (animated) recyclerView.smoothScrollBy(delta, 0) else recyclerView.scrollBy(delta, 0)
721
+ recyclerView.smoothScrollBy(delta, 0)
587
722
  }
588
723
  }
589
724
 
@@ -714,6 +849,7 @@ class NativeListView(
714
849
  reorderTouchHandler = null
715
850
  reorderTouchListener?.let(recyclerView::removeOnItemTouchListener)
716
851
  reorderTouchListener = null
852
+ recyclerView.removeOnItemTouchListener(pagerGestureTouchListener)
717
853
  itemTouchHelper?.attachToRecyclerView(null)
718
854
  itemTouchHelper = null
719
855
  footerView.recycle()
@@ -731,7 +867,8 @@ class NativeListView(
731
867
  val horizontalPadding = next.contentPaddingHorizontal ?: defaultPadding
732
868
  val topPadding = next.contentPaddingTop ?: defaultPadding
733
869
  val bottomPadding = next.contentPaddingBottom ?: defaultPadding
734
- val indexGutter = if (sectionIndexEntries.isEmpty()) 0 else 48
870
+ // OneKey patch: the section index overlays rows and keeps only an accessory-safe inset.
871
+ val indexGutter = if (sectionIndexEntries.isEmpty()) 0 else SECTION_INDEX_CONTENT_INSET_DP
735
872
  recyclerView.setPaddingRelative(
736
873
  dp(horizontalPadding),
737
874
  dp(topPadding),
@@ -742,7 +879,7 @@ class NativeListView(
742
879
  recyclerView.isVerticalScrollBarEnabled = sectionIndexEntries.isEmpty()
743
880
 
744
881
  spacingDecoration?.let(recyclerView::removeItemDecoration)
745
- spacingDecoration = ItemSpacingDecoration(dp(next.itemSpacing)).also(recyclerView::addItemDecoration)
882
+ spacingDecoration = ItemSpacingDecoration(dp(next.itemSpacing), next.itemSpacing, density).also(recyclerView::addItemDecoration)
746
883
  stickyDecoration?.let(recyclerView::removeItemDecoration)
747
884
  stickyDecoration = if (next.stickyHeaders && orientation == RecyclerView.VERTICAL) {
748
885
  StickySectionHeaderDecoration(adapter, context, next.theme, density).also(recyclerView::addItemDecoration)
@@ -769,12 +906,13 @@ class NativeListView(
769
906
  sectionIndexEntries.map { it.title },
770
907
  themeColor(next.theme, "secondaryText", "#646464"),
771
908
  themeColor(next.theme, "accent", "#108303"),
909
+ themeColor(next.theme, "inverseText", "#FCFCFC"),
772
910
  )
773
911
  sectionIndexView.visibility = if (sectionIndexEntries.isEmpty()) GONE else VISIBLE
774
912
  sectionIndexPreview.setTextColor(themeColor(next.theme, "inverseText", "#FCFCFC"))
775
913
  sectionIndexPreview.background = GradientDrawable().apply {
776
914
  setColor(themeColor(next.theme, "inverseBackground", "#202020"))
777
- cornerRadius = dp(16).toFloat()
915
+ cornerRadius = dp(14).toFloat()
778
916
  }
779
917
  sectionIndexView.setActiveIndex(
780
918
  previousKey?.let { key -> sectionIndexEntries.indexOfFirst { it.key == key }.takeIf { it >= 0 } },
@@ -789,6 +927,10 @@ class NativeListView(
789
927
  sectionIndexView.setActiveIndex(index)
790
928
  recyclerView.stopScroll()
791
929
  scrollToIndex(entry.position, animated = false, alignment = "start")
930
+ recyclerView.post {
931
+ sectionIndexProgrammaticScroll = false
932
+ syncSectionIndexToVisibleRows()
933
+ }
792
934
  if (interacting) {
793
935
  sectionIndexPreview.animate().cancel()
794
936
  sectionIndexPreview.text = entry.title
@@ -803,6 +945,7 @@ class NativeListView(
803
945
  private fun finishSectionIndexInteraction(immediately: Boolean = false) {
804
946
  sectionIndexScrubbing = false
805
947
  if (immediately) sectionIndexProgrammaticScroll = false
948
+ syncSectionIndexToVisibleRows()
806
949
  sectionIndexPreview.animate().cancel()
807
950
  if (immediately) {
808
951
  sectionIndexPreview.alpha = 0f
@@ -846,6 +989,7 @@ class NativeListView(
846
989
  null,
847
990
  next.selectedKeys.contains(footer.key),
848
991
  ::resolveCheckboxState,
992
+ useSourceScale = usesSelectorSourceScale,
849
993
  )
850
994
  }
851
995
  }
@@ -898,15 +1042,17 @@ class NativeListView(
898
1042
  origin.sourceView.getLocationInWindow(location)
899
1043
  val record = ActionAnchorRecord(token, origin)
900
1044
  actionAnchor = record
1045
+ // OneKey patch: selector menus anchor to the glyph slot, not its expanded press target.
1046
+ val anchorInset = origin.anchorInsetPixels
901
1047
  return JSONObject()
902
1048
  .put("token", token)
903
1049
  .put(
904
1050
  "windowRect",
905
1051
  JSONObject()
906
- .put("x", location[0] / density)
907
- .put("y", location[1] / density)
908
- .put("width", origin.sourceView.width / density)
909
- .put("height", origin.sourceView.height / density),
1052
+ .put("x", (location[0] + anchorInset) / density)
1053
+ .put("y", (location[1] + anchorInset) / density)
1054
+ .put("width", (origin.sourceView.width - anchorInset * 2) / density)
1055
+ .put("height", (origin.sourceView.height - anchorInset * 2) / density),
910
1056
  )
911
1057
  .put("source", origin.source)
912
1058
  .put("generation", generation)
@@ -1037,7 +1183,9 @@ class NativeListView(
1037
1183
  current.theme,
1038
1184
  current.layout,
1039
1185
  position,
1040
- current.selectedKeys.contains(item.key),
1186
+ // OneKey patch: match the full binder when the snapshot carries explicit row selection.
1187
+ // current.selectedKeys.contains(item.key),
1188
+ item.json.optBoolean("selected", false) || current.selectedKeys.contains(item.key),
1041
1189
  ::resolveCheckboxState,
1042
1190
  )
1043
1191
  }
@@ -1297,12 +1445,14 @@ class NativeListView(
1297
1445
  ?.let(recyclerView::getChildViewHolder)
1298
1446
  ?.takeIf { holder ->
1299
1447
  adapter.itemAt(holder.bindingAdapterPosition)?.let { item ->
1300
- val holderLocation = IntArray(2)
1301
- holder.itemView.getLocationOnScreen(holderLocation)
1302
- item.isReorderable && (
1303
- item.type != "walletGroup" ||
1304
- event.rawY in holderLocation[1].toFloat()..(holderLocation[1] + dp(68)).toFloat()
1305
- )
1448
+ // OneKey patch: any wallet group member can initiate the group drag.
1449
+ // val holderLocation = IntArray(2)
1450
+ // holder.itemView.getLocationOnScreen(holderLocation)
1451
+ // item.isReorderable && (
1452
+ // item.type != "walletGroup" ||
1453
+ // event.rawY in holderLocation[1].toFloat()..(holderLocation[1] + dp(68)).toFloat()
1454
+ // )
1455
+ item.isReorderable
1306
1456
  } == true
1307
1457
  }
1308
1458
  if (candidate != null) handler.postDelayed(startDrag, REORDER_LONG_PRESS_MS)
@@ -1460,9 +1610,13 @@ class NativeListView(
1460
1610
  )
1461
1611
  }
1462
1612
 
1463
- private fun dp(value: Int): Int = NativeListScale.dp(resources, value)
1613
+ private fun dp(value: Int): Int = if (usesSelectorSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
1464
1614
 
1465
1615
  companion object {
1616
+ private const val SECTION_INDEX_CONTENT_INSET_DP = 16
1617
+ private const val SECTION_INDEX_RAIL_WIDTH_DP = 32
1618
+ private const val SECTION_INDEX_PREVIEW_SIZE_DP = 48
1619
+ private const val SECTION_INDEX_PREVIEW_END_MARGIN_DP = 40
1466
1620
  private const val REORDER_LONG_PRESS_MS = 200L
1467
1621
  private const val REORDER_ALLOWABLE_MOVEMENT_DP = 10
1468
1622
  private const val REORDER_PLACEHOLDER_INSET_DP = 8
@@ -1546,7 +1700,9 @@ private class NativeListSectionIndexView(
1546
1700
  private var titles: List<String> = emptyList()
1547
1701
  private var normalColor = Color.GRAY
1548
1702
  private var activeColor = Color.BLACK
1703
+ private var activeTextColor = Color.WHITE
1549
1704
  private var lastTouchIndex: Int? = null
1705
+ private val activeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
1550
1706
  private val normalPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1551
1707
  textAlign = Paint.Align.CENTER
1552
1708
  typeface = NativeListFonts.medium(context)
@@ -1563,10 +1719,16 @@ private class NativeListSectionIndexView(
1563
1719
  contentDescription = ACCESSIBILITY_LABEL
1564
1720
  }
1565
1721
 
1566
- fun configure(titles: List<String>, normalColor: Int, activeColor: Int) {
1722
+ fun configure(
1723
+ titles: List<String>,
1724
+ normalColor: Int,
1725
+ activeColor: Int,
1726
+ activeTextColor: Int,
1727
+ ) {
1567
1728
  this.titles = titles
1568
1729
  this.normalColor = normalColor
1569
1730
  this.activeColor = activeColor
1731
+ this.activeTextColor = activeTextColor
1570
1732
  activeIndex = null
1571
1733
  updateContentDescription()
1572
1734
  invalidate()
@@ -1590,8 +1752,30 @@ private class NativeListSectionIndexView(
1590
1752
  activePaint.color = activeColor
1591
1753
  activePaint.textSize = textSize
1592
1754
  titles.forEachIndexed { index, title ->
1593
- val paint = if (index == activeIndex) activePaint else normalPaint
1755
+ val active = index == activeIndex
1756
+ val paint = if (active) activePaint else normalPaint
1594
1757
  val centerY = originY + cellHeight * (index + 0.5f)
1758
+ if (active && cellHeight >= NativeListScale.dp(resources, 12f)) {
1759
+ val badgeWidth = NativeListScale.dp(resources, 20f)
1760
+ val badgeHeight = minOf(cellHeight, NativeListScale.dp(resources, 16f))
1761
+ activeBackgroundPaint.color = activeColor
1762
+ canvas.drawRoundRect(
1763
+ width / 2f - badgeWidth / 2f,
1764
+ centerY - badgeHeight / 2f,
1765
+ width / 2f + badgeWidth / 2f,
1766
+ centerY + badgeHeight / 2f,
1767
+ badgeHeight / 2f,
1768
+ badgeHeight / 2f,
1769
+ activeBackgroundPaint,
1770
+ )
1771
+ }
1772
+ paint.color = if (active && cellHeight >= NativeListScale.dp(resources, 12f)) {
1773
+ activeTextColor
1774
+ } else if (active) {
1775
+ activeColor
1776
+ } else {
1777
+ normalColor
1778
+ }
1595
1779
  val baseline = centerY - (paint.descent() + paint.ascent()) / 2f
1596
1780
  canvas.drawText(title, width / 2f, baseline, paint)
1597
1781
  }
@@ -1697,7 +1881,11 @@ private class NativeListSectionIndexView(
1697
1881
  }
1698
1882
  }
1699
1883
 
1700
- private class ItemSpacingDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
1884
+ private class ItemSpacingDecoration(
1885
+ private val spacing: Int,
1886
+ private val sourceSpacing: Int,
1887
+ private val density: Float,
1888
+ ) : RecyclerView.ItemDecoration() {
1701
1889
  override fun getItemOffsets(
1702
1890
  outRect: android.graphics.Rect,
1703
1891
  view: View,
@@ -1706,7 +1894,14 @@ private class ItemSpacingDecoration(private val spacing: Int) : RecyclerView.Ite
1706
1894
  ) {
1707
1895
  if (spacing <= 0) return
1708
1896
  val horizontal = (parent.layoutManager as? LinearLayoutManager)?.orientation == RecyclerView.HORIZONTAL
1709
- if (horizontal) outRect.right = spacing else outRect.bottom = spacing
1897
+ val item = view.tag as? NativeListItem
1898
+ val sourceWallet = item?.type == "identity" && item.json.optString("presentation") == "walletSidebar" && item.json.has("height")
1899
+ // OneKey patch: V1 measures the wallet and its bottom padding as one sortable row.
1900
+ val itemSpacing = if (!horizontal && sourceWallet) {
1901
+ val sourceHeight = item!!.json.optInt("height")
1902
+ ((sourceHeight + sourceSpacing) * density).roundToInt() - (sourceHeight * density).roundToInt()
1903
+ } else spacing
1904
+ if (horizontal) outRect.right = itemSpacing else outRect.bottom = itemSpacing
1710
1905
  }
1711
1906
  }
1712
1907
 
@@ -1733,22 +1928,32 @@ private class StickySectionHeaderDecoration(
1733
1928
  for (index in first downTo 0) {
1734
1929
  val candidate = adapter.itemAt(index)
1735
1930
  if (candidate?.type == "sectionHeader") {
1736
- if (candidate.json.optString("variant") == "summary") continue
1931
+ if (candidate.json.optString("variant") == "summary" || !candidate.json.optBoolean("sticky", true)) continue
1737
1932
  header = candidate.takeIf(::isSimpleStickySectionHeader)
1738
1933
  break
1739
1934
  }
1740
1935
  }
1741
1936
  val item = header ?: return
1937
+ // OneKey patch: a pinned selector heading must use the same text rasterization as its row.
1938
+ textPaint.flags = if (item.usesSelectorSourceScale) Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG else Paint.ANTI_ALIAS_FLAG
1742
1939
  val isHistory = item.json.optString("variant") == "history" ||
1743
1940
  item.sectionKey?.startsWith("history-") == true
1744
- val height = NativeListScale.dp(context.resources, if (isHistory) 16 else 36)
1745
- val textSize = NativeListScale.font(context.resources, if (isHistory) 12f else 14f)
1746
- textPaint.textSize = textSize * density
1747
- val horizontalInset = NativeListScale.dp(context.resources, if (isHistory) 8 else 20).toFloat()
1941
+ val sourceHeight = item.json.optInt("height", 36) * density
1942
+ val height = if (item.usesSelectorSourceScale) {
1943
+ if (item.json.optString("heightRounding") == "nearest") sourceHeight.roundToInt() else sourceHeight.toInt()
1944
+ } else NativeListScale.dp(context.resources, if (isHistory) 16 else 36)
1945
+ val textSize = if (item.usesSelectorSourceScale) 14f else NativeListScale.font(context.resources, if (isHistory) 12f else 14f)
1946
+ textPaint.textSize = if (item.usesSelectorSourceScale) kotlin.math.ceil((textSize * density).toDouble()).toFloat() else textSize * density
1947
+ val horizontalInset = if (item.usesSelectorSourceScale) ((20 * density).toInt() - parent.paddingLeft).toFloat() else NativeListScale.dp(context.resources, if (isHistory) 8 else 20).toFloat()
1748
1948
  val left = parent.paddingLeft.toFloat()
1749
1949
  val right = (parent.width - parent.paddingRight).toFloat()
1750
1950
  canvas.drawRect(left, 0f, right, height.toFloat(), backgroundPaint)
1751
- val baseline = height / 2f - (textPaint.descent() + textPaint.ascent()) / 2f
1951
+ val baseline = if (item.usesSelectorSourceScale) {
1952
+ val metrics = textPaint.fontMetricsInt
1953
+ val lineHeight = kotlin.math.ceil(20 * density.toDouble()).toInt()
1954
+ val leading = lineHeight - (metrics.descent - metrics.ascent)
1955
+ (height - lineHeight) / 2 - metrics.ascent + kotlin.math.ceil(leading / 2.0).toFloat()
1956
+ } else height / 2f - (textPaint.descent() + textPaint.ascent()) / 2f
1752
1957
  val value = item.json.optString("title").let { if (isHistory) it.uppercase() else it }
1753
1958
  val isRightToLeft = parent.layoutDirection == View.LAYOUT_DIRECTION_RTL
1754
1959
  val textWidth = if (isHistory) {
@@ -1789,6 +1994,29 @@ private class StickySectionHeaderDecoration(
1789
1994
 
1790
1995
  internal fun isSimpleStickySectionHeader(item: NativeListItem): Boolean =
1791
1996
  item.type == "sectionHeader" &&
1997
+ item.json.optBoolean("sticky", true) &&
1792
1998
  item.json.optString("variant") != "summary" &&
1793
1999
  item.json.optString("value").isEmpty() &&
1794
2000
  item.json.optJSONObject("checkbox") == null
2001
+
2002
+ // OneKey patch: paint only explicitly requested backgrounds into list side padding.
2003
+ private class SelectorBackgroundDecoration(private val adapter: NativeListAdapter) : RecyclerView.ItemDecoration() {
2004
+ private val paint = Paint()
2005
+ override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
2006
+ for (index in 0 until parent.childCount) {
2007
+ val child = parent.getChildAt(index)
2008
+ val item = adapter.itemAt(parent.getChildAdapterPosition(child)) ?: continue
2009
+ if (!item.json.optBoolean("backgroundFullWidth", false)) continue
2010
+ val color = item.json.optString("backgroundColor")
2011
+ if (color.isEmpty()) continue
2012
+ paint.color = try { parseNativeListColor(color) } catch (_: IllegalArgumentException) { Color.TRANSPARENT }
2013
+ val top = child.y
2014
+ canvas.drawRect(0f, top, parent.width.toFloat(), top + child.height, paint)
2015
+ if (item.type == "system" && item.json.optString("variant") == "warning") {
2016
+ paint.color = try { parseNativeListColor(item.json.optString("borderColor", "#E0E0E0")) } catch (_: IllegalArgumentException) { Color.TRANSPARENT }
2017
+ canvas.drawRect(0f, top, parent.width.toFloat(), top + 1, paint)
2018
+ canvas.drawRect(0f, top + child.height - 1, parent.width.toFloat(), top + child.height, paint)
2019
+ }
2020
+ }
2021
+ }
2022
+ }
@@ -21,7 +21,14 @@ final class HybridNativeList: HybridNativeListSpec {
21
21
  var onSelectionDelta: ((_ payloadJson: String) -> Void)?
22
22
  var onReorder: ((_ payloadJson: String) -> Void)?
23
23
  var onEndReached: ((_ payloadJson: String) -> Void)?
24
- var onVisibleRangeChanged: ((_ payloadJson: String) -> Void)?
24
+ var onVisibleRangeChanged: ((_ payloadJson: String) -> Void)? {
25
+ didSet {
26
+ runOnMain { [weak self] in
27
+ guard let self else { return }
28
+ self.hostView.onVisibleRangeChanged = self.onVisibleRangeChanged
29
+ }
30
+ }
31
+ }
25
32
 
26
33
  override init() {
27
34
  super.init()
@@ -30,7 +37,6 @@ final class HybridNativeList: HybridNativeListSpec {
30
37
  hostView.onSelectionDelta = { [weak self] in self?.onSelectionDelta?($0) }
31
38
  hostView.onReorder = { [weak self] in self?.onReorder?($0) }
32
39
  hostView.onEndReached = { [weak self] in self?.onEndReached?($0) }
33
- hostView.onVisibleRangeChanged = { [weak self] in self?.onVisibleRangeChanged?($0) }
34
40
  }
35
41
 
36
42
  func applySnapshot(snapshotJson: String) throws {