@onekeyfe/react-native-pager-view 6.7.0-20 → 6.7.0-22

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 (30) hide show
  1. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +40 -9
  2. package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +8 -2
  3. package/docs/SPEC.md +44 -0
  4. package/ios/RNCCollapsiblePagerViewComponentView.mm +410 -19
  5. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +36 -5
  6. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +756 -0
  7. package/lib/module/CollapsiblePagerViewNativeComponent.ts +41 -46
  8. package/lib/module/NativeScroller.web.js +3 -0
  9. package/lib/module/NativeScrollerNativeComponent.ts +43 -46
  10. package/lib/module/PagerViewNativeComponent.ts +20 -25
  11. package/lib/module/WebCollapsiblePagerPage.js +7 -2
  12. package/lib/typescript/src/CollapsiblePagerView.d.ts +1 -1
  13. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +1 -1
  14. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +41 -42
  15. package/lib/typescript/src/NativeScroller.d.ts +5 -3
  16. package/lib/typescript/src/NativeScroller.web.d.ts +1 -1
  17. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +47 -48
  18. package/lib/typescript/src/PagerView.d.ts +1 -1
  19. package/lib/typescript/src/PagerView.web.d.ts +1 -1
  20. package/lib/typescript/src/PagerViewNativeComponent.d.ts +20 -21
  21. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +1 -1
  22. package/lib/typescript/src/usePagerView.d.ts +2 -2
  23. package/lib/typescript/src/utils.d.ts +2 -2
  24. package/package.json +19 -7
  25. package/src/CollapsiblePagerViewNativeComponent.ts +41 -46
  26. package/src/NativeScroller.tsx +14 -3
  27. package/src/NativeScroller.web.tsx +15 -1
  28. package/src/NativeScrollerNativeComponent.ts +43 -46
  29. package/src/PagerViewNativeComponent.ts +20 -25
  30. package/src/WebCollapsiblePagerPage.tsx +7 -2
@@ -591,9 +591,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
591
591
  cancelPagerTop()
592
592
  if (adapter.itemCount == 0 || !nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
593
593
  if (transitionHeaderOffsets == null) {
594
- transitionSourceDisplayBody = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
595
- transitionSourceRecyclerAnchor = (primaryScrollableForPage(selectedPage) as? RecyclerView)?.let(::captureRecyclerAnchor)
596
- pageHeaderOffsets[currentPageKey()] = headerOffsetPx
594
+ val sourceKey = currentPageKey()
595
+ val sourceView = primaryScrollableForPage(selectedPage)
596
+ val topInset = headerHeightPx + stickyHeaderHeightPx
597
+ val sourceRestored = sourceKey !in pendingPageRestores && when (sourceView) {
598
+ is RecyclerView -> restoredRecyclerKeys[sourceView] == sourceKey &&
599
+ originalRecyclerPadding[sourceView]?.appliedTopInset == topInset
600
+ is ScrollView -> restoredScrollerKeys[sourceView] == sourceKey &&
601
+ originalScrollerPadding[sourceView]?.appliedTopInset == topInset
602
+ is NativeScrollerViewport -> ownedScrollerKeys[sourceView] == sourceKey
603
+ else -> false
604
+ }
605
+ transitionSourceDisplayBody = if (sourceRestored && sourceView != null) {
606
+ verticalScrollOffset(sourceView)
607
+ } else {
608
+ displayBodyOverrides[sourceKey] ?: pageOffsets[sourceKey] ?: 0
609
+ }
610
+ transitionSourceRecyclerAnchor = if (sourceRestored && sourceView is RecyclerView) {
611
+ captureRecyclerAnchor(sourceView)
612
+ } else {
613
+ (displayRecyclerAnchors[sourceKey] ?: recyclerReadingAnchors[sourceKey])
614
+ ?.takeIf { sourceView !is RecyclerView || (it.view.get() === sourceView && it.adapter === sourceView.adapter) }
615
+ }
616
+ pageHeaderOffsets[sourceKey] = headerOffsetPx
597
617
  detachPrimaryScrollObserver()
598
618
  transitionSourcePage = selectedPage
599
619
  transitionHeaderOffsets = pageHeaderOffsets.toMap()
@@ -1684,6 +1704,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1684
1704
  )
1685
1705
  }
1686
1706
  updateRecyclerPaddingOwnership(recycler, original)
1707
+ val key = pageKey(pageIndex)
1687
1708
  val topInset = headerHeightPx + stickyHeaderHeightPx
1688
1709
  recycler.clipToPadding = false
1689
1710
  val top = original.top + topInset
@@ -1695,12 +1716,23 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1695
1716
  // OneKey patch: LinearLayoutManager otherwise anchors children at their old
1696
1717
  // screen coordinates when a page changes the sticky header height.
1697
1718
  val manager = recycler.layoutManager as? LinearLayoutManager
