@onekeyfe/react-native-pager-view 3.0.157-alpha.253 → 3.0.157-alpha.255

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/README.md CHANGED
@@ -75,3 +75,5 @@ including unmounted pages that would show blank.
75
75
 
76
76
  Thank you again to Callstack and everyone who contributes to
77
77
  `react-native-pager-view` 💙
78
+
79
+ The native ancestor-scroll contribution protocol is specified in [docs/SPEC.md](docs/SPEC.md).
@@ -20,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
20
20
  import androidx.viewpager2.widget.ViewPager2
21
21
  import com.facebook.react.R as ReactR
22
22
  import com.facebook.react.uimanager.events.NativeGestureUtil
23
+ import com.facebook.react.views.scroll.ReactScrollView
23
24
  import com.margelo.nitro.nativelogger.OneKeyLog
24
25
  import java.util.WeakHashMap
25
26
  import kotlin.math.max
@@ -508,6 +509,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
508
509
  applyHeaderOffset()
509
510
  }
510
511
 
512
+ private fun publishConsumedOffset(recycler: RecyclerView, remove: Boolean = false) {
513
+ val key = R.id.onekey_native_scroll_coordinator_consumed_offsets
514
+ val existing = recycler.getTag(key) as? NativeScrollCoordinatorContributions
515
+ val contributions = existing ?: if (remove) return else NativeScrollCoordinatorContributions().also {
516
+ recycler.setTag(key, it)
517
+ }
518
+ val changed = if (remove) contributions.clearOwner(this) else contributions.update(this, headerOffsetPx)
519
+ if (changed) {
520
+ (recycler.getTag(R.id.onekey_native_scroll_coordinator_offset_listener) as? Runnable)?.run()
521
+ }
522
+ if (contributions.isEmpty()) recycler.setTag(key, null)
523
+ }
524
+
511
525
  private fun applyHeaderOffset() {
512
526
  val offset = headerOffsetPx.toFloat()
513
527
  pager.translationY = -offset
@@ -515,6 +529,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
515
529
  stickyHeaderView?.translationY = -offset
516
530
  nativeTabBarView.translationY = -offset
517
531
  nativeSubHeaderView.translationY = -offset
532
+ observedRecyclerView?.let { publishConsumedOffset(it) }
518
533
  }
519
534
 
520
535
  private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
