@onekeyfe/react-native-native-list 6.7.0-alpha.399 → 6.7.0-alpha.401

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.
@@ -46,6 +46,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
46
46
  import com.facebook.react.uimanager.ThemedReactContext
47
47
  import com.facebook.react.uimanager.PointerEvents
48
48
  import com.facebook.react.uimanager.ReactPointerEventsView
49
+ import com.facebook.react.uimanager.events.NativeGestureUtil
49
50
  import com.margelo.nitro.nativelogger.OneKeyLog
50
51
  import org.json.JSONArray
51
52
  import org.json.JSONObject
@@ -221,6 +222,14 @@ class NativeListView(
221
222
  if (isLayoutSuppressed) return
222
223
  super.onLayout(changed, left, top, right, bottom)
223
224
  }
225
+
226
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
227
+ val handled = super.dispatchTouchEvent(event)
228
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
229
+ finishPagerNativeGesture(event)
230
+ }
231
+ return handled
232
+ }
224
233
  }
225
234
  private var configuredTopPaddingPx = 0
226
235
  private var slotInsetPx = 0
@@ -1843,6 +1852,7 @@ class NativeListView(
1843
1852
  // OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
1844
1853
  private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
1845
1854
  private var pagerGestureIsVertical = false
1855
+ private var pagerNativeGestureStarted = false
1846
1856
  private val pagerGestureTouchListener = object : RecyclerView.SimpleOnItemTouchListener() {
1847
1857
  private var downX = 0f
1848
1858
  private var downY = 0f
@@ -1856,30 +1866,43 @@ class NativeListView(
1856
1866
  downY = event.y
1857
1867
  directionResolved = false
1858
1868
  pagerGestureIsVertical = false
1869
+ pagerNativeGestureStarted = false
1859
1870
  }
1860
- MotionEvent.ACTION_MOVE -> if (!directionResolved) {
1871
+ MotionEvent.ACTION_MOVE -> {
1861
1872
  val deltaX = abs(event.x - downX)
1862
1873
  val deltaY = abs(event.y - downY)
1863
- if (max(deltaX, deltaY) > pagerGestureTouchSlop) {
1874
+ if (!directionResolved && max(deltaX, deltaY) > pagerGestureTouchSlop) {
1864
1875
  directionResolved = true
1865
1876
  pagerGestureIsVertical = deltaY >= deltaX
1866
1877
  recyclerView.parent?.requestDisallowInterceptTouchEvent(pagerGestureIsVertical)
1867
- if (
1868
- pagerGestureIsVertical &&
1869
- keyboardDismissMode == NativeListKeyboardDismissMode.ON_DRAG
1870
- ) {
1878
+ } else if (!pagerGestureIsVertical && deltaY > pagerGestureTouchSlop && deltaY >= deltaX) {
1879
+ pagerGestureIsVertical = true
1880
+ recyclerView.parent?.requestDisallowInterceptTouchEvent(true)
1881
+ }
1882
+ if (pagerGestureIsVertical && !pagerNativeGestureStarted) {
1883
+ NativeGestureUtil.notifyNativeGestureStarted(this@NativeListView, event)
1884
+ pagerNativeGestureStarted = true
1885
+ if (keyboardDismissMode == NativeListKeyboardDismissMode.ON_DRAG) {
1871
1886
  dismissKeyboard()
1872
1887
  }
1873
1888
  }
1874
1889
  }
1875
1890
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
1876
- recyclerView.parent?.requestDisallowInterceptTouchEvent(false)
1877
- pagerGestureIsVertical = false
1891
+ finishPagerNativeGesture(event)
1878
1892
  }
1879
1893
  }
1880
1894
  return false
1881
1895
  }
1882
1896
  }
1897
+
1898
+ private fun finishPagerNativeGesture(event: MotionEvent) {
1899
+ if (pagerNativeGestureStarted) {
1900
+ NativeGestureUtil.notifyNativeGestureEnded(this, event)
1901
+ pagerNativeGestureStarted = false
1902
+ }
1903
+ recyclerView.parent?.requestDisallowInterceptTouchEvent(false)
1904
+ pagerGestureIsVertical = false
1905
+ }
1883
1906
  // Intercept only a completed tap so RecyclerView keeps drag and reorder ownership.
1884
1907
  private val keyboardTapTouchListener = object : RecyclerView.SimpleOnItemTouchListener() {
1885
1908
  private var downX = 0f
package/docs/SPEC.md CHANGED
@@ -384,6 +384,46 @@ image that already loaded. Common container styling clears what it wrote on the
384
384
  next bind, including properties it set on template children. UI mutation remains on the platform
385
385
  UI thread; [DESIGN.md](DESIGN.md) describes update and event ownership.
386
386
 
387
+ ### Android vertical-drag React touch cancellation
388
+
389
+ Status: implemented; four real Android MotionEvent/JSTouchDispatcher regressions
390
+ passed on a dedicated API 34 emulator on 2026-10-30. The focused regression is
391
+ in `native-tests/android/NativeListTouchDeliveryTest.kt`; build and run
392
+ instructions are in that directory's README. Physical-device press/scroll
393
+ acceptance remains separate.
394
+
395
+ This behavior applies to every vertical NativeList, including standalone lists
396
+ outside a CollapsiblePager and touches beginning on React container slots.
397
+ On the UI thread, movement exceeding Android touch slop with vertical
398
+ displacement at least equal to horizontal displacement confirms a vertical
399
+ drag. An initially horizontal movement may become vertical while the list still
400
+ receives the stream; once vertical, ownership stays vertical until release or
401
+ cancellation. Horizontal lists retain their existing touch behavior.
402
+
403
+ The list notifies its React root of native gesture ownership once per confirmed
404
+ drag, cancelling the active React touch before finger-up. Later MOVE events MUST
405
+ NOT repeat that notification. The listener does not consume the stream: native
406
+ RecyclerView scrolling continues. A stationary tap or movement below touch slop
407
+ MUST retain ordinary React touch-end delivery without native-gesture start/end
408
+ notifications. The existing `keyboardDismissMode: 'on-drag'` behavior shares the
409
+ confirmed-drag boundary.
410
+
411
+ ACTION_UP and ACTION_CANCEL finish a started native gesture exactly once and
412
+ release the parent's interception restriction, even after RecyclerView has
413
+ intercepted its child and stops invoking the item-touch interception listener.
414
+ They clear vertical ownership; the next ACTION_DOWN resets direction and
415
+ gesture tracking. Android's touch-target removal cancellation must finish the
416
+ old stream before a retained list is reattached or its slots are reused, so the
417
+ next tap is not suppressed by the previous drag. Disposal releases the listener
418
+ through the existing detached-resource cleanup. This change adds no prop or
419
+ callback and changes neither iOS nor Web touch handling.
420
+
421
+ Conformance uses real MotionEvent dispatch through NativeListView/RecyclerView
422
+ and React Native's JSTouchDispatcher: vertical drag produces one touch-cancel
423
+ and one native end, a tap produces touch-end, and cancellation/removal followed
424
+ by reuse leaves the next tap functional. This proves native touch delivery, not
425
+ execution of a JavaScript press handler or physical-device gesture feel.
426
+
387
427
  ### Android reorder cancellation
388
428
 
389
429
  Status: implemented; three real Android View/callback regressions verified on
@@ -1619,12 +1619,15 @@ final class NativeListLifecycleTests: XCTestCase {
1619
1619
  let thousandCells = try sampleCells(count: 1000)
1620
1620
  print("NativeList 100 visible/attached first/mid/tail: \(hundredCells)")
1621
1621
  print("NativeList 1000 visible/attached first/mid/tail: \(thousandCells)")
1622
+ let visibleCellLimit = Int(ceil(list.bounds.height / 60)) + 2
1622
1623
  XCTAssertTrue(hundredCells.allSatisfy {
1623
- $0.visible > 0 && $0.visible <= 24 && $0.attached > 0 && $0.attached <= 24
1624
+ $0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
1624
1625
  })
1625
1626
  XCTAssertTrue(thousandCells.allSatisfy {
1626
- $0.visible > 0 && $0.visible <= 24 && $0.attached > 0 && $0.attached <= 24
1627
+ $0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
1627
1628
  })
1629
+ // UIKit also retains offscreen cells in subviews. Their count may exceed
1630
+ // the visible budget, but must remain bounded as the data grows tenfold.
1628
1631
  XCTAssertLessThanOrEqual(thousandCells.map(\.attached).max() ?? 0,
1629
1632
  (hundredCells.map(\.attached).max() ?? 0) + 4)
1630
1633
  }
@@ -1941,6 +1944,7 @@ final class NativeListLifecycleTests: XCTestCase {
1941
1944
  for (index, slot) in slots.enumerated() { view.mountContainerSlot(slot, atIndex: index) }
1942
1945
  view.setContainerSlotHeightsJson("[0,0,140,0,100,40]")
1943
1946
  var settlements: [(String, String, Bool)] = []
1947
+ let latestSettled = expectation(description: "Latest disclosure transition settled")
1944
1948
  view.onStructuralTransitionSettled = { payload in
1945
1949
  guard let data = payload.data(using: .utf8),
1946
1950
  let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
@@ -1948,6 +1952,7 @@ final class NativeListLifecycleTests: XCTestCase {
1948
1952
  let status = object["status"] as? String,
1949
1953
  let expanded = object["settledExpanded"] as? Bool else { return }
1950
1954
  settlements.append((id, status, expanded))
1955
+ if id == "collapse-latest" { latestSettled.fulfill() }
1951
1956
  }
1952
1957
  view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 1,
1953
1958
  disclosureExpanded: false))
@@ -1968,11 +1973,15 @@ final class NativeListLifecycleTests: XCTestCase {
1968
1973
  view.applySnapshotJson(try structuralRowsSnapshot(count: 50, generation: 4,
1969
1974
  transitionID: "expand-superseded", durationMs: 400,
1970
1975
  disclosureExpanded: true))
1971
- waitForStructuralFrames(0.08)
1976
+ // Supersede on the same main-thread turn: a timed wait can outlast the
1977
+ // expansion on a busy simulator and legitimately deliver its settlement.
1978
+ XCTAssertNotNil(Mirror(reflecting: view).descendant("structuralDisplayLink")
1979
+ as? CADisplayLink)
1980
+ XCTAssertEqual(settlements.map(\.0), ["same-rows"])
1972
1981
  view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 5,
1973
1982
  transitionID: "collapse-latest", durationMs: 400,
1974
1983
  disclosureExpanded: false))
1975
- waitForStructuralFrames(0.5)
1984
+ wait(for: [latestSettled], timeout: 2)
1976
1985
  XCTAssertEqual(settlements.map(\.0), ["same-rows", "collapse-latest"])
1977
1986
  XCTAssertEqual(settlements.last?.1, "completed")
1978
1987
  XCTAssertEqual(settlements.last?.2, false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "6.7.0-alpha.399",
3
+ "version": "6.7.0-alpha.401",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -86,8 +86,8 @@
86
86
  "typescript": "^5.9.2"
87
87
  },
88
88
  "peerDependencies": {
89
- "@onekeyfe/react-native-image": "6.7.0-alpha.399",
90
- "@onekeyfe/react-native-native-logger": "6.7.0-alpha.399",
89
+ "@onekeyfe/react-native-image": "6.7.0-alpha.401",
90
+ "@onekeyfe/react-native-native-logger": "6.7.0-alpha.401",
91
91
  "react": "*",
92
92
  "react-native": "*",
93
93
  "react-native-nitro-modules": "0.37.0"