@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.
@@ -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.cancelPendingTopFrame();
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.cancelPendingTopFrame();
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, offset);
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.cancelPendingTopFrame);
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.cancelPendingTopFrame);
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
- onVerticalScrollSettled = () => {
303
- if (this.pendingTopFrame) return;
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.pageOffsets.set(key, scrollElement.scrollTop);
312
- this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), Math.max(0, this.props.headerHeight));
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
- scrollElement.scrollTop = Math.max(this.pageOffsets.get(key) ?? this.sharedHeaderOffset, this.sharedHeaderOffset);
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.cancelPendingTopFrame();
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.175",
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.175",
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.cancelPendingTopFrame();
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.cancelPendingTopFrame();
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, offset);
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.cancelPendingTopFrame
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.cancelPendingTopFrame
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 onVerticalScrollSettled = () => {
505
- if (this.pendingTopFrame) return;
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
- this.pageOffsets.set(key, scrollElement.scrollTop);
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
- Math.max(0, this.props.headerHeight)
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
- scrollElement.scrollTop = Math.max(
527
- this.pageOffsets.get(key) ?? this.sharedHeaderOffset,
528
- this.sharedHeaderOffset
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.cancelPendingTopFrame();
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;