@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 ->
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
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 <=
|
|
1624
|
+
$0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
|
|
1624
1625
|
})
|
|
1625
1626
|
XCTAssertTrue(thousandCells.allSatisfy {
|
|
1626
|
-
$0.visible > 0 && $0.visible <=
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
90
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
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"
|