1698
- val first = manager?.takeIf { it.orientation == RecyclerView.VERTICAL }
1699
- ?.findFirstVisibleItemPosition() ?: RecyclerView.NO_POSITION
1700
- val anchorOffset = if (first != RecyclerView.NO_POSITION) {
1719
+ val savedAnchor = if (nativeSmoothHeaderScrollEnabled && original.appliedTopInset == 0) {
1720
+ if (headerOffsetPx < headerHeightPx) {
1721
+ recycler.adapter?.let { RecyclerReadingAnchor(recycler, it, 0, 0) }
1722
+ } else {
1723
+ (if (transitionPageOffsets != null && pageIndex == transitionSourcePage) transitionSourceRecyclerAnchor
1724
+ else displayRecyclerAnchors[key] ?: recyclerReadingAnchors[key])
1725
+ ?.takeIf { it.view.get() === recycler && it.adapter === recycler.adapter }
1726
+ }
1727
+ } else null
1728
+ val first = savedAnchor?.position
1729
+ ?: manager?.takeIf { it.orientation == RecyclerView.VERTICAL }
1730
+ ?.findFirstVisibleItemPosition()
1731
+ ?: RecyclerView.NO_POSITION
1732
+ val anchorOffset = savedAnchor?.top ?: if (first != RecyclerView.NO_POSITION) {
1701
1733
  manager?.findViewByPosition(first)?.let { manager.getDecoratedTop(it) - recycler.paddingTop }
1702
1734
  } else null
1703
- offsetDiag("recycler-inset-before-nextTop=$top-anchorPosition=$first-anchorOffset=$anchorOffset", recycler, pageKey(pageIndex), pageIndex)
1735
+ offsetDiag("recycler-inset-before-nextTop=$top-anchorPosition=$first-anchorOffset=$anchorOffset", recycler, key, pageIndex)
1704
1736
  recycler.setPadding(original.left, top, original.right, bottom)
1705
1737
  if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
1706
1738
  // React Native can stop this nested requestLayout at its React-owned parent.
@@ -1714,9 +1746,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1714
1746
  recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
1715
1747
  }
1716
1748
  }
1717
- if (original.appliedTopInset != topInset) offsetDiag("recycler-inset-after-old=${original.appliedTopInset}-new=$topInset", recycler, pageKey(pageIndex), pageIndex)
1749
+ if (original.appliedTopInset != topInset) offsetDiag("recycler-inset-after-old=${original.appliedTopInset}-new=$topInset", recycler, key, pageIndex)
1718
1750
  original.appliedTopInset = topInset
1719
- val key = pageKey(pageIndex)
1720
1751
  val geometry = Triple(recycler.computeVerticalScrollRange(), recycler.computeVerticalScrollExtent(), recycler.childCount)
