@onekeyfe/react-native-native-list 3.0.128 → 3.0.130

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 (62) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +15 -0
  2. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +18 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +169 -10
  4. package/ios/HybridNativeList.swift +20 -0
  5. package/ios/RNCNativeListView.swift +106 -3
  6. package/lib/module/NativeList.js +6 -1
  7. package/lib/module/NativeList.web.js +2 -0
  8. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  9. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  10. package/lib/nitrogen/generated/android/c++/JNativeListKeyboardDismissMode.hpp +61 -0
  11. package/lib/nitrogen/generated/android/c++/JNativeListKeyboardShouldPersistTaps.hpp +61 -0
  12. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  13. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +12 -0
  14. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListKeyboardDismissMode.kt +24 -0
  15. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListKeyboardShouldPersistTaps.kt +24 -0
  16. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +6 -0
  17. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +20 -0
  18. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  19. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  20. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +22 -0
  21. package/lib/nitrogen/generated/ios/swift/NativeListKeyboardDismissMode.swift +44 -0
  22. package/lib/nitrogen/generated/ios/swift/NativeListKeyboardShouldPersistTaps.swift +44 -0
  23. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  24. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +10 -0
  25. package/lib/nitrogen/generated/shared/c++/NativeListKeyboardDismissMode.hpp +80 -0
  26. package/lib/nitrogen/generated/shared/c++/NativeListKeyboardShouldPersistTaps.hpp +80 -0
  27. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  28. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +8 -0
  29. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  30. package/lib/typescript/src/NativeList.d.ts +1 -1
  31. package/lib/typescript/src/NativeList.nitro.d.ts +4 -0
  32. package/lib/typescript/src/NativeList.types.d.ts +9 -0
  33. package/lib/typescript/src/NativeList.web.d.ts +1 -1
  34. package/lib/typescript/src/index.d.ts +1 -1
  35. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  36. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  37. package/nitrogen/generated/android/c++/JNativeListKeyboardDismissMode.hpp +61 -0
  38. package/nitrogen/generated/android/c++/JNativeListKeyboardShouldPersistTaps.hpp +61 -0
  39. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +12 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListKeyboardDismissMode.kt +24 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListKeyboardShouldPersistTaps.kt +24 -0
  43. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +6 -0
  44. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +20 -0
  45. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  46. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  47. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +22 -0
  48. package/nitrogen/generated/ios/swift/NativeListKeyboardDismissMode.swift +44 -0
  49. package/nitrogen/generated/ios/swift/NativeListKeyboardShouldPersistTaps.swift +44 -0
  50. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  51. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +10 -0
  52. package/nitrogen/generated/shared/c++/NativeListKeyboardDismissMode.hpp +80 -0
  53. package/nitrogen/generated/shared/c++/NativeListKeyboardShouldPersistTaps.hpp +80 -0
  54. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  55. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +8 -0
  56. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  57. package/package.json +3 -3
  58. package/src/NativeList.nitro.ts +9 -0
  59. package/src/NativeList.tsx +12 -0
  60. package/src/NativeList.types.ts +14 -0
  61. package/src/NativeList.web.tsx +4 -0
  62. package/src/index.ts +2 -0
@@ -19,6 +19,21 @@ class HybridNativeList(context: ThemedReactContext) : HybridNativeListSpec() {
19
19
  dispatchToUi { hostView.applySnapshot(value) }
20
20
  }
21
21
 
22
+ override var keyboardDismissMode: NativeListKeyboardDismissMode = NativeListKeyboardDismissMode.NONE
23
+ set(value) {
24
+ if (field == value) return
25
+ field = value
26
+ dispatchToUi { hostView.keyboardDismissMode = value }
27
+ }
28
+
29
+ override var keyboardShouldPersistTaps: NativeListKeyboardShouldPersistTaps =
30
+ NativeListKeyboardShouldPersistTaps.NEVER
31
+ set(value) {
32
+ if (field == value) return
33
+ field = value
34
+ dispatchToUi { hostView.keyboardShouldPersistTaps = value }
35
+ }
36
+
22
37
  override var onRowAction: ((payloadJson: String) -> Unit)? = null
23
38
  set(value) { field = value; hostView.onRowAction = value }
24
39
  override var onActionAnchorInvalidated: ((payloadJson: String) -> Unit)? = null