@@ -616,12 +631,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
616
631
  private fun attachRecyclerObserver(recycler: RecyclerView) {
617
632
  if (observedRecyclerView === recycler) {
618
633
  applyRecyclerInsets(recycler, selectedPage)
634
+ publishConsumedOffset(recycler)
619
635
  return
620
636
  }
621
637
 
622
638
  detachPrimaryScrollObserver()
623
639
  observedRecyclerView = recycler
624
640
  applyRecyclerInsets(recycler, selectedPage)
641
+ publishConsumedOffset(recycler)
625
642
  val listener = object : RecyclerView.OnScrollListener() {
626
643
  override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
627
644
  val contentOffset = recyclerView.computeVerticalScrollOffset()
@@ -646,6 +663,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
646
663
  val listener = observedScrollListener
647
664
  if (recycler != null) {
648
665
  pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
666
+ publishConsumedOffset(recycler, remove = true)
649
667
  }
650
668
  if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
651
669
  if (recycler != null) {
@@ -775,11 +793,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
775
793
  }
776
794
  updateScrollerPaddingOwnership(scroller, original)
777
795
  val topInset = headerHeightPx + stickyHeaderHeightPx
778
- val top = original.top + topInset
779
796
  scroller.clipToPadding = false
780
- if (scroller.paddingLeft != original.left || scroller.paddingTop != top ||
781
- scroller.paddingRight != original.right || scroller.paddingBottom != original.bottom) {
782
- scroller.setPadding(original.left, top, original.right, original.bottom)
797
+ if (scroller is ReactScrollView) {
798
+ // Fabric owns the content child's layout, so ScrollView padding alone
799
+ // cannot move it. RN's native ScrollAway API also updates Fabric culling.
800
+ val state = scroller.reactScrollViewScrollState
801
+ val scrollAwayTop = nativeScrollerScrollAwayTop(
802
+ state.scrollAwayPaddingTop, original.appliedTopInset, topInset,
803
+ )
804
+ if (state.scrollAwayPaddingTop != scrollAwayTop ||
805
+ scroller.getChildAt(0)?.translationY != scrollAwayTop.toFloat()) {
806
+ scroller.setScrollAwayPaddingEnabledUnstable(scrollAwayTop, state.scrollAwayPaddingBottom)
807
+ }
808
+ val bottom = original.bottom + topInset
809
+ if (scroller.paddingLeft != original.left || scroller.paddingTop != original.top ||
810
+ scroller.paddingRight != original.right || scroller.paddingBottom != bottom) {
811
+ scroller.setPadding(original.left, original.top, original.right, bottom)
812
+ }
813
+ } else {
814
+ val top = original.top + topInset
815
+ if (scroller.paddingLeft != original.left || scroller.paddingTop != top ||
816
+ scroller.paddingRight != original.right || scroller.paddingBottom != original.bottom) {
817
+ scroller.setPadding(original.left, top, original.right, original.bottom)
818
+ }
783
819
  }
784
820
  original.appliedTopInset = topInset
785
821
 
@@ -804,13 +840,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
804
840
  scroller: ScrollView,
805
841
  original: ScrollerPadding,
806
842
  ) {
807
- val expectedTop = original.top + original.appliedTopInset
843
+ val isReactScroller = scroller is ReactScrollView
844
+ if (scroller is ReactScrollView) {
845
+ val state = scroller.reactScrollViewScrollState
846
+ if (nativeScrollerPaddingWasResetByScrollAway(
847
+ original.appliedTopInset,
848
+ scroller.paddingLeft, scroller.paddingTop, scroller.paddingRight, scroller.paddingBottom,
849
+ state.scrollAwayPaddingTop, state.scrollAwayPaddingBottom,
850
+ )) {
851
+ // RN may replay ScrollAway state before this pass. Its setter clears
852
+ // caller padding; that native reset must not become our new baseline.
853
+ return
854
+ }
855
+ }
856
+ val ownedTop = if (isReactScroller) 0 else original.appliedTopInset
857
+ val ownedBottom = if (isReactScroller) original.appliedTopInset else 0
808
858
  if (scroller.paddingLeft != original.left) original.left = scroller.paddingLeft
809
- if (scroller.paddingTop != expectedTop) {
810
- original.top = (scroller.paddingTop - original.appliedTopInset).coerceAtLeast(0)
859
+ if (scroller.paddingTop != original.top + ownedTop) {
860
+ original.top = (scroller.paddingTop - ownedTop).coerceAtLeast(0)
811
861
  }
812
862
  if (scroller.paddingRight != original.right) original.right = scroller.paddingRight
813
- if (scroller.paddingBottom != original.bottom) original.bottom = scroller.paddingBottom
863
+ if (scroller.paddingBottom != original.bottom + ownedBottom) {
864
+ original.bottom = (scroller.paddingBottom - ownedBottom).coerceAtLeast(0)
865
+ }
814
866
  if (scroller.clipToPadding) original.clipToPadding = true
815
867
  }
816
868
 
@@ -835,6 +887,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
835
887
  }
836
888
 
837
889
  private fun restoreRecyclerInsets(recycler: RecyclerView) {
890
+ publishConsumedOffset(recycler, remove = true)
838
891
  val original = originalRecyclerPadding.remove(recycler) ?: return
839
892
  updateRecyclerPaddingOwnership(recycler, original)
840
893
  restoredRecyclerKeys.remove(recycler)
@@ -846,6 +899,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
846
899
  val original = originalScrollerPadding.remove(scroller) ?: return
847
900
  updateScrollerPaddingOwnership(scroller, original)
848
901
  restoredScrollerKeys.remove(scroller)
902
+ if (scroller is ReactScrollView) {
903
+ val state = scroller.reactScrollViewScrollState
904
+ scroller.setScrollAwayPaddingEnabledUnstable(
905
+ nativeScrollerScrollAwayTop(state.scrollAwayPaddingTop, original.appliedTopInset, 0),
906
+ state.scrollAwayPaddingBottom,
907
+ )
908
+ }
849
909
  scroller.clipToPadding = original.clipToPadding
850
910
  scroller.setPadding(original.left, original.top, original.right, original.bottom)
851
911
  }
@@ -0,0 +1,15 @@
1
+ package com.reactnativepagerview
2
+
3
+ import java.util.WeakHashMap
4
+
5
+ /** Native tag payload; leaf observers can read it as Map without a pager dependency. */
6
+ internal class NativeScrollCoordinatorContributions : WeakHashMap<Any, Int>() {
7
+ fun update(owner: Any, pixels: Int): Boolean {
8
+ val value = pixels.coerceAtLeast(0)
9
+ if (get(owner) == value) return false
10
+ put(owner, value)
11
+ return true
12
+ }
13
+
14
+ fun clearOwner(owner: Any): Boolean = remove(owner) != null
15
+ }
@@ -0,0 +1,17 @@
1
+ package com.reactnativepagerview
2
+
3
+ // Replace only this pager's contribution; another native owner may share the scroller.
4
+ internal fun nativeScrollerScrollAwayTop(currentTop: Int, previousInset: Int, nextInset: Int): Int =
5
+ (currentTop - previousInset).coerceAtLeast(0) + nextInset
6
+
7
+ // ReactScrollView's ScrollAway setter always writes this exact padding shape.
8
+ internal fun nativeScrollerPaddingWasResetByScrollAway(
9
+ appliedInset: Int,
10
+ left: Int,
11
+ top: Int,
12
+ right: Int,
13
+ bottom: Int,
14
+ scrollAwayTop: Int,
15
+ scrollAwayBottom: Int,
16
+ ): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
17
+ bottom == scrollAwayTop + scrollAwayBottom
@@ -0,0 +1,4 @@
1
+ <resources>
2
+ <item name="onekey_native_scroll_coordinator_consumed_offsets" type="id" />
3
+ <item name="onekey_native_scroll_coordinator_offset_listener" type="id" />
4
+ </resources>
@@ -0,0 +1,26 @@
1
+ package com.reactnativepagerview
2
+
3
+ /** Standalone JVM regression: compile together with the production helper. */
4
+ fun main() {
5
+ val outer = Any()
6
+ val inner = Any()
7
+ val firstChild = NativeScrollCoordinatorContributions()
8
+ check(firstChild.update(outer, 120))
9
+ check(firstChild.update(inner, 60))
10
+ check(firstChild.values.sum() == 180)
11
+ check(!firstChild.update(outer, 120))
12
+ check(firstChild.update(outer, 30))
13
+ check(firstChild.values.sum() == 90)
14
+ check(firstChild.clearOwner(outer))
15
+ check(firstChild.values.sum() == 60)
16
+ check(!firstChild.clearOwner(outer))
17
+ val secondChild = NativeScrollCoordinatorContributions()
18
+ check(secondChild.update(outer, 30))
19
+ check(secondChild.values.sum() == 30)
20
+ check(firstChild.values.sum() == 60)
21
+ check(firstChild.clearOwner(inner))
22
+ check(firstChild.isEmpty())
23
+ check(secondChild.update(outer, -1))
24
+ check(secondChild.values.sum() == 0)
25
+ println("Native coordinator contributions: nested owners, duplicate updates, cleanup, and replacement passed")
26
+ }
@@ -0,0 +1,37 @@
1
+ package com.reactnativepagerview
2
+
3
+ /** Standalone JVM contract regression for Fabric scroller inset ownership. */
4
+ fun main() {
5
+ val initialTop = 24
6
+ val firstHeader = 182 + 48
7
+ val initialApplied = nativeScrollerScrollAwayTop(initialTop, 0, firstHeader)
8
+ check(initialApplied == 254)
9
+ check(nativeScrollerScrollAwayTop(initialApplied, firstHeader, firstHeader) == initialApplied)
10
+ val measuredHeader = 310 + 48
11
+ val resized = nativeScrollerScrollAwayTop(initialApplied, firstHeader, measuredHeader)
12
+ check(resized == initialTop + measuredHeader)
13
+ // A second pager's contribution survives the first pager's detach.
14
+ val nested = nativeScrollerScrollAwayTop(resized, 0, 100)
15
+ val outerDetached = nativeScrollerScrollAwayTop(nested, measuredHeader, 0)
16
+ check(outerDetached == initialTop + 100)
17
+ check(nativeScrollerScrollAwayTop(outerDetached, 100, 0) == initialTop)
18
+ // Fabric may reset state while replacing its content root; do not apply a negative baseline.
19
+ check(nativeScrollerScrollAwayTop(0, measuredHeader, firstHeader) == firstHeader)
20
+ // Caller L/R/B = 16/16/40 must survive RN's setter replay after our 230px inset.
21
+ val caller = intArrayOf(16, 0, 16, 40)
22
+ val rnReplay = intArrayOf(0, 0, 0, 230)
23
+ val keepCaller = nativeScrollerPaddingWasResetByScrollAway(
24
+ 230, rnReplay[0], rnReplay[1], rnReplay[2], rnReplay[3], 230, 0,
25
+ )
26
+ check(keepCaller)
27
+ val restored = if (keepCaller) caller else rnReplay
28
+ check(restored.contentEquals(intArrayOf(16, 0, 16, 40)))
29
+ // Nonzero native bottom state is part of RN's reset signature too.
30
+ check(nativeScrollerPaddingWasResetByScrollAway(230, 0, 0, 0, 278, 254, 24))
31
+ // Actual caller updates, including clearing all padding, remain observable.
32
+ check(!nativeScrollerPaddingWasResetByScrollAway(230, 24, 0, 24, 290, 230, 0))
33
+ check(!nativeScrollerPaddingWasResetByScrollAway(230, 0, 0, 0, 0, 230, 0))
34
+ // Initial discovery must capture caller values before this pager owns an inset.
35
+ check(!nativeScrollerPaddingWasResetByScrollAway(0, 0, 0, 0, 230, 230, 0))
36
+ println("NativeScroller ScrollAway ownership: initial, repeat, measured resize, nested detach, Fabric reset, RN padding replay, and caller updates passed")
37
+ }
package/docs/SPEC.md ADDED
@@ -0,0 +1,108 @@
1
+ # PagerView native scrolling contract
2
+
3
+ Existing PagerView and CollapsiblePagerView APIs are documented in README.md.
4
+ The sections below define native coordination boundaries without new React props.
5
+
6
+ ## iOS shared-header hit testing
7
+
8
+ Smooth-header hit forwarding is restricted to the pager's own interactive
9
+ viewport. A collapsed header clipped above that viewport MUST NOT intercept
10
+ touches intended for a sibling toolbar. Hidden, disabled and effectively
11
+ transparent pagers do not forward touches. Fabric `pointerEvents="none"` and
12
+ `"box-only"` retain their superclass behavior; `"auto"` and `"box-none"` may
13
+ forward to visible shared-header children inside the pager.
14
+
15
+ The shared header's internal hit testing still supports sticky children that
16
+ have moved outside the header host's original bounds while remaining inside the
17
+ pager. Focused UIKit tests cover sibling-toolbar hit delivery, pinned-child
18
+ delivery, pointer-event modes and interaction flags. Device acceptance includes
19
+ toolbar taps with expanded and collapsed headers on retained pages.
20
+
21
+ ## iOS cached-offset restoration after content shrink
22
+
23
+ When restoring a retained page's cached vertical offset, the pager limits only
24
+ the upper bound to the current content height, viewport and applied insets. A
25
+ cache captured before rows were removed MUST NOT restore an offset below the new
26
+ content bottom, including when removal occurs during a page transition. Valid
27
+ deep offsets and the existing lower-bound/header alignment rules are unchanged.
28
+ This check runs only on explicit restoration, not on scroll frames; UIKit pull
29
+ overscroll and refresh settlement retain their existing ownership. An unmeasured
30
+ zero-height viewport defers this upper-bound limit to a later restoration.
31
+
32
+ The focused policy test covers shrink after the list has already corrected its
33
+ offset, valid deep offsets, negative pull offsets and an unmeasured viewport.
34
+ Runtime acceptance requires clearing a retained page during a page transition,
35
+ then returning without an extra drag to expose its header/empty/footer slots.
36
+
37
+
38
+ ## Ancestor consumed-scroll contributions
39
+
40
+ Status: source implementation; real nested-pager runtime verification pending.
41
+ Android collapsible pagers consume header travel before the primary RecyclerView
42
+ receives content-scroll deltas. A leaf threshold observer needs both distances.
43
+ The coordinator publishes a non-negative physical-pixel contribution under its
44
+ own weak owner identity on the active primary RecyclerView. Nested coordinators
45
+ contribute independently; summing their values gives ancestor-consumed travel.
46
+
47
+ The native View tag named `onekey_native_scroll_coordinator_consumed_offsets`
48
+ stores a WeakHashMap with owner-view keys and integer pixel values. The optional
49
+ `onekey_native_scroll_coordinator_offset_listener` tag holds a Runnable owned by
50
+ the leaf. The publisher updates only its own entry and invokes the listener only
51
+ when that entry changes. An absent map/listener means zero/no observer.
52
+ This protocol depends only on Android View/resources and Java collections; the
53
+ pager MUST NOT depend on NativeList or infer Home/business semantics.
54
+
55
+ On switching the primary child, restoring insets, or detaching, the coordinator
56
+ removes only its own contribution. It MUST NOT retain an old RecyclerView or
57
+ another coordinator. Zero contributions may remain while the owner is attached;
58
+ weak keys prevent abandoned owners from becoming resource roots. The leaf owns
59
+ listener registration and cleanup. All operations occur on the UI thread; there
60
+ are no JavaScript scroll-frame callbacks in this protocol.
61
+
62
+ A new child receives current header travel before threshold evaluation. Native
63
+ list offsets remain local to the leaf, while consumers of this protocol add all
64
+ ancestor contributions exactly once. Programmatic scrolling/header restoration
65
+ must publish the resulting header offset through the same path.
66
+
67
+ ## Acceptance
68
+
69
+ The focused JVM helper test covers independent ancestor contributions and removal.
70
+ Required Android runtime cases are one/nested collapsible pagers, header-only
71
+ scrolling, reversal to expanded top, switching pages, native child replacement,
72
+ and detach/remount without stale offsets. iOS uses its existing normalized
73
+ UIScrollView offset and requires no equivalent tag protocol. Source/helper
74
+ checks do not establish the Android runtime acceptance above.
75
+
76
+
77
+ ## Android Fabric NativeScroller content insets
78
+
79
+ A ReactScrollView's content root is laid out by Fabric. Native `setPadding(top)`
80
+ alone MUST NOT be used to reserve a collapsible header: ReactScrollView does not
81
+ perform the platform ScrollView child layout, so that leaves rows under the
82
+ header. For the supported RN 0.86 runtime, the coordinator uses
83
+ `setScrollAwayPaddingEnabledUnstable` to offset the content root and synchronize
84
+ Fabric content-origin/culling state. The equivalent inset is added to the native
85
+ scroll range at the bottom; it is not added a second time at the top. Ordinary
86
+ Android ScrollView and RecyclerView retain their existing inset paths.
87
+
88
+ Each coordinator replaces only its previous header-plus-sticky contribution.
89
+ Measured-height changes do not accumulate insets; detach restores the remaining
90
+ ScrollAway state and caller padding, including another native owner's inset.
91
+ Content-root replacement reapplies the native offset. No Home layout values,
92
+ new React props, or per-frame JS updates are introduced; iOS/Web are unchanged.
93
+
94
+ The focused JVM policy regression covers an existing inset, repeated application,
95
+ measured-header resize, nested-owner detach, and Fabric state reset. The API call
96
+ is compile-checked against the installed React Android 0.86.2 artifact. Runtime
97
+ acceptance requires first content below the expanded sticky tabs, collapse and
98
+ expand without overlap, retained-page switching, header height changes, and
99
+ reaching the actual final content without an extra header-sized blank footer.
100
+
101
+ RN's ScrollAway setter (including native state replay when Fabric culling flags
102
+ are enabled) resets physical padding to `(0, 0, 0, scrollAwayTop + scrollAwayBottom)`.
103
+ While owning an inset, the coordinator recognizes that exact native reset and
104
+ retains saved caller padding; other physical-padding updates remain observable.
105
+ The focused policy regression includes nonzero caller left/right/bottom padding,
106
+ nonzero prior native insets, explicit caller padding changes/clear, and initial
107
+ discovery before ownership. Current Home zero-padding behavior is unchanged by
108
+ this ownership guard.
@@ -9,4 +9,12 @@ FOUNDATION_EXPORT void RNCPruneReleasedPageStates(
9
9
  NSArray<NSString *> *validPageKeys
10
10
  );
11
11
 
12
+ FOUNDATION_EXPORT CGFloat RNCLimitRestoredScrollOffset(
13
+ CGFloat offset,
14
+ CGFloat contentHeight,
15
+ CGFloat viewportHeight,
16
+ CGFloat topInset,
17
+ CGFloat bottomInset
18
+ );
19
+
12
20
  NS_ASSUME_NONNULL_END
@@ -1,5 +1,18 @@
1
1
  #import "RNCCollapsiblePagerReleasedStatePolicy.h"
2
2
 
3
+ CGFloat RNCLimitRestoredScrollOffset(
4
+ CGFloat offset,
5
+ CGFloat contentHeight,
6
+ CGFloat viewportHeight,
7
+ CGFloat topInset,
8
+ CGFloat bottomInset
9
+ )
10
+ {
11
+ if (viewportHeight <= 0) return offset;
12
+ CGFloat maximum = MAX(-topInset, contentHeight - viewportHeight + bottomInset);
13
+ return MIN(offset, maximum);
14
+ }
15
+
3
16
  void RNCPruneReleasedPageStates(
4
17
  NSMutableDictionary *releasedStates,
5
18
  NSArray<NSString *> *validPageKeys
@@ -1145,6 +1145,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1145
1145
 
1146
1146
  - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
1147
1147
  {
1148
+ // Shared headers can sit above their clipped viewport after collapsing.
1149
+ // Forward only viewport touches, so invisible headers cannot cover siblings.
1150
+ if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01 ||
1151
+ ![self pointInside:point withEvent:event]) return nil;
1152
+ const auto &viewProps =
1153
+ *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
1154
+ if (viewProps.pointerEvents == PointerEventsMode::None ||
1155
+ viewProps.pointerEvents == PointerEventsMode::BoxOnly) {
1156
+ return [super hitTest:point withEvent:event];
1157
+ }
1148
1158
  if (_nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
1149
1159
  _sharedHeaderHostView.superview != _containerView) {
1150
1160
  CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
@@ -2580,6 +2590,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2580
2590
  }
2581
2591
  targetOffset = restoredLogicalOffset - next.top;
2582
2592
  targetOffset = MAX(targetOffset, -next.top + _headerOffset);
2593
+ // An inactive list may shrink after its offset observer was detached.
2594
+ // Do not overwrite its corrected offset with a now-unreachable cached one.
2595
+ targetOffset = RNCLimitRestoredScrollOffset(
2596
+ targetOffset,
2597
+ scrollView.contentSize.height,
2598
+ scrollView.bounds.size.height,
2599
+ next.top,
2600
+ next.bottom
2601
+ );
2583
2602
  }
2584
2603
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
2585
2604
  if (restore || previousTopInset != next.top) {
@@ -0,0 +1,80 @@
1
+ #import <XCTest/XCTest.h>
2
+
3
+ #import "RNCCollapsiblePagerViewComponentView.h"
4
+ #import <react/renderer/components/pagerview/Props.h>
5
+
6
+ using namespace facebook::react;
7
+
8
+ @interface RNCCollapsiblePagerHitTestTests : XCTestCase
9
+ @property (nonatomic, strong) UIView *root;
10
+ @property (nonatomic, strong) UIButton *toolbar;
11
+ @property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
12
+ @property (nonatomic, strong) UIButton *sticky;
13
+ @end
14
+
15
+ @implementation RNCCollapsiblePagerHitTestTests
16
+
17
+ - (void)setPointerEvents:(PointerEventsMode)mode
18
+ {
19
+ auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
20
+ props->nativeSmoothHeaderScrollEnabled = true;
21
+ props->pointerEvents = mode;
22
+ [self.pager updateProps:props oldProps:nullptr];
23
+ }
24
+
25
+ - (void)setUp
26
+ {
27
+ [super setUp];
28
+ self.root = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
29
+ self.toolbar = [[UIButton alloc] initWithFrame:CGRectMake(0, 36, 320, 64)];
30
+ [self.root addSubview:self.toolbar];
31
+ self.pager = [[RNCCollapsiblePagerViewComponentView alloc] init];
32
+ [self setPointerEvents:PointerEventsMode::Auto];
33
+ self.pager.frame = CGRectMake(0, 100, 320, 500);
34
+ [self.root addSubview:self.pager];
35
+
36
+ // Model smooth mode's header reparenting and its collapsed, clipped content.
37
+ UIView *scroll = [[UIView alloc] initWithFrame:self.pager.bounds];
38
+ [self.pager addSubview:scroll];
39
+ UIView *header = [self.pager valueForKey:@"sharedHeaderHostView"];
40
+ for (UIView *child in header.subviews.copy) [child removeFromSuperview];
41
+ [scroll addSubview:header];
42
+ header.frame = CGRectMake(0, -64, 320, 108);
43
+ [header addSubview:[[UIButton alloc] initWithFrame:CGRectMake(0, 0, 320, 64)]];
44
+ self.sticky = [[UIButton alloc] initWithFrame:CGRectMake(0, 140, 320, 44)];
45
+ [header addSubview:self.sticky];
46
+ }
47
+
48
+ - (void)testCollapsedHeaderDoesNotInterceptSiblingToolbar
49
+ {
50
+ XCTAssertEqual([self.root hitTest:CGPointMake(20, 80) withEvent:nil], self.toolbar);
51
+ }
52
+
53
+ - (void)testPinnedChildOutsideHeaderHostStillReceivesViewportTouches
54
+ {
55
+ XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
56
+ [self setPointerEvents:PointerEventsMode::BoxNone];
57
+ XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
58
+ }
59
+
60
+ - (void)testFabricPointerEventsAreNotBypassed
61
+ {
62
+ [self setPointerEvents:PointerEventsMode::None];
63
+ XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
64
+ [self setPointerEvents:PointerEventsMode::BoxOnly];
65
+ XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.pager);
66
+ }
67
+
68
+ - (void)testNoninteractivePagerDoesNotForwardHeaderTouches
69
+ {
70
+ self.pager.hidden = YES;
71
+ XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
72
+ self.pager.hidden = NO;
73
+ self.pager.userInteractionEnabled = NO;
74
+ XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
75
+ self.pager.userInteractionEnabled = YES;
76
+ self.pager.alpha = 0.005;
77
+ XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
78
+ }
79
+
80
+ @end
@@ -7,6 +7,29 @@
7
7
 
8
8
  @implementation RNCCollapsiblePagerReleasedStatePolicyTests
9
9
 
10
+ - (void)testRestorationAfterInactiveContentShrinkCannotReapplyStaleCache
11
+ {
12
+ // The list already corrected to -44 while inactive, but the pager still
13
+ // retains the old logical offset captured before rows were removed.
14
+ CGFloat savedLogicalOffset = 513.67;
15
+ CGFloat restored = RNCLimitRestoredScrollOffset(
16
+ savedLogicalOffset - 108, 132, 708, 108, 532);
17
+ XCTAssertEqualWithAccuracy(restored, -44, 0.001);
18
+ XCTAssertEqualWithAccuracy(restored + 108, 64, 0.001);
19
+ XCTAssertEqualWithAccuracy(
20
+ RNCLimitRestoredScrollOffset(restored, 132, 708, 108, 532), restored, 0.001);
21
+ }
22
+
23
+ - (void)testRestorationPreservesValidDeepAndNegativeOffsets
24
+ {
25
+ XCTAssertEqualWithAccuracy(
26
+ RNCLimitRestoredScrollOffset(600, 2400, 708, 108, 0), 600, 0.001);
27
+ XCTAssertEqualWithAccuracy(
28
+ RNCLimitRestoredScrollOffset(-160, 132, 708, 108, 532), -160, 0.001);
29
+ XCTAssertEqualWithAccuracy(
30
+ RNCLimitRestoredScrollOffset(600, 0, 0, 108, 0), 600, 0.001);
31
+ }
32
+
10
33
  - (void)testPrunesRemovedKeysAndKeepsStableKeysAcrossReorders
11
34
  {
12
35
  NSObject *alpha = [NSObject new];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.157-alpha.253",
3
+ "version": "3.0.157-alpha.255",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -21,6 +21,7 @@
21
21
  "lib",
22
22
  "android",
23
23
  "ios",
24
+ "docs",
24
25
  "react-native-pager-view.podspec",
25
26
  "!ios/build",
26
27
  "!android/build",
@@ -66,7 +67,7 @@
66
67
  "typescript": "^5.9.2"
67
68
  },
68
69
  "peerDependencies": {
69
- "@onekeyfe/react-native-native-logger": "3.0.157-alpha.253",
70
+ "@onekeyfe/react-native-native-logger": "3.0.157-alpha.255",
70
71
  "react": "*",
71
72
  "react-native": "*"
72
73
  },