1721
1752
  deferredRecyclerGeometry[recycler]?.let { deferred ->
1722
1753
  if (geometry != deferred) {
@@ -112,6 +112,10 @@ open class NestedScrollableHost : FrameLayout {
112
112
  else -> 0
113
113
  }
114
114
  if (smoothTouchAxis != 0) {
115
+ if (!nestedNativeGestureStarted) {
116
+ NativeGestureUtil.notifyNativeGestureStarted(this, e)
117
+ nestedNativeGestureStarted = true
118
+ }
115
119
  OneKeyLog.debug("CollapsiblePager", "content-direction owner=${if (smoothTouchAxis == 1) "list" else "horizontal"} dx=$dx dy=$dy nested=${nestedHorizontalOwner != null}")
116
120
  }
117
121
  }
@@ -184,8 +188,10 @@ open class NestedScrollableHost : FrameLayout {
184
188
  val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
185
189
 
186
190
  if (scaledDx > touchSlop || scaledDy > touchSlop) {
187
- NativeGestureUtil.notifyNativeGestureStarted(this, e)
188
- nestedNativeGestureStarted = true
191
+ if (!nestedNativeGestureStarted) {
192
+ NativeGestureUtil.notifyNativeGestureStarted(this, e)
193
+ nestedNativeGestureStarted = true
194
+ }
189
195
 
190
196
  if (orientation == null) return
191
197
  if (isVpHorizontal == (scaledDy > scaledDx)) {
package/docs/SPEC.md CHANGED
@@ -41,6 +41,31 @@ UIKit's Hard style, owner or interactions. During temporary horizontal owner
41
41
  transitions the layer holds its last opacity until geometry is valid again;
42
42
  disabling the local overlay, changing style or recycling removes it.
43
43
 
44
+ For this iOS 27 local smooth Hard direct-pager case, an animated page command
45
+ uses `UIScrollView`'s native content-offset animation; UIKit determines its
46
+ duration. Both page commands and horizontal user drags transfer the source's
47
+ existing top and pinned sticky scroll-edge interactions, markers, hosts and
48
+ saved original style to the transition owner before the source page detaches.
49
+ The interactions remain bound to the source list on their
50
+ original hosts; the borrowed pinned sticky host stays inside the source list
51
+ while the shared controls move. Incoming pages use temporary interactions; a
52
+ drag prepares both adjacent pages so either direction can remain visible.
53
+ Participating pages retain their scroll-edge sampling area during the animation;
54
+ fixed controls still reserve their existing hit-test area. Completion or
55
+ cancellation releases temporary ownership, restores each list's prior style
56
+ and pinned host, and reattaches the settled page's normal owner before a queued
57
+ page command starts. A drag that returns to its source page follows the same
58
+ cleanup path. A new drag invalidates an older pending settlement before it can
59
+ release the new transition's owners.
60
+ If the direct pager viewport width changes while a native animated page command is pending, stop the old-coordinate animation before resizing its viewport. After the normal layout completes, settle the captured logical destination at its new physical offset through the original generation/transition-guarded completion path. This restores scrolling, releases temporary edge ownership and drains queued commands. Any width change, including a fractional-point change, is significant. Zero width completes the logical destination without retaining a disabled pending animation; a later positive-width idle layout realigns the current page. Height-only layout and horizontal user drags retain their existing paths. Late animation-end callbacks must not complete a newer command before its own current target has been reached.
61
+
62
+ Other pager modes retain their prior command and observer ordering. Recycling,
63
+ window detachment, slot replacement and disabling the style also release the
64
+ temporary resources. Conformance requires preserving the source interaction
65
+ and marker identities and prior style until settlement, releasing borrowed
66
+ resources on completion and cancellation, and rendered blur continuity in both
67
+ directions over long scrollable pages.
68
+
44
69
  For an iOS 26 pager whose fixed header is rendered by a sibling in the same
45
70
  React surface, `externalTopOverlayTag` may identify that header's native view.
46
71
  The pager weakly observes its geometry in the same window, retaining its own
@@ -111,6 +136,21 @@ style forwarding, overlay retention and disabling; UIKit tests cover native
111
136
  style switching and restoring the previous style. Rendered iOS acceptance must
112
137
  be reported separately from type, codegen and build checks.
113
138
 
139
+ ### iOS 27 external-overlay interactive ownership
140
+
141
+ Status: Implemented in iOS source; runtime acceptance is reported separately.
142
+
143
+ For a direct native pager using smooth headers and a Hard top edge, an interactive horizontal drag may preserve scroll-edge ownership when the external overlay is resolved in the same window with a positive effective height. A registered outer pager must own the active page and must not be transitioning; an external overlay without an outer pager remains eligible.
144
+
145
+ Before detaching the source observer, transfer the source's existing top/sticky interactions, markers, original edge style and borrowed pinned host into the transition owner. Do not move or take ownership of the actual external React overlay. Interactive preparation is bounded to the source and its valid immediate neighbours.
146
+
147
+ Each newly prepared external incoming page owns separate transparent, noninteractive top/sticky interaction hosts inside that page. Their horizontal origin follows the page's scroll viewport rather than the fixed external overlay; their vertical positions and heights match the existing top/sticky geometry. Hosts and markers follow page-width changes. They add no replacement material or background. The source retains its existing host identities. Local-overlay ownership and external programmatic page commands keep their existing paths; this extension is selected only by interactive drag preparation.
148
+
149
+ Completion or release back to the source releases temporary interactions and markers, restores the captured original styles, removes only the owned temporary hosts, and reattaches normal ownership to the settled page. Existing cancellation, window removal, slot/recycle and external invalidation paths must release these resources as well. External identity/tag/effective-height changes and outer-owner inactivity or transition invalidate temporary ownership before references are updated. Never remove a borrowed pinned host, shared proxy, shared sticky view or actual external overlay as though it were an owned temporary host.
150
+
151
+ Rendering conformance requires real underlying content on both pages to remain blurred throughout the drag and release, with no premature whiteout, sharp penetration or frozen texture. Object registration and lifecycle tests do not establish visual conformance.
152
+
153
+
114
154
  ## Smooth shared-header and per-page body memory
115
155
 
116
156
  With nativeSmoothHeaderScrollEnabled, the header collapse C is shared across all
@@ -435,6 +475,10 @@ controls belongs to the consuming responder implementation; the Pager package
435
475
  does not contain or publish Tamagui helpers. The native application tab bar has
436
476
  a separate contract in react-native-bottom-tabs/docs/SPEC.md.
437
477
 
478
+ On Android, smooth-touch coordination and nested interception share the same
479
+ native-gesture state. Once either path starts the gesture, later MOVE events
480
+ MUST NOT start it again before its end or cancellation clears that state.
481
+
438
482
  The current native implementation confirms horizontal intent after at least
439
483
  32 points on iOS or 32 dp on Android, with horizontal displacement exceeding
440
484
  vertical displacement. Vertical intent uses the native vertical scroll threshold