@@ -876,6 +876,7 @@ internal class NativeListRowView(
876
876
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
877
877
  useSourceScale: Boolean = false,
878
878
  ) {
879
+ val reusesImageIdentity = boundKey == item.key && (tag as? NativeListItem)?.type == item.type
879
880
  val shouldRestorePressed = touchPressed && boundKey == item.key
880
881
  cancelMarketLongPress()
881
882
  marketLongPressFired = false
@@ -887,10 +888,13 @@ internal class NativeListRowView(
887
888
  tag = item
888
889
  selectorUsesSourceScale = item.type == "market" || item.type == "system" && item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry" || item.usesSelectorSourceScale || useSourceScale
889
890
  reorderActive = false
890
- leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
891
- secondaryImage.prepareForReuse()
892
- mediaNetworkImage.prepareForReuse()
893
- metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
891
+ if (!reusesImageIdentity) {
892
+ selectorImages.forEach(OneKeyImageReusableView::prepareForReuse)
893
+ leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
894
+ secondaryImage.prepareForReuse()
895
+ mediaNetworkImage.prepareForReuse()
896
+ metricVisualImages.forEach(OneKeyImageReusableView::prepareForReuse)
897
+ }
894
898
  resetViews()
895
899
 
896
900
  val primary = color(theme, "primaryText", "#000000DF")
@@ -1014,6 +1018,7 @@ internal class NativeListRowView(
1014
1018
  restoreRestingBackground()
1015
1019
  invalidateCurrentBinding()
1016
1020
  boundKey = null
1021
+ selectorImages.forEach(OneKeyImageReusableView::prepareForReuse)
1017
1022
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
1018
1023
  secondaryImage.prepareForReuse()
1019
1024
  mediaNetworkImage.prepareForReuse()
@@ -1185,11 +1190,10 @@ internal class NativeListRowView(
1185
1190
  marketOriginalPaintFlags.clear()
1186
1191
  selectorLineHeights.clear()
1187
1192
  selectorFontSizes.clear()
1193
+ selectorImages.forEach { (it.parent as? ViewGroup)?.removeView(it) }
1188
1194
  // OneKey patch: selector-only views cannot survive a recycled binding.
1189
1195
  selectorViews.forEach { (it.parent as? ViewGroup)?.removeView(it) }
1190
1196
  selectorViews.clear()
1191
- selectorImages.forEach(OneKeyImageReusableView::dispose)
1192
- selectorImages.clear()
1193
1197
  selectorHeight = null
1194
1198
  marketBadgeViews.forEachIndexed { index, badge ->
1195
1199
  (badge.parent as? ViewGroup)?.removeView(badge)
@@ -3386,9 +3390,8 @@ internal class NativeListRowView(
3386
3390
  }
3387
3391
  val image = overlay.optJSONObject("image")
3388
3392
  val view = when {
3389
- image != null -> OneKeyImageReusableView(reactContext).also {
3393
+ image != null -> (selectorImages.getOrNull(index) ?: OneKeyImageReusableView(reactContext).also(selectorImages::add)).also {
3390
3394
  bindImage(image, it, boundKey ?: "", 10 + index, "generic", hideUntilLoaded = true)
3391
- selectorImages.add(it)
3392
3395
  }
3393
3396
  overlay.optString("text").isNotEmpty() -> TextView(context).apply {
3394
3397
  text = overlay.optString("text")
@@ -4141,7 +4144,11 @@ internal class NativeListRowView(
4141
4144
  val expectedEpoch = bindingEpoch
4142
4145
  val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
4143
4146
  val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
4144
- if (hideUntilLoaded) imageView.visibility = INVISIBLE
4147
+ val sourceHeadersJson = source.optJSONObject("headers")?.toString()
4148
+ val recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt"
4149
+ if (hideUntilLoaded && !imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)) {
4150
+ imageView.visibility = INVISIBLE
4151
+ }
4145
4152
  val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
4146
4153
  if (bindingEpoch == expectedEpoch) {
4147
4154
  imageView.visibility = VISIBLE
@@ -4156,12 +4163,12 @@ internal class NativeListRowView(
4156
4163
  }) else onError
4157
4164
  imageView.configure(
4158
4165
  sourceUri = uri,
4159
- sourceHeadersJson = source.optJSONObject("headers")?.toString(),
4166
+ sourceHeadersJson = sourceHeadersJson,
4160
4167
  variant = variant,
4161
4168
  contentFit = source.optString("contentFit", "cover"),
4162
4169
  cachePolicy = source.optString("cachePolicy", "memory-disk"),
4163
4170
  autoplay = source.optBoolean("autoplay", false),
4164
- recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt",
4171
+ recyclingKey = recyclingKey,
4165
4172
  optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
4166
4173
  overscan = source.optDouble("overscan", 1.1),
4167
4174
  loadingStrategy = source.optString("loadingStrategy", "none"),
@@ -1,10 +1,12 @@
1
1
  package com.margelo.nitro.nativelist
2
2
 
3
3
  import android.animation.TimeInterpolator
4
+ import android.content.Context
4
5
  import android.graphics.Canvas
5
6
  import android.graphics.Color
6
7
  import android.graphics.Paint
7
8
  import android.graphics.Path
9
+ import android.graphics.Rect
8
10
  import android.graphics.RectF
9
11
  import android.graphics.drawable.ShapeDrawable
10
12
  import android.graphics.drawable.shapes.PathShape
@@ -19,12 +21,16 @@ import android.view.MotionEvent
19
21
  import android.view.View
20
22
  import android.view.ViewConfiguration
21
23
  import android.view.ViewGroup
24
+ import android.view.ViewTreeObserver
22
25
  import android.view.accessibility.AccessibilityEvent
23
26
  import android.view.accessibility.AccessibilityNodeInfo
27
+ import android.view.inputmethod.InputMethodManager
24
28
  import android.widget.FrameLayout
25
29
  import android.widget.LinearLayout
26
30
  import android.widget.SeekBar
27
31
  import android.widget.TextView
32
+ import androidx.core.view.ViewCompat
33
+ import androidx.core.view.WindowInsetsCompat
28
34
  import androidx.recyclerview.widget.GridLayoutManager
29
35
  import androidx.recyclerview.widget.ItemTouchHelper
30
36
  import androidx.recyclerview.widget.LinearLayoutManager
@@ -93,6 +99,8 @@ class NativeListView(
93
99
  var onReorder: ((String) -> Unit)? = null
94
100
  var onEndReached: ((String) -> Unit)? = null
95
101
  var onVisibleRangeChanged: ((String) -> Unit)? = null
102
+ var keyboardDismissMode = NativeListKeyboardDismissMode.NONE
103
+ var keyboardShouldPersistTaps = NativeListKeyboardShouldPersistTaps.NEVER
96
104
  private val density = resources.displayMetrics.density
97
105
  private val recyclerView = RecyclerView(context)
98
106
  private var configuredTopPaddingPx = 0
@@ -105,6 +113,7 @@ class NativeListView(
105
113
  private val layoutManager = GridLayoutManager(context, 1)
106
114
  // OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
107
115
  private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
116
+ private var pagerGestureIsVertical = false
108
117
  private val pagerGestureTouchListener = object : RecyclerView.SimpleOnItemTouchListener() {
109
118
  private var downX = 0f
110
119
  private var downY = 0f
@@ -117,22 +126,77 @@ class NativeListView(
117
126
  downX = event.x
118
127
  downY = event.y
119
128
  directionResolved = false
129
+ pagerGestureIsVertical = false
120
130
  }
121
131
  MotionEvent.ACTION_MOVE -> if (!directionResolved) {
122
132
  val deltaX = abs(event.x - downX)
123
133
  val deltaY = abs(event.y - downY)
124
134
  if (max(deltaX, deltaY) > pagerGestureTouchSlop) {
125
135
  directionResolved = true
126
- recyclerView.parent?.requestDisallowInterceptTouchEvent(deltaY >= deltaX)
136
+ pagerGestureIsVertical = deltaY >= deltaX
137
+ recyclerView.parent?.requestDisallowInterceptTouchEvent(pagerGestureIsVertical)
138
+ if (
139
+ pagerGestureIsVertical &&
140
+ keyboardDismissMode == NativeListKeyboardDismissMode.ON_DRAG
141
+ ) {
142
+ dismissKeyboard()
143
+ }
127
144
  }
128
145
  }
129
146
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
130
147
  recyclerView.parent?.requestDisallowInterceptTouchEvent(false)
148
+ pagerGestureIsVertical = false
131
149
  }
132
150
  }
133
151
  return false
134
152
  }
135
153
  }
154
+ // Intercept only a completed tap so RecyclerView keeps drag and reorder ownership.
155
+ private val keyboardTapTouchListener = object : RecyclerView.SimpleOnItemTouchListener() {
156
+ private var downX = 0f
157
+ private var downY = 0f
158
+ private var observingTap = false
159
+ private var moved = false
160
+
161
+ override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean {
162
+ when (event.actionMasked) {
163
+ MotionEvent.ACTION_DOWN -> {
164
+ downX = event.x
165
+ downY = event.y
166
+ moved = false
167
+ observingTap =
168
+ keyboardShouldPersistTaps != NativeListKeyboardShouldPersistTaps.ALWAYS &&
169
+ focusedTextEditor() != null
170
+ }
171
+ MotionEvent.ACTION_MOVE -> if (
172
+ observingTap && hypot(event.x - downX, event.y - downY) > pagerGestureTouchSlop
173
+ ) {
174
+ moved = true
175
+ }
176
+ MotionEvent.ACTION_UP -> {
177
+ val shouldDismiss =
178
+ observingTap &&
179
+ !moved &&
180
+ reorderPlaceholderDecoration.position == RecyclerView.NO_POSITION &&
181
+ when (keyboardShouldPersistTaps) {
182
+ NativeListKeyboardShouldPersistTaps.NEVER -> true
183
+ NativeListKeyboardShouldPersistTaps.HANDLED ->
184
+ !keyboardTapIsHandled(recyclerView, event)
185
+ NativeListKeyboardShouldPersistTaps.ALWAYS -> false
186
+ }
187
+ observingTap = false
188
+ if (shouldDismiss) {
189
+ dismissKeyboard()
190
+ return true
191
+ }
192
+ }
193
+ MotionEvent.ACTION_CANCEL -> observingTap = false
194
+ }
195
+ return false
196
+ }
197
+
198
+ override fun onTouchEvent(recyclerView: RecyclerView, event: MotionEvent) = Unit
199
+ }
136
200
  private val reorderPlaceholderDecoration = ReorderPlaceholderDecoration(
137
201
  adapter = adapter,
138
202
  insetPx = dp(REORDER_PLACEHOLDER_INSET_DP),
@@ -169,6 +233,11 @@ class NativeListView(
169
233
  private var sectionIndexHapticsEnabled = true
170
234
  private val sectionIndexLocationOnScreen = IntArray(2)
171
235
  private val sectionIndexHostLocationOnScreen = IntArray(2)
236
+ private var sectionIndexObservedTree: ViewTreeObserver? = null
237
+ private val sectionIndexPreDrawListener = ViewTreeObserver.OnPreDrawListener {
238
+ updateSectionIndexAttachment()
239
+ true
240
+ }
172
241
  private var pendingScrollRequest: ScrollRequest? = null
173
242
  private val actionAnchorInstanceId = UUID.randomUUID().toString()
174
243
  private var actionAnchorCounter = 0L
@@ -178,12 +247,14 @@ class NativeListView(
178
247
  private var lastLayoutDirection = layoutDirection
179
248
  private var lastMarketPaginationAnchorLogAtMs = 0L
180
249
  private var disposed = false
250
+ private var resourcesDisposed = false
181
251
 
182
252
  init {
183
253
  orientation = VERTICAL
184
254
  recyclerView.adapter = adapter
185
255
  recyclerView.layoutManager = layoutManager
186
256
  recyclerView.itemAnimator = null
257
+ recyclerView.addOnItemTouchListener(keyboardTapTouchListener)
187
258
  recyclerView.addOnItemTouchListener(pagerGestureTouchListener)
188
259
  recyclerView.addItemDecoration(reorderPlaceholderDecoration)
189
260
  // OneKey patch: full-width selector header backgrounds do not change row content insets.
@@ -272,6 +343,12 @@ class NativeListView(
272
343
  RecyclerView.SCROLL_STATE_DRAGGING -> {
273
344
  sectionIndexProgrammaticScroll = false
274
345
  invalidateActionAnchor("scroll")
346
+ if (
347
+ keyboardDismissMode == NativeListKeyboardDismissMode.ON_DRAG &&
348
+ (layoutManager.orientation == RecyclerView.HORIZONTAL || pagerGestureIsVertical)
349
+ ) {
350
+ dismissKeyboard()
351
+ }
275
352
  }
276
353
  RecyclerView.SCROLL_STATE_IDLE -> sectionIndexProgrammaticScroll = false
277
354
  }
@@ -286,6 +363,44 @@ class NativeListView(
286
363
  })
287
364
  }
288
365
 
366
+ private fun focusedTextEditor(): View? {
367
+ val insets = ViewCompat.getRootWindowInsets(this)
368
+ if (insets != null && !insets.isVisible(WindowInsetsCompat.Type.ime())) return null
369
+ val focused = rootView.findFocus() ?: reactContext.currentActivity?.currentFocus
370
+ return focused?.takeIf(View::onCheckIsTextEditor)
371
+ }
372
+
373
+ private fun dismissKeyboard(): Boolean {
374
+ val focused = focusedTextEditor() ?: return false
375
+ val windowToken = focused.windowToken
376
+ focused.clearFocus()
377
+ val inputMethodManager =
378
+ context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
379
+ return inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
380
+ }
381
+
382
+ private fun keyboardTapIsHandled(recyclerView: RecyclerView, event: MotionEvent): Boolean {
383
+ val child = recyclerView.findChildViewUnder(event.x, event.y) ?: return false
384
+ val position = recyclerView.getChildAdapterPosition(child)
385
+ val item = adapter.itemAt(position) ?: return false
386
+ val clickable = clickableDescendantAt(child, event.rawX.toInt(), event.rawY.toInt())
387
+ val accessoryHandlesTap = clickable != null && clickable !== child
388
+ return accessoryHandlesTap ||
389
+ (!item.json.optBoolean("disabled", false) && !item.json.optBoolean("pressDisabled", false))
390
+ }
391
+
392
+ private fun clickableDescendantAt(view: View, rawX: Int, rawY: Int): View? {
393
+ if (!view.isShown) return null
394
+ val bounds = Rect()
395
+ if (!view.getGlobalVisibleRect(bounds) || !bounds.contains(rawX, rawY)) return null
396
+ if (view is ViewGroup) {
397
+ for (index in view.childCount - 1 downTo 0) {
398
+ clickableDescendantAt(view.getChildAt(index), rawX, rawY)?.let { return it }
399
+ }
400
+ }
401
+ return view.takeIf { it.isEnabled && it.isClickable }
402
+ }
403
+
289
404
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
290
405
  super.onLayout(changed, left, top, right, bottom)
291
406
  val first = config?.items?.firstOrNull()
@@ -328,8 +443,10 @@ class NativeListView(
328
443
  }
329
444
 
330
445
  override fun onDetachedFromWindow() {
446
+ stopSectionIndexAttachmentTracking()
331
447
  attachSectionIndexToList()
332
448
  updateRefreshIndicatorOffset(0)
449
+ if (disposed) disposeResources()
333
450
  super.onDetachedFromWindow()
334
451
  }
335
452
 
@@ -954,17 +1071,25 @@ class NativeListView(
954
1071
 
955
1072
  fun dispose() {
956
1073
  if (disposed) return
1074
+ disposed = true
1075
+ if (!isAttachedToWindow) disposeResources()
1076
+ }
1077
+
1078
+ private fun disposeResources() {
1079
+ if (resourcesDisposed) return
1080
+ resourcesDisposed = true
1081
+ stopSectionIndexAttachmentTracking()
957
1082
  attachSectionIndexToList()
958
1083
  stopReorderRelayoutLoop()
959
1084
  invalidateActionAnchor("destroy")
960
1085
  actionAnchor = null
961
- disposed = true
962
1086
  pendingScrollRequest = null
963
1087
  visibleEventScheduled = false
964
1088
  reorderTouchHandler?.removeCallbacksAndMessages(null)
965
1089
  reorderTouchHandler = null
966
1090
  reorderTouchListener?.let(recyclerView::removeOnItemTouchListener)
967
1091
  reorderTouchListener = null
1092
+ recyclerView.removeOnItemTouchListener(keyboardTapTouchListener)
968
1093
  recyclerView.removeOnItemTouchListener(pagerGestureTouchListener)
969
1094
  itemTouchHelper?.attachToRecyclerView(null)
970
1095
  itemTouchHelper = null
@@ -1093,16 +1218,21 @@ class NativeListView(
1093
1218
 
1094
1219
  private fun updateSectionIndexAttachment() {
1095
1220
  val windowHost = rootView as? FrameLayout
1096
- val useWindowHost = config?.sectionIndexCenteredInWindow == true &&
1221
+ val canUseWindowHost = config?.sectionIndexCenteredInWindow == true &&
1097
1222
  sectionIndexEntries.isNotEmpty() &&
1098
1223
  isAttachedToWindow &&
1099
- isShown &&
1100
1224
  width > 0 &&
1101
1225
  height > 0 &&
1102
1226
  windowHost != null &&
1103
1227
  windowHost.width > 0 &&
1104
1228
  windowHost.height > 0 &&
1105
1229
  windowHost !== contentContainer
1230
+ if (canUseWindowHost) {
1231
+ startSectionIndexAttachmentTracking()
1232
+ } else {
1233
+ stopSectionIndexAttachmentTracking()
1234
+ }
1235
+ val useWindowHost = canUseWindowHost && isShown
1106
1236
  if (useWindowHost) {
1107
1237
  attachSectionIndexToWindow(windowHost)
1108
1238
  } else {
@@ -1110,9 +1240,25 @@ class NativeListView(
1110
1240
  }
1111
1241
  }
1112
1242
 
1243
+ private fun startSectionIndexAttachmentTracking() {
1244
+ val observer = viewTreeObserver
1245
+ if (sectionIndexObservedTree === observer) return
1246
+ sectionIndexObservedTree?.takeIf(ViewTreeObserver::isAlive)
1247
+ ?.removeOnPreDrawListener(sectionIndexPreDrawListener)
1248
+ observer.addOnPreDrawListener(sectionIndexPreDrawListener)
1249
+ sectionIndexObservedTree = observer
1250
+ }
1251
+
1252
+ private fun stopSectionIndexAttachmentTracking() {
1253
+ sectionIndexObservedTree?.takeIf(ViewTreeObserver::isAlive)
1254
+ ?.removeOnPreDrawListener(sectionIndexPreDrawListener)
1255
+ sectionIndexObservedTree = null
1256
+ }
1257
+
1113
1258
  private fun attachSectionIndexToList() {
1114
1259
  if (sectionIndexView.parent === contentContainer) return
1115
1260
  (sectionIndexView.parent as? ViewGroup)?.removeView(sectionIndexView)
1261
+ sectionIndexView.translationX = 0f
1116
1262
  contentContainer.addView(
1117
1263
  sectionIndexView,
1118
1264
  FrameLayout.LayoutParams(
@@ -1129,27 +1275,40 @@ class NativeListView(
1129
1275
  windowHost.getLocationOnScreen(sectionIndexHostLocationOnScreen)
1130
1276
  getLocationOnScreen(sectionIndexLocationOnScreen)
1131
1277
  val listLeft = sectionIndexLocationOnScreen[0] - sectionIndexHostLocationOnScreen[0]
1132
- val maximumEndMargin = (windowHost.width - railWidth).coerceAtLeast(0)
1133
- val endMargin = if (layoutDirection == LAYOUT_DIRECTION_RTL) {
1278
+ val targetRailLeft = if (layoutDirection == LAYOUT_DIRECTION_RTL) {
1134
1279
  listLeft
1135
1280
  } else {
1136
- windowHost.width - listLeft - width
1137
- }.coerceIn(0, maximumEndMargin)
1281
+ listLeft + width - railWidth
1282
+ }
1283
+ val restingRailLeft = if (layoutDirection == LAYOUT_DIRECTION_RTL) {
1284
+ 0
1285
+ } else {
1286
+ windowHost.width - railWidth
1287
+ }
1138
1288
  val layoutParams = FrameLayout.LayoutParams(
1139
1289
  railWidth,
1140
1290
  railHeight,
1141
1291
  Gravity.TOP or Gravity.END,
1142
1292
  ).apply {
1143
- marginEnd = endMargin
1144
1293
  topMargin = (windowHost.height - railHeight) / 2
1145
1294
  }
1146
1295
  if (sectionIndexView.parent !== windowHost) {
1147
1296
  (sectionIndexView.parent as? ViewGroup)?.removeView(sectionIndexView)
1148
1297
  windowHost.addView(sectionIndexView, layoutParams)
1149
1298
  sectionIndexView.bringToFront()
1150
- } else {
1299
+ } else if (!sectionIndexView.hasSameWindowLayout(layoutParams)) {
1151
1300
  sectionIndexView.layoutParams = layoutParams
1152
1301
  }
1302
+ sectionIndexView.translationX = (targetRailLeft - restingRailLeft).toFloat()
1303
+ }
1304
+
1305
+ private fun View.hasSameWindowLayout(next: FrameLayout.LayoutParams): Boolean {
1306
+ val current = layoutParams as? FrameLayout.LayoutParams ?: return false
1307
+ return current.width == next.width &&
1308
+ current.height == next.height &&
1309
+ current.gravity == next.gravity &&
1310
+ current.marginEnd == next.marginEnd &&
1311
+ current.topMargin == next.topMargin
1153
1312
  }
1154
1313
 
1155
1314
  private fun sectionIndexPreviewBackground(): ShapeDrawable {
@@ -16,6 +16,26 @@ final class HybridNativeList: HybridNativeListSpec {
16
16
  }
17
17
  }
18
18
 
19
+ var keyboardDismissMode: NativeListKeyboardDismissMode = .none {
20
+ didSet {
21
+ guard keyboardDismissMode != oldValue else { return }
22
+ runOnMain { [weak self] in
23
+ guard let self else { return }
24
+ self.hostView.setKeyboardDismissMode(self.keyboardDismissMode)
25
+ }
26
+ }
27
+ }
28
+
29
+ var keyboardShouldPersistTaps: NativeListKeyboardShouldPersistTaps = .never {
30
+ didSet {
31
+ guard keyboardShouldPersistTaps != oldValue else { return }
32
+ runOnMain { [weak self] in
33
+ guard let self else { return }
34
+ self.hostView.setKeyboardShouldPersistTaps(self.keyboardShouldPersistTaps)
35
+ }
36
+ }
37
+ }
38
+
19
39
  var onRowAction: ((_ payloadJson: String) -> Void)?
20
40
  var onActionAnchorInvalidated: ((_ payloadJson: String) -> Void)?
21
41
  var onSelectionDelta: ((_ payloadJson: String) -> Void)?
@@ -89,8 +89,18 @@ final class NativeListView: UIView {
89
89
  target: self,
90
90
  action: #selector(marketLongPressChanged(_:))
91
91
  )
92
+ private lazy var keyboardTapRecognizer = UITapGestureRecognizer(
93
+ target: self,
94
+ action: #selector(keyboardTapRecognized(_:))
95
+ )
96
+ private lazy var keyboardDragRecognizer = UIPanGestureRecognizer(
97
+ target: self,
98
+ action: #selector(keyboardDragRecognized(_:))
99
+ )
92
100
  // OneKey patch: claim only vertical drags so held rows and ancestor pagers stay responsive.
93
101
  private lazy var listBodyGestureGuard = UIPanGestureRecognizer(target: nil, action: nil)
102
+ private var keyboardDismissMode: NativeListKeyboardDismissMode = .none
103
+ private var keyboardShouldPersistTaps: NativeListKeyboardShouldPersistTaps = .never
94
104
  private var interactiveReorderSource: (key: String, index: Int)?
95
105
  private weak var interactiveReorderCell: NativeListCell?
96
106
  private var interactiveReorderCompactKey: String?
@@ -123,6 +133,16 @@ final class NativeListView: UIView {
123
133
  collectionView.dragDelegate = self
124
134
  collectionView.dropDelegate = self
125
135
  collectionView.alwaysBounceVertical = true
136
+ keyboardTapRecognizer.cancelsTouchesInView = true
137
+ keyboardTapRecognizer.delegate = self
138
+ addGestureRecognizer(keyboardTapRecognizer)
139
+ keyboardDragRecognizer.cancelsTouchesInView = false
140
+ keyboardDragRecognizer.delegate = self
141
+ keyboardDragRecognizer.isEnabled = false
142
+ if #available(iOS 13.4, *) {
143
+ keyboardDragRecognizer.allowedScrollTypesMask = .all
144
+ }
145
+ collectionView.addGestureRecognizer(keyboardDragRecognizer)
126
146
  // OneKey patch: keep list-body drags from being claimed by an ancestor modal sheet.
127
147
  listBodyGestureGuard.cancelsTouchesInView = false
128
148
  listBodyGestureGuard.isEnabled = false
@@ -228,6 +248,60 @@ final class NativeListView: UIView {
228
248
  }
229
249
  }
230
250
 
251
+ func setKeyboardDismissMode(_ mode: NativeListKeyboardDismissMode) {
252
+ keyboardDismissMode = mode
253
+ keyboardDragRecognizer.isEnabled = mode == .onDrag
254
+ switch mode {
255
+ case .none:
256
+ collectionView.keyboardDismissMode = .none
257
+ case .onDrag:
258
+ collectionView.keyboardDismissMode = .onDrag
259
+ case .interactive:
260
+ collectionView.keyboardDismissMode = .interactive
261
+ }
262
+ }
263
+
264
+ func setKeyboardShouldPersistTaps(_ mode: NativeListKeyboardShouldPersistTaps) {
265
+ keyboardShouldPersistTaps = mode
266
+ }
267
+
268
+ @objc private func keyboardTapRecognized(_ gesture: UITapGestureRecognizer) {
269
+ guard gesture.state == .ended else { return }
270
+ window?.endEditing(true)
271
+ }
272
+
273
+ @objc private func keyboardDragRecognized(_ gesture: UIPanGestureRecognizer) {
274
+ guard gesture.state == .began, keyboardDismissMode == .onDrag else { return }
275
+ window?.endEditing(true)
276
+ }
277
+
278
+ private func focusedTextInput(in view: UIView?) -> UIView? {
279
+ guard let view else { return nil }
280
+ if view.isFirstResponder, view is any UITextInput { return view }
281
+ for child in view.subviews {
282
+ if let result = focusedTextInput(in: child) { return result }
283
+ }
284
+ return nil
285
+ }
286
+
287
+ private func keyboardTapIsHandled(_ touch: UITouch) -> Bool {
288
+ var touchedView = touch.view
289
+ while let current = touchedView, current !== self {
290
+ if let control = current as? UIControl, control.isEnabled { return true }
291
+ touchedView = current.superview
292
+ }
293
+ if let touchView = touch.view,
294
+ touchView === footerCell || touchView.isDescendant(of: footerCell) {
295
+ guard let footer = config?.fixedFooter else { return false }
296
+ return !footer.data.bool("disabled") && !footer.data.bool("pressDisabled")
297
+ }
298
+ let point = touch.location(in: collectionView)
299
+ guard collectionView.bounds.contains(point),
300
+ let indexPath = collectionView.indexPathForItem(at: point),
301
+ let item = item(at: indexPath) else { return false }
302
+ return !item.data.bool("disabled") && !item.data.bool("pressDisabled")
303
+ }
304
+
231
305
  required init?(coder: NSCoder) {
232
306
  fatalError("init(coder:) has not been implemented")
233
307
  }
@@ -1072,7 +1146,7 @@ final class NativeListView: UIView {
1072
1146
  attachSectionIndexToList()
1073
1147
  return
1074
1148
  }
1075
- attachSectionIndex(to: hostView)
1149
+ attachSectionIndex(to: hostView, centeredIn: window)
1076
1150
  }
1077
1151
 
1078
1152
  private var sectionIndexHostView: UIView? {
@@ -1110,7 +1184,7 @@ final class NativeListView: UIView {
1110
1184
  NSLayoutConstraint.activate(sectionIndexLayoutConstraints)
1111
1185
  }
1112
1186
 
1113
- private func attachSectionIndex(to hostView: UIView) {
1187
+ private func attachSectionIndex(to hostView: UIView, centeredIn window: UIWindow) {
1114
1188
  let railHeight = sectionIndexView.preferredHeight(constrainedTo: hostView.bounds.height)
1115
1189
  if sectionIndexView.superview === hostView {
1116
1190
  sectionIndexHostHeightConstraint?.constant = railHeight
@@ -1123,7 +1197,7 @@ final class NativeListView: UIView {
1123
1197
  sectionIndexHostHeightConstraint = heightConstraint
1124
1198
  sectionIndexLayoutConstraints = [
1125
1199
  sectionIndexView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
1126
- sectionIndexView.centerYAnchor.constraint(equalTo: hostView.centerYAnchor),
1200
+ sectionIndexView.centerYAnchor.constraint(equalTo: window.centerYAnchor),
1127
1201
  sectionIndexView.widthAnchor.constraint(equalToConstant: Self.sectionIndexRailWidth),
1128
1202
  heightConstraint,
1129
1203
  ]
@@ -1879,6 +1953,9 @@ final class NativeListView: UIView {
1879
1953
  }
1880
1954
 
1881
1955
  override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
1956
+ if gestureRecognizer === keyboardDragRecognizer {
1957
+ return keyboardDismissMode == .onDrag && focusedTextInput(in: window) != nil
1958
+ }
1882
1959
  if gestureRecognizer === reorderLongPress || gestureRecognizer === marketLongPress {
1883
1960
  let point = gestureRecognizer.location(in: collectionView)
1884
1961
  guard let indexPath = collectionView.indexPathForItem(at: point),
@@ -1898,6 +1975,20 @@ final class NativeListView: UIView {
1898
1975
 
1899
1976
  extension NativeListView: UIGestureRecognizerDelegate {
1900
1977
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
1978
+ if gestureRecognizer === keyboardDragRecognizer {
1979
+ return keyboardDismissMode == .onDrag && focusedTextInput(in: window) != nil
1980
+ }
1981
+ if gestureRecognizer === keyboardTapRecognizer {
1982
+ guard focusedTextInput(in: window) != nil else { return false }
1983
+ switch keyboardShouldPersistTaps {
1984
+ case .never:
1985
+ return true
1986
+ case .always:
1987
+ return false
1988
+ case .handled:
1989
+ return !keyboardTapIsHandled(touch)
1990
+ }
1991
+ }
1901
1992
  if gestureRecognizer === listBodyGestureGuard { return true }
1902
1993
  var view = touch.view
1903
1994
  while let current = view, current !== footerCell {
@@ -1911,6 +2002,18 @@ extension NativeListView: UIGestureRecognizerDelegate {
1911
2002
  _ gestureRecognizer: UIGestureRecognizer,
1912
2003
  shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
1913
2004
  ) -> Bool {
2005
+ if gestureRecognizer === keyboardDragRecognizer {
2006
+ return otherGestureRecognizer === collectionView.panGestureRecognizer ||
2007
+ otherGestureRecognizer === listBodyGestureGuard
2008
+ }
2009
+ if otherGestureRecognizer === keyboardDragRecognizer {
2010
+ return gestureRecognizer === collectionView.panGestureRecognizer ||
2011
+ gestureRecognizer === listBodyGestureGuard
2012
+ }
2013
+ if gestureRecognizer === keyboardTapRecognizer ||
2014
+ otherGestureRecognizer === keyboardTapRecognizer {
2015
+ return false
2016
+ }
1914
2017
  if gestureRecognizer === listBodyGestureGuard {
1915
2018
  guard let otherView = otherGestureRecognizer.view else { return false }
1916
2019
  return otherView === collectionView || otherView.isDescendant(of: collectionView)
@@ -27,6 +27,8 @@ function parsePayload(payloadJson) {
27
27
  export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
28
28
  snapshot,
29
29
  webVirtualizationEnabled: _webVirtualizationEnabled,
30
+ keyboardDismissMode = 'none',
31
+ keyboardShouldPersistTaps = 'never',
30
32
  onRowAction,
31
33
  onActionAnchorInvalidated,
32
34
  onSelectionDelta,
@@ -41,6 +43,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
41
43
  initialScrollViewOffset,
42
44
  ...viewProps
43
45
  }, forwardedRef) {
46
+ const normalizedKeyboardShouldPersistTaps = keyboardShouldPersistTaps === true ? 'always' : keyboardShouldPersistTaps === false ? 'never' : keyboardShouldPersistTaps;
44
47
  const nativeRef = useRef(null);
45
48
  const callbacksRef = useRef({
46
49
  onRowAction,
@@ -275,7 +278,9 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
275
278
  return /*#__PURE__*/_jsx(NativeListHost, {
276
279
  ...viewProps,
277
280
  ...nativeCallbacks,
278
- snapshotJson: snapshotJson
281
+ snapshotJson: snapshotJson,
282
+ keyboardDismissMode: keyboardDismissMode,
283
+ keyboardShouldPersistTaps: normalizedKeyboardShouldPersistTaps
279
284
  });
280
285
  });
281
286
  //# sourceMappingURL=NativeList.js.map
@@ -23,6 +23,8 @@ export function preloadNativeListAvatarImages(sources) {
23
23
  export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
24
24
  snapshot,
25
25
  webVirtualizationEnabled = true,
26
+ keyboardDismissMode: _keyboardDismissMode,
27
+ keyboardShouldPersistTaps: _keyboardShouldPersistTaps,
26
28
  onRowAction,
27
29
  onActionAnchorInvalidated,
28
30
  onSelectionDelta,