@onekeyfe/react-native-pager-view 3.0.175 → 3.0.177
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.
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +66 -8
- package/docs/SPEC.md +12 -0
- package/lib/module/CollapsiblePagerView.web.js +60 -11
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +5 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.web.tsx +90 -14
|
@@ -123,6 +123,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
123
123
|
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
124
124
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
125
125
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
126
|
+
private val deferredScrollerGeometry = WeakHashMap<ScrollView, Triple<Int, Int, Int>>()
|
|
126
127
|
private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
|
|
127
128
|
private val ownedScrollerRestoreRequests = HashMap<String, Pair<WeakReference<NativeScrollerViewport>, Int?>>()
|
|
128
129
|
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
@@ -473,6 +474,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
473
474
|
releaseRefreshForeground()
|
|
474
475
|
val index = logicalChildren.indexOf(child)
|
|
475
476
|
if (index < 0) return
|
|
477
|
+
if (index >= PAGE_SLOT_OFFSET) {
|
|
478
|
+
val observed = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
479
|
+
val removingObservedPage = observed != null && isDescendant(observed, child)
|
|
480
|
+
cancelPageTransitionForPageMutation()
|
|
481
|
+
cancelPagerTop()
|
|
482
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
483
|
+
if (removingObservedPage) observedPageRemoved = true
|
|
484
|
+
}
|
|
476
485
|
logicalChildren.removeAt(index)
|
|
477
486
|
when (index) {
|
|
478
487
|
0 -> {
|
|
@@ -484,12 +493,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
484
493
|
stickyHeaderView = null
|
|
485
494
|
}
|
|
486
495
|
else -> {
|
|
487
|
-
val observed = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
488
|
-
if (observed != null && isDescendant(observed, child)) {
|
|
489
|
-
// The page detaches only later; its scroller must not keep writing this key.
|
|
490
|
-
detachPrimaryScrollObserver(recordOffset = false)
|
|
491
|
-
observedPageRemoved = true
|
|
492
|
-
}
|
|
493
496
|
when (val scrollable =
|
|
494
497
|
findExplicitNativeScrollerInView(child) ?: findVerticalScrollableView(child)) {
|
|
495
498
|
is RecyclerView -> restoreRecyclerInsets(scrollable)
|
|
@@ -497,14 +500,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
497
500
|
is NativeScrollerViewport -> restoreOwnedScrollerInsets(scrollable)
|
|
498
501
|
}
|
|
499
502
|
adapter.removePage(child)
|
|
503
|
+
selectedPage = if (adapter.itemCount == 0) 0 else selectedPage.coerceIn(0, adapter.itemCount - 1)
|
|
500
504
|
log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
501
505
|
}
|
|
502
506
|
}
|
|
503
507
|
}
|
|
504
508
|
|
|
505
509
|
fun removeAllReactChildren() {
|
|
510
|
+
cancelPageTransitionForPageMutation()
|
|
511
|
+
cancelPagerTop()
|
|
506
512
|
releaseRefreshForeground()
|
|
507
|
-
detachPrimaryScrollObserver()
|
|
513
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
508
514
|
pageRestoreRequests.clear()
|
|
509
515
|
pendingPageRestores.clear()
|
|
510
516
|
commandBodyKeys.clear()
|
|
@@ -513,6 +519,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
513
519
|
alignedBodyKeys.clear()
|
|
514
520
|
transitionAlignedViews.clear()
|
|
515
521
|
deferredRecyclerGeometry.clear()
|
|
522
|
+
deferredScrollerGeometry.clear()
|
|
516
523
|
displayBodyOverrides.clear()
|
|
517
524
|
displayRecyclerAnchors.clear()
|
|
518
525
|
ownedScrollerRestoreRequests.clear()
|
|
@@ -532,6 +539,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
532
539
|
stickyHeaderView = null
|
|
533
540
|
logicalChildren.clear()
|
|
534
541
|
adapter.clear()
|
|
542
|
+
selectedPage = 0
|
|
535
543
|
hasAppliedInitialPage = false
|
|
536
544
|
log("pages-clear")
|
|
537
545
|
}
|
|
@@ -540,9 +548,28 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
540
548
|
|
|
541
549
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
542
550
|
|
|
551
|
+
private fun cancelPageTransitionForPageMutation() {
|
|
552
|
+
if (transitionHeaderOffsets != null) {
|
|
553
|
+
val keys = transitionAlignedViews.values.toTypedArray().toMutableSet()
|
|
554
|
+
keys.add(pageKey(transitionSourcePage))
|
|
555
|
+
transitionTargetPage?.let { keys.add(pageKey(it)) }
|
|
556
|
+
for (key in keys) {
|
|
557
|
+
displayBodyOverrides.remove(key)
|
|
558
|
+
displayRecyclerAnchors.remove(key)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
transitionHeaderOffsets = null
|
|
562
|
+
transitionPageOffsets = null
|
|
563
|
+
transitionTargetPage = null
|
|
564
|
+
transitionSourceRecyclerAnchor = null
|
|
565
|
+
transitionSourceDisplayBody = 0
|
|
566
|
+
transitionAlignedViews.clear()
|
|
567
|
+
resumeTransitionSource = null
|
|
568
|
+
}
|
|
569
|
+
|
|
543
570
|
internal fun beginPageTransition(target: Int? = null) {
|
|
544
571
|
cancelPagerTop()
|
|
545
|
-
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
572
|
+
if (adapter.itemCount == 0 || !nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
546
573
|
if (transitionHeaderOffsets == null) {
|
|
547
574
|
transitionSourceDisplayBody = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
548
575
|
transitionSourceRecyclerAnchor = (primaryScrollableForPage(selectedPage) as? RecyclerView)?.let(::captureRecyclerAnchor)
|
|
@@ -576,6 +603,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
576
603
|
}
|
|
577
604
|
|
|
578
605
|
private fun finishPageTransition(position: Int) {
|
|
606
|
+
if (adapter.itemCount == 0) {
|
|
607
|
+
cancelPageTransitionForPageMutation()
|
|
608
|
+
selectedPage = 0
|
|
609
|
+
return
|
|
610
|
+
}
|
|
579
611
|
val headers = transitionHeaderOffsets ?: return
|
|
580
612
|
detachPrimaryScrollObserver(recordOffset = false)
|
|
581
613
|
releaseRefreshForeground()
|
|
@@ -907,6 +939,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
907
939
|
pendingPageRestores.remove(key)
|
|
908
940
|
ownedScrollerRestoreRequests.remove(key)
|
|
909
941
|
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
942
|
+
if (view is ScrollView) deferredScrollerGeometry.remove(view)
|
|
910
943
|
(view as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
911
944
|
suspendedBodyKeys.remove(key)
|
|
912
945
|
alignedBodyKeys.remove(key)
|
|
@@ -985,6 +1018,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
985
1018
|
displayRecyclerAnchors.remove(key)
|
|
986
1019
|
recyclerReadingAnchors.remove(key)
|
|
987
1020
|
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
1021
|
+
if (view is ScrollView) deferredScrollerGeometry.remove(view)
|
|
988
1022
|
pageOffsets[key] = 0
|
|
989
1023
|
val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, target, ++pagerTopToken)
|
|
990
1024
|
pagerTopRequest = request
|
|
@@ -1717,6 +1751,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1717
1751
|
original.appliedTopInset = topInset
|
|
1718
1752
|
|
|
1719
1753
|
val key = pageKey(pageIndex)
|
|
1754
|
+
val geometry = Triple(scroller.getChildAt(0)?.height ?: 0, scroller.height,
|
|
1755
|
+
scroller.paddingTop + scroller.paddingBottom)
|
|
1756
|
+
deferredScrollerGeometry[scroller]?.let { deferred ->
|
|
1757
|
+
if (geometry != deferred) {
|
|
1758
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1759
|
+
restoredScrollerKeys.remove(scroller)
|
|
1760
|
+
if (nativeSmoothHeaderScrollEnabled && headerOffsetPx == headerHeightPx) {
|
|
1761
|
+
displayBodyOverrides.remove(key)
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1720
1765
|
if (restoredScrollerKeys[scroller] != key) {
|
|
1721
1766
|
restoredScrollerKeys[scroller] = key
|
|
1722
1767
|
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
@@ -1744,6 +1789,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1744
1789
|
return@post
|
|
1745
1790
|
}
|
|
1746
1791
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1792
|
+
val unreachableReadingIntent = nativeSmoothHeaderScrollEnabled &&
|
|
1793
|
+
headerOffsetPx == headerHeightPx && displayBodyOverrides.containsKey(key) &&
|
|
1794
|
+
(pageOffsets[key] ?: 0) > scroller.scrollY && !scroller.canScrollVertically(1)
|
|
1795
|
+
if (nativeSmoothHeaderScrollEnabled && (scroller.scrollY != saved || unreachableReadingIntent)) {
|
|
1796
|
+
deferredScrollerGeometry[scroller] = Triple(scroller.getChildAt(0)?.height ?: 0,
|
|
1797
|
+
scroller.height, scroller.paddingTop + scroller.paddingBottom)
|
|
1798
|
+
offsetDiag("scrollview-restore-pending-saved=$saved-current=${scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
|
|
1799
|
+
return@post
|
|
1800
|
+
}
|
|
1801
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1747
1802
|
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1748
1803
|
else pendingPageRestores.remove(key)
|
|
1749
1804
|
offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
@@ -1813,6 +1868,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1813
1868
|
|
|
1814
1869
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
1815
1870
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
1871
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1816
1872
|
updateScrollerPaddingOwnership(scroller, original)
|
|
1817
1873
|
restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
|
|
1818
1874
|
if (scroller is ReactScrollView) {
|
|
@@ -2193,6 +2249,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2193
2249
|
val request = pageRestoreRequests[currentPageKey()]
|
|
2194
2250
|
offsetDiag("touch-cancel-before", target, request = request)
|
|
2195
2251
|
observedRecyclerView?.let { deferredRecyclerGeometry.remove(it) }
|
|
2252
|
+
observedNativeScroller?.let { deferredScrollerGeometry.remove(it) }
|
|
2196
2253
|
pageRestoreRequests.remove(currentPageKey())
|
|
2197
2254
|
pendingPageRestores.remove(currentPageKey())
|
|
2198
2255
|
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
@@ -2334,6 +2391,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2334
2391
|
alignedBodyKeys.clear()
|
|
2335
2392
|
transitionAlignedViews.clear()
|
|
2336
2393
|
deferredRecyclerGeometry.clear()
|
|
2394
|
+
deferredScrollerGeometry.clear()
|
|
2337
2395
|
displayBodyOverrides.clear()
|
|
2338
2396
|
displayRecyclerAnchors.clear()
|
|
2339
2397
|
ownedScrollerRestoreRequests.clear()
|
package/docs/SPEC.md
CHANGED
|
@@ -38,6 +38,12 @@ Late callbacks cannot revive cancelled restoration or erase a newer request.
|
|
|
38
38
|
Observers and commands must match page key, view identity and generation.
|
|
39
39
|
Removed or recycled owners cannot apply callbacks to another page; recycling
|
|
40
40
|
resets owned memory.
|
|
41
|
+
The Web backend shares C and stores per-page B separately from absolute DOM
|
|
42
|
+
scrollTop. Selection aligns to C while expanded and C + B while fully collapsed;
|
|
43
|
+
programmatic alignment and geometry-only scrollend events MUST NOT erase the
|
|
44
|
+
suspended reading position. Real vertical input or a valid List command takes
|
|
45
|
+
over the current display. PageTop/StickyTop clear only the current B.
|
|
46
|
+
|
|
41
47
|
Legacy mode and pagers without a collapsible header retain their existing
|
|
42
48
|
behavior. Vertical scrolling, refresh, snapping and repeated-current-tab behavior
|
|
43
49
|
are unchanged. This state belongs to native UI instances, with no new React prop
|
|
@@ -726,3 +732,9 @@ activation remain unverified.
|
|
|
726
732
|
The Header is settled only when C is at its expanded top, the current attached view/key/index/generation still matches, and there is no actual drag, deceleration, scrolling command or horizontal transition. Offset/inset/content/header geometry MUST remain stable for 150ms before true is emitted. Motion or geometry changes revoke true promptly. Deferred reading memory alone MUST NOT be treated as active motion. Pull-to-refresh may settle at a normalized negative offset after drag, bounce and inset geometry stop; network refreshing alone is not a motion gate. Checks stop for collapsed, detached and recycled owners; delayed callbacks revalidate their captured identity. State changes are deduplicated; identity changes still publish an unknown/false snapshot.
|
|
727
733
|
|
|
728
734
|
The native Home Spotlight adapter consumes this snapshot in the main UI runtime and keeps unknown states paused. It MUST use the mounted page-key mapping and reject old page/source callbacks. Native snapshots already include the 150ms stabilization delay: consumers MUST NOT add another such delay. Legacy Tabs keep their existing normalized-offset and debounce behavior. Anchor measurement, guide qualification and the existing display delay remain independent gates.
|
|
735
|
+
|
|
736
|
+
### Android page mutation during horizontal transitions
|
|
737
|
+
|
|
738
|
+
Removing a page or clearing the page collection MUST cancel the active horizontal transition and its temporary display snapshots before notifying the adapter. Cancellation MUST NOT rewrite the shared Header collapse or surviving pages' saved body reading memory. The previous observed owner MUST be detached without committing an offset under a shifted page key.
|
|
739
|
+
|
|
740
|
+
An empty adapter MUST reject transition starts and transition completion without coercing an index into an empty range. The selected index uses zero as an empty-state sentinel; nonempty mutations clamp it to the remaining valid range. Readding pages MUST allow a fresh selection and MUST NOT resume a removed transition source. Conformance requires actual Android active-transition → zero-pages → readd/selection regression and remaining-page mutation checks; structural checks and compilation alone do not establish runtime behavior.
|
|
@@ -44,6 +44,8 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
44
44
|
pageOffsets = new Map();
|
|
45
45
|
pageHosts = new Map();
|
|
46
46
|
sharedHeaderOffset = 0;
|
|
47
|
+
selectionRestoreToken = 0;
|
|
48
|
+
bodyAlignment = null;
|
|
47
49
|
scrollEnabled = this.props.scrollEnabled ?? true;
|
|
48
50
|
lastMountSignature = null;
|
|
49
51
|
configureFrame = null;
|
|
@@ -71,6 +73,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
71
73
|
const pageCount = this.pages().length;
|
|
72
74
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
73
75
|
if (selectedPage !== this.state.selectedPage) {
|
|
76
|
+
this.selectionRestoreToken += 1;
|
|
74
77
|
this.cancelPendingTopFrame();
|
|
75
78
|
this.finishPageTransition();
|
|
76
79
|
this.setState({
|
|
@@ -83,6 +86,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
83
86
|
this.notifyMountedPagesChanged();
|
|
84
87
|
}
|
|
85
88
|
componentWillUnmount() {
|
|
89
|
+
this.selectionRestoreToken += 1;
|
|
86
90
|
this.cancelPendingTopFrame();
|
|
87
91
|
this.savePageOffset(this.state.selectedPage);
|
|
88
92
|
if (this.configureFrame !== null) {
|
|
@@ -116,16 +120,32 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
116
120
|
if (this.pageScrollElement(this.state.selectedPage) === pending.element && this.pages().map(pageKey)[this.state.selectedPage] === pending.key) this.savePageOffset(this.state.selectedPage);
|
|
117
121
|
};
|
|
118
122
|
onQueuedTopWheel = event => {
|
|
119
|
-
if (event.deltaY !== 0) this.
|
|
123
|
+
if (event.deltaY !== 0) this.takeOverVerticalScroll();
|
|
124
|
+
};
|
|
125
|
+
onScrollbarPointerDown = event => {
|
|
126
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
127
|
+
if (!element || event.target !== element || event.button !== 0) return;
|
|
128
|
+
const rect = element.getBoundingClientRect();
|
|
129
|
+
const computed = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
130
|
+
const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
|
|
131
|
+
const borderRight = Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
|
|
132
|
+
const clientStart = rect.left + element.clientLeft;
|
|
133
|
+
const clientEnd = clientStart + element.clientWidth;
|
|
134
|
+
const leftGutter = clientStart - (rect.left + borderLeft);
|
|
135
|
+
const rightGutter = rect.right - borderRight - clientEnd;
|
|
136
|
+
const inVerticalTrack = event.clientY >= rect.top + element.clientTop && event.clientY < rect.top + element.clientTop + element.clientHeight;
|
|
137
|
+
const inGutter = leftGutter > 0 && event.clientX >= rect.left + borderLeft && event.clientX < clientStart || rightGutter > 0 && event.clientX >= clientEnd && event.clientX < rect.right - borderRight;
|
|
138
|
+
if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
|
|
120
139
|
};
|
|
121
140
|
onQueuedTopKey = event => {
|
|
122
|
-
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.
|
|
141
|
+
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.takeOverVerticalScroll();
|
|
123
142
|
};
|
|
124
143
|
scrollCurrentPageToTop(offset, animated) {
|
|
125
144
|
if (this.horizontalTouch || this.transitionTimer !== null) return;
|
|
126
145
|
const element = this.pageScrollElement(this.state.selectedPage);
|
|
127
146
|
const key = this.pages().map(pageKey)[this.state.selectedPage];
|
|
128
147
|
if (!element?.isConnected || !key) return;
|
|
148
|
+
this.selectionRestoreToken += 1;
|
|
129
149
|
this.cancelPendingTopFrame();
|
|
130
150
|
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
131
151
|
if (EventConstructor) element.dispatchEvent(new EventConstructor("ok-collapsible-pager-cancel-scroll-command"));
|
|
@@ -138,8 +158,12 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
138
158
|
if (this.pendingTopFrame?.frame !== frame) return;
|
|
139
159
|
this.pendingTopFrame = null;
|
|
140
160
|
if (!element.isConnected || this.horizontalTouch || this.transitionTimer !== null || this.pageScrollElement(this.state.selectedPage) !== element || this.pages().map(pageKey)[this.state.selectedPage] !== key) return;
|
|
141
|
-
this.pageOffsets.set(key,
|
|
161
|
+
this.pageOffsets.set(key, 0);
|
|
142
162
|
this.sharedHeaderOffset = offset;
|
|
163
|
+
this.bodyAlignment = {
|
|
164
|
+
element,
|
|
165
|
+
key
|
|
166
|
+
};
|
|
143
167
|
element.scrollTo({
|
|
144
168
|
top: offset,
|
|
145
169
|
behavior: animated ? "smooth" : "instant"
|
|
@@ -218,8 +242,9 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
218
242
|
const configured = this.configuredScrollElement;
|
|
219
243
|
if (!configured) return;
|
|
220
244
|
this.cancelPendingTopFrame();
|
|
221
|
-
configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.
|
|
245
|
+
configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
|
|
222
246
|
configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
|
|
247
|
+
configured.element.removeEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
223
248
|
configured.element.removeEventListener("keydown", this.onQueuedTopKey);
|
|
224
249
|
configured.element.removeEventListener("scrollend", this.onVerticalScrollSettled);
|
|
225
250
|
const style = timelineStyle(configured.element);
|
|
@@ -234,6 +259,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
234
259
|
configured.element.removeAttribute("data-collapsible-pager-active");
|
|
235
260
|
this.notifyInsetsChanged(configured.element);
|
|
236
261
|
this.configuredScrollElement = null;
|
|
262
|
+
if (this.bodyAlignment?.element === configured.element) this.bodyAlignment = null;
|
|
237
263
|
}
|
|
238
264
|
configureActiveScrollElement = () => {
|
|
239
265
|
this.configureFrame = null;
|
|
@@ -269,8 +295,9 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
269
295
|
pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET)
|
|
270
296
|
};
|
|
271
297
|
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
272
|
-
element.addEventListener("ok-native-list-scroll-command-accepted", this.
|
|
298
|
+
element.addEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
|
|
273
299
|
element.addEventListener("wheel", this.onQueuedTopWheel);
|
|
300
|
+
element.addEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
274
301
|
element.addEventListener("keydown", this.onQueuedTopKey);
|
|
275
302
|
}
|
|
276
303
|
const style = timelineStyle(element);
|
|
@@ -299,8 +326,18 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
299
326
|
this.configureActiveScrollElement();
|
|
300
327
|
}
|
|
301
328
|
}
|
|
302
|
-
|
|
303
|
-
|
|
329
|
+
takeOverVerticalScroll = () => {
|
|
330
|
+
this.selectionRestoreToken += 1;
|
|
331
|
+
this.bodyAlignment = null;
|
|
332
|
+
this.cancelPendingTopFrame();
|
|
333
|
+
this.savePageOffset(this.state.selectedPage);
|
|
334
|
+
};
|
|
335
|
+
onListScrollCommandAccepted = event => {
|
|
336
|
+
if (event.target !== this.pageScrollElement(this.state.selectedPage)) return;
|
|
337
|
+
this.takeOverVerticalScroll();
|
|
338
|
+
};
|
|
339
|
+
onVerticalScrollSettled = event => {
|
|
340
|
+
if (this.pendingTopFrame || event && event.target !== this.pageScrollElement(this.state.selectedPage)) return;
|
|
304
341
|
this.savePageOffset(this.state.selectedPage);
|
|
305
342
|
this.emitDiagnostics("scroll-settled");
|
|
306
343
|
};
|
|
@@ -308,15 +345,24 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
308
345
|
const key = this.pages().map(pageKey)[index];
|
|
309
346
|
const scrollElement = this.pageScrollElement(index);
|
|
310
347
|
if (key && scrollElement) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
348
|
+
if (this.bodyAlignment?.key === key && this.bodyAlignment.element === scrollElement) return;
|
|
349
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
350
|
+
this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
|
|
351
|
+
this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), D);
|
|
313
352
|
}
|
|
314
353
|
}
|
|
315
354
|
restorePageOffset(index) {
|
|
316
355
|
const key = this.pages().map(pageKey)[index];
|
|
317
356
|
const scrollElement = this.pageScrollElement(index);
|
|
318
357
|
if (!key || !scrollElement) return;
|
|
319
|
-
|
|
358
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
359
|
+
const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
|
|
360
|
+
const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
|
|
361
|
+
this.bodyAlignment = {
|
|
362
|
+
element: scrollElement,
|
|
363
|
+
key
|
|
364
|
+
};
|
|
365
|
+
scrollElement.scrollTop = expectedQ;
|
|
320
366
|
}
|
|
321
367
|
selectPage(selectedPage, animated) {
|
|
322
368
|
const pages = this.pages();
|
|
@@ -324,6 +370,8 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
324
370
|
if (position === null || position < 0 || position >= pages.length || position === this.state.selectedPage) {
|
|
325
371
|
return;
|
|
326
372
|
}
|
|
373
|
+
const selectionToken = ++this.selectionRestoreToken;
|
|
374
|
+
const expectedKey = this.pages().map(pageKey)[position];
|
|
327
375
|
this.cancelPendingTopFrame();
|
|
328
376
|
this.savePageOffset(this.state.selectedPage);
|
|
329
377
|
if (animated) this.beginPageTransition();else this.finishPageTransition();
|
|
@@ -333,6 +381,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
333
381
|
}, () => {
|
|
334
382
|
this.scheduleScrollConfiguration();
|
|
335
383
|
const restore = () => {
|
|
384
|
+
if (this.selectionRestoreToken !== selectionToken || this.state.selectedPage !== position || this.pages().map(pageKey)[position] !== expectedKey || !this.pageScrollElement(position)?.isConnected) return;
|
|
336
385
|
this.configureActiveScrollElement();
|
|
337
386
|
this.restorePageOffset(position);
|
|
338
387
|
};
|
|
@@ -392,7 +441,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
392
441
|
if (!touch) return;
|
|
393
442
|
const deltaX = touch.pageX - this.touchStartX;
|
|
394
443
|
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
395
|
-
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.
|
|
444
|
+
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.takeOverVerticalScroll();
|
|
396
445
|
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
397
446
|
this.cancelPendingTopFrame();
|
|
398
447
|
this.horizontalTouch = true;
|
|
@@ -59,6 +59,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
59
59
|
private pageOffsets;
|
|
60
60
|
private pageHosts;
|
|
61
61
|
private sharedHeaderOffset;
|
|
62
|
+
private selectionRestoreToken;
|
|
63
|
+
private bodyAlignment;
|
|
62
64
|
private scrollEnabled;
|
|
63
65
|
private lastMountSignature;
|
|
64
66
|
private configureFrame;
|
|
@@ -80,6 +82,7 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
80
82
|
}>) => void;
|
|
81
83
|
private cancelPendingTopFrame;
|
|
82
84
|
private onQueuedTopWheel;
|
|
85
|
+
private onScrollbarPointerDown;
|
|
83
86
|
private onQueuedTopKey;
|
|
84
87
|
private scrollCurrentPageToTop;
|
|
85
88
|
setScrollEnabled: (scrollEnabled: boolean) => void;
|
|
@@ -97,6 +100,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
97
100
|
private restoreConfiguredScrollElement;
|
|
98
101
|
private configureActiveScrollElement;
|
|
99
102
|
private scheduleScrollConfiguration;
|
|
103
|
+
private takeOverVerticalScroll;
|
|
104
|
+
private onListScrollCommandAccepted;
|
|
100
105
|
private onVerticalScrollSettled;
|
|
101
106
|
private savePageOffset;
|
|
102
107
|
private restorePageOffset;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.177",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"typescript": "^5.9.2"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@onekeyfe/react-native-native-logger": "3.0.
|
|
75
|
+
"@onekeyfe/react-native-native-logger": "3.0.177",
|
|
76
76
|
"react": "*",
|
|
77
77
|
"react-native": "*"
|
|
78
78
|
},
|
|
@@ -154,6 +154,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
154
154
|
private pageOffsets = new Map<string, number>();
|
|
155
155
|
private pageHosts = new Map<number, HTMLElement>();
|
|
156
156
|
private sharedHeaderOffset = 0;
|
|
157
|
+
private selectionRestoreToken = 0;
|
|
158
|
+
private bodyAlignment: Readonly<{
|
|
159
|
+
element: HTMLElement;
|
|
160
|
+
key: string;
|
|
161
|
+
}> | null = null;
|
|
157
162
|
private scrollEnabled = this.props.scrollEnabled ?? true;
|
|
158
163
|
private lastMountSignature: string | null = null;
|
|
159
164
|
private configureFrame: number | null = null;
|
|
@@ -188,6 +193,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
188
193
|
const pageCount = this.pages().length;
|
|
189
194
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
190
195
|
if (selectedPage !== this.state.selectedPage) {
|
|
196
|
+
this.selectionRestoreToken += 1;
|
|
191
197
|
this.cancelPendingTopFrame();
|
|
192
198
|
this.finishPageTransition();
|
|
193
199
|
this.setState({
|
|
@@ -201,6 +207,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
componentWillUnmount() {
|
|
210
|
+
this.selectionRestoreToken += 1;
|
|
204
211
|
this.cancelPendingTopFrame();
|
|
205
212
|
this.savePageOffset(this.state.selectedPage);
|
|
206
213
|
if (this.configureFrame !== null) {
|
|
@@ -247,7 +254,33 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
247
254
|
};
|
|
248
255
|
|
|
249
256
|
private onQueuedTopWheel = (event: WheelEvent) => {
|
|
250
|
-
if (event.deltaY !== 0) this.
|
|
257
|
+
if (event.deltaY !== 0) this.takeOverVerticalScroll();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
private onScrollbarPointerDown = (event: PointerEvent) => {
|
|
261
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
262
|
+
if (!element || event.target !== element || event.button !== 0) return;
|
|
263
|
+
const rect = element.getBoundingClientRect();
|
|
264
|
+
const computed =
|
|
265
|
+
element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
266
|
+
const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
|
|
267
|
+
const borderRight =
|
|
268
|
+
Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
|
|
269
|
+
const clientStart = rect.left + element.clientLeft;
|
|
270
|
+
const clientEnd = clientStart + element.clientWidth;
|
|
271
|
+
const leftGutter = clientStart - (rect.left + borderLeft);
|
|
272
|
+
const rightGutter = rect.right - borderRight - clientEnd;
|
|
273
|
+
const inVerticalTrack =
|
|
274
|
+
event.clientY >= rect.top + element.clientTop &&
|
|
275
|
+
event.clientY < rect.top + element.clientTop + element.clientHeight;
|
|
276
|
+
const inGutter =
|
|
277
|
+
(leftGutter > 0 &&
|
|
278
|
+
event.clientX >= rect.left + borderLeft &&
|
|
279
|
+
event.clientX < clientStart) ||
|
|
280
|
+
(rightGutter > 0 &&
|
|
281
|
+
event.clientX >= clientEnd &&
|
|
282
|
+
event.clientX < rect.right - borderRight);
|
|
283
|
+
if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
|
|
251
284
|
};
|
|
252
285
|
|
|
253
286
|
private onQueuedTopKey = (event: KeyboardEvent) => {
|
|
@@ -262,7 +295,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
262
295
|
" ",
|
|
263
296
|
].includes(event.key)
|
|
264
297
|
)
|
|
265
|
-
this.
|
|
298
|
+
this.takeOverVerticalScroll();
|
|
266
299
|
};
|
|
267
300
|
|
|
268
301
|
private scrollCurrentPageToTop(offset: number, animated: boolean) {
|
|
@@ -270,6 +303,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
270
303
|
const element = this.pageScrollElement(this.state.selectedPage);
|
|
271
304
|
const key = this.pages().map(pageKey)[this.state.selectedPage];
|
|
272
305
|
if (!element?.isConnected || !key) return;
|
|
306
|
+
this.selectionRestoreToken += 1;
|
|
273
307
|
this.cancelPendingTopFrame();
|
|
274
308
|
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
275
309
|
if (EventConstructor)
|
|
@@ -292,8 +326,9 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
292
326
|
this.pages().map(pageKey)[this.state.selectedPage] !== key
|
|
293
327
|
)
|
|
294
328
|
return;
|
|
295
|
-
this.pageOffsets.set(key,
|
|
329
|
+
this.pageOffsets.set(key, 0);
|
|
296
330
|
this.sharedHeaderOffset = offset;
|
|
331
|
+
this.bodyAlignment = { element, key };
|
|
297
332
|
element.scrollTo({
|
|
298
333
|
top: offset,
|
|
299
334
|
behavior: animated ? "smooth" : "instant",
|
|
@@ -392,9 +427,13 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
392
427
|
this.cancelPendingTopFrame();
|
|
393
428
|
configured.element.removeEventListener(
|
|
394
429
|
"ok-native-list-scroll-command-accepted",
|
|
395
|
-
this.
|
|
430
|
+
this.onListScrollCommandAccepted
|
|
396
431
|
);
|
|
397
432
|
configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
|
|
433
|
+
configured.element.removeEventListener(
|
|
434
|
+
"pointerdown",
|
|
435
|
+
this.onScrollbarPointerDown
|
|
436
|
+
);
|
|
398
437
|
configured.element.removeEventListener("keydown", this.onQueuedTopKey);
|
|
399
438
|
configured.element.removeEventListener(
|
|
400
439
|
"scrollend",
|
|
@@ -412,6 +451,8 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
412
451
|
configured.element.removeAttribute("data-collapsible-pager-active");
|
|
413
452
|
this.notifyInsetsChanged(configured.element);
|
|
414
453
|
this.configuredScrollElement = null;
|
|
454
|
+
if (this.bodyAlignment?.element === configured.element)
|
|
455
|
+
this.bodyAlignment = null;
|
|
415
456
|
}
|
|
416
457
|
|
|
417
458
|
private configureActiveScrollElement = () => {
|
|
@@ -458,9 +499,10 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
458
499
|
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
459
500
|
element.addEventListener(
|
|
460
501
|
"ok-native-list-scroll-command-accepted",
|
|
461
|
-
this.
|
|
502
|
+
this.onListScrollCommandAccepted
|
|
462
503
|
);
|
|
463
504
|
element.addEventListener("wheel", this.onQueuedTopWheel);
|
|
505
|
+
element.addEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
464
506
|
element.addEventListener("keydown", this.onQueuedTopKey);
|
|
465
507
|
}
|
|
466
508
|
|
|
@@ -501,8 +543,26 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
501
543
|
}
|
|
502
544
|
}
|
|
503
545
|
|
|
504
|
-
private
|
|
505
|
-
|
|
546
|
+
private takeOverVerticalScroll = () => {
|
|
547
|
+
this.selectionRestoreToken += 1;
|
|
548
|
+
this.bodyAlignment = null;
|
|
549
|
+
this.cancelPendingTopFrame();
|
|
550
|
+
this.savePageOffset(this.state.selectedPage);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
private onListScrollCommandAccepted = (event: Event) => {
|
|
554
|
+
if (event.target !== this.pageScrollElement(this.state.selectedPage))
|
|
555
|
+
return;
|
|
556
|
+
this.takeOverVerticalScroll();
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
private onVerticalScrollSettled = (event?: Event) => {
|
|
560
|
+
if (
|
|
561
|
+
this.pendingTopFrame ||
|
|
562
|
+
(event &&
|
|
563
|
+
event.target !== this.pageScrollElement(this.state.selectedPage))
|
|
564
|
+
)
|
|
565
|
+
return;
|
|
506
566
|
this.savePageOffset(this.state.selectedPage);
|
|
507
567
|
this.emitDiagnostics("scroll-settled");
|
|
508
568
|
};
|
|
@@ -511,10 +571,16 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
511
571
|
const key = this.pages().map(pageKey)[index];
|
|
512
572
|
const scrollElement = this.pageScrollElement(index);
|
|
513
573
|
if (key && scrollElement) {
|
|
514
|
-
|
|
574
|
+
if (
|
|
575
|
+
this.bodyAlignment?.key === key &&
|
|
576
|
+
this.bodyAlignment.element === scrollElement
|
|
577
|
+
)
|
|
578
|
+
return;
|
|
579
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
580
|
+
this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
|
|
515
581
|
this.sharedHeaderOffset = Math.min(
|
|
516
582
|
Math.max(scrollElement.scrollTop, 0),
|
|
517
|
-
|
|
583
|
+
D
|
|
518
584
|
);
|
|
519
585
|
}
|
|
520
586
|
}
|
|
@@ -523,10 +589,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
523
589
|
const key = this.pages().map(pageKey)[index];
|
|
524
590
|
const scrollElement = this.pageScrollElement(index);
|
|
525
591
|
if (!key || !scrollElement) return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
592
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
593
|
+
const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
|
|
594
|
+
const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
|
|
595
|
+
this.bodyAlignment = { element: scrollElement, key };
|
|
596
|
+
scrollElement.scrollTop = expectedQ;
|
|
530
597
|
}
|
|
531
598
|
|
|
532
599
|
private selectPage(selectedPage: number, animated: boolean) {
|
|
@@ -540,6 +607,8 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
540
607
|
) {
|
|
541
608
|
return;
|
|
542
609
|
}
|
|
610
|
+
const selectionToken = ++this.selectionRestoreToken;
|
|
611
|
+
const expectedKey = this.pages().map(pageKey)[position];
|
|
543
612
|
this.cancelPendingTopFrame();
|
|
544
613
|
this.savePageOffset(this.state.selectedPage);
|
|
545
614
|
|
|
@@ -550,6 +619,13 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
550
619
|
() => {
|
|
551
620
|
this.scheduleScrollConfiguration();
|
|
552
621
|
const restore = () => {
|
|
622
|
+
if (
|
|
623
|
+
this.selectionRestoreToken !== selectionToken ||
|
|
624
|
+
this.state.selectedPage !== position ||
|
|
625
|
+
this.pages().map(pageKey)[position] !== expectedKey ||
|
|
626
|
+
!this.pageScrollElement(position)?.isConnected
|
|
627
|
+
)
|
|
628
|
+
return;
|
|
553
629
|
this.configureActiveScrollElement();
|
|
554
630
|
this.restorePageOffset(position);
|
|
555
631
|
};
|
|
@@ -629,7 +705,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
629
705
|
const deltaX = touch.pageX - this.touchStartX;
|
|
630
706
|
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
631
707
|
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX))
|
|
632
|
-
this.
|
|
708
|
+
this.takeOverVerticalScroll();
|
|
633
709
|
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
634
710
|
this.cancelPendingTopFrame();
|
|
635
711
|
this.horizontalTouch = true;
|