@onekeyfe/react-native-pager-view 3.0.158 → 3.0.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +57 -4
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +379 -31
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
  4. package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
  5. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +41 -0
  6. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
  8. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  9. package/android/src/main/jni/CMakeLists.txt +19 -0
  10. package/android/src/main/jni/pagerview.h +11 -0
  11. package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
  12. package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
  13. package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
  14. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +75 -0
  15. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
  16. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
  17. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
  18. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
  19. package/docs/SPEC.md +451 -0
  20. package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
  21. package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
  22. package/ios/RNCCollapsiblePagerViewComponentView.mm +420 -2
  23. package/ios/RNCNativeScrollerComponentView.h +17 -0
  24. package/ios/RNCNativeScrollerComponentView.mm +479 -0
  25. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
  26. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +788 -0
  27. package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
  28. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +214 -0
  29. package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
  30. package/ios/tests/RNCNativeScrollerTests.mm +252 -0
  31. package/lib/module/CollapsiblePagerView.js +10 -1
  32. package/lib/module/NativeScroller.js +171 -11
  33. package/lib/module/NativeScrollerNativeComponent.ts +101 -0
  34. package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
  35. package/lib/typescript/src/NativeScroller.d.ts +15 -7
  36. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
  37. package/package.json +8 -4
  38. package/react-native-pager-view.podspec +2 -1
  39. package/react-native.config.js +15 -0
  40. package/src/CollapsiblePagerView.tsx +14 -4
  41. package/src/NativeScroller.tsx +291 -14
  42. package/src/NativeScrollerNativeComponent.ts +101 -0
@@ -1,4 +1,5 @@
1
1
  #import "RNCCollapsiblePagerViewComponentView.h"
2
+ #import "RNCNativeScrollerComponentView.h"
2
3
  #import "RNCCollapsiblePagerReleasedStatePolicy.h"
3
4
 
4
5
  #import <react/renderer/components/pagerview/ComponentDescriptors.h>
@@ -11,6 +12,8 @@
11
12
  #import "React/RCTScrollViewComponentView.h"
12
13
  #import "React/RCTSurfaceTouchHandler.h"
13
14
  #import "React/RCTTouchHandler.h"
15
+ #import <objc/runtime.h>
16
+ #import <UIKit/UIGestureRecognizerSubclass.h>
14
17
 
15
18
  // OneKey patch: deduplicate structural mutation parents before scanning the page.
16
19
  #include <unordered_set>
@@ -18,6 +21,116 @@
18
21
  using namespace facebook::react;
19
22
 
20
23
  static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
24
+ static void *RNCRefreshControlLayerContext = &RNCRefreshControlLayerContext;
25
+ static void *RNCScrollRefreshControlContext = &RNCScrollRefreshControlContext;
26
+ static char RNCRefreshControlOffsetKey;
27
+
28
+ @interface RNCRefreshControlOffsetState : NSObject
29
+ + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner;
30
+ + (void)removeControl:(UIRefreshControl *)control owner:(id)owner;
31
+ @end
32
+
33
+ @implementation RNCRefreshControlOffsetState {
34
+ __weak UIRefreshControl *_control;
35
+ NSMapTable<id, NSNumber *> *_offsets;
36
+ CATransform3D _baselineTransform;
37
+ CATransform3D _appliedTransform;
38
+ BOOL _originalMasksToBounds;
39
+ BOOL _applying;
40
+ BOOL _observing;
41
+ }
42
+
43
+ - (instancetype)initWithControl:(UIRefreshControl *)control
44
+ {
45
+ if ((self = [super init])) {
46
+ _control = control;
47
+ _baselineTransform = control.layer.sublayerTransform;
48
+ _originalMasksToBounds = control.layer.masksToBounds;
49
+ _offsets = [NSMapTable weakToStrongObjectsMapTable];
50
+ [control.layer addObserver:self forKeyPath:@"sublayerTransform"
51
+ options:NSKeyValueObservingOptionNew
52
+ context:RNCRefreshControlLayerContext];
53
+ [control.layer addObserver:self forKeyPath:@"masksToBounds"
54
+ options:NSKeyValueObservingOptionNew
55
+ context:RNCRefreshControlLayerContext];
56
+ _observing = YES;
57
+ }
58
+ return self;
59
+ }
60
+
61
+ - (void)apply
62
+ {
63
+ UIRefreshControl *control = _control;
64
+ if (control == nil) return;
65
+ CGFloat offset = 0;
66
+ for (NSNumber *value in _offsets.objectEnumerator) offset += value.doubleValue;
67
+ CATransform3D transform = CATransform3DConcat(
68
+ _baselineTransform, CATransform3DMakeTranslation(0, -offset, 0));
69
+ _appliedTransform = transform;
70
+ _applying = YES;
71
+ // Visual placement must not change UIKit's refresh geometry or RN's bounds
72
+ // origin (progressViewOffset). Allow the shifted indicator outside its host.
73
+ [CATransaction begin];
74
+ [CATransaction setDisableActions:YES];
75
+ control.layer.masksToBounds = _offsets.count == 0 ? _originalMasksToBounds : NO;
76
+ if (!CATransform3DEqualToTransform(control.layer.sublayerTransform, transform)) {
77
+ control.layer.sublayerTransform = transform;
78
+ }
79
+ [CATransaction commit];
80
+ _applying = NO;
81
+ }
82
+
83
+ + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner
84
+ {
85
+ if (control == nil) return;
86
+ RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
87
+ if (state == nil) {
88
+ state = [[self alloc] initWithControl:control];
89
+ objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
90
+ }
91
+ [state->_offsets setObject:@(offset) forKey:owner];
92
+ [state apply];
93
+ }
94
+
95
+ + (void)removeControl:(UIRefreshControl *)control owner:(id)owner
96
+ {
97
+ if (control == nil) return;
98
+ RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
99
+ if (state == nil) return;
100
+ [state->_offsets removeObjectForKey:owner];
101
+ [state apply];
102
+ if (state->_offsets.count == 0) {
103
+ [control.layer removeObserver:state forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
104
+ [control.layer removeObserver:state forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
105
+ state->_observing = NO;
106
+ objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
107
+ }
108
+ }
109
+
110
+ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
111
+ change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
112
+ {
113
+ if (context == RNCRefreshControlLayerContext) {
114
+ if (_applying) return;
115
+ if ([keyPath isEqualToString:@"sublayerTransform"]) {
116
+ CATransform3D next = [change[NSKeyValueChangeNewKey] CATransform3DValue];
117
+ if (CATransform3DEqualToTransform(next, _appliedTransform)) return;
118
+ _baselineTransform = next;
119
+ }
120
+ [self apply];
121
+ return;
122
+ }
123
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
124
+ }
125
+
126
+ - (void)dealloc
127
+ {
128
+ if (_observing && _control != nil) {
129
+ [_control.layer removeObserver:self forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
130
+ [_control.layer removeObserver:self forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
131
+ }
132
+ }
133
+ @end
21
134
  static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
22
135
  @"rnc-collapsible-pager-native-scroller:";
23
136
 
@@ -908,6 +1021,144 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
908
1021
 
909
1022
  @end
910
1023
 
1024
+ // This observer never competes for scrolling. The owning pager explicitly
1025
+ // recognizes it only after its real pan begins, so UIKit terminates RN presses.
1026
+ @interface RNCCollapsiblePagerContentPressCancellationGestureRecognizer : UIGestureRecognizer
1027
+ @property (nonatomic, weak) UIGestureRecognizer *reactTouchHandler;
1028
+ - (void)cancelPressForPan:(UIPanGestureRecognizer *)pan;
1029
+ - (void)observeIncomingTouch:(UITouch *)touch;
1030
+ @end
1031
+
1032
+ @implementation RNCCollapsiblePagerContentPressCancellationGestureRecognizer {
1033
+ __weak UITouch *_trackedTouch;
1034
+ UIEvent *_touchEvent;
1035
+ BOOL _multipleSurfaceTouchesObserved;
1036
+ CGPoint _trackedTouchStartPoint;
1037
+ BOOL _didCancelReactPressForVerticalMovement;
1038
+ }
1039
+
1040
+ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other
1041
+ {
1042
+ return other == self.reactTouchHandler;
1043
+ }
1044
+
1045
+ - (void)observeIncomingTouch:(UITouch *)touch
1046
+ {
1047
+ if (_trackedTouch != nil && touch != _trackedTouch &&
1048
+ [touch.view isDescendantOfView:self.reactTouchHandler.view]) {
1049
+ _multipleSurfaceTouchesObserved = YES;
1050
+ }
1051
+ }
1052
+
1053
+ - (void)recordEvent:(UIEvent *)event
1054
+ {
1055
+ _touchEvent = event;
1056
+ NSUInteger count = 0;
1057
+ for (UITouch *touch in event.allTouches) {
1058
+ if (touch.phase != UITouchPhaseEnded && touch.phase != UITouchPhaseCancelled &&
1059
+ [touch.view isDescendantOfView:self.reactTouchHandler.view]) ++count;
1060
+ }
1061
+ if (count > 1) _multipleSurfaceTouchesObserved = YES;
1062
+ }
1063
+
1064
+ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1065
+ {
1066
+ if (_trackedTouch != nil || touches.count != 1) {
1067
+ _multipleSurfaceTouchesObserved = YES;
1068
+ if (self.state == UIGestureRecognizerStatePossible) {
1069
+ self.state = UIGestureRecognizerStateFailed;
1070
+ }
1071
+ return;
1072
+ }
1073
+ _trackedTouch = touches.anyObject;
1074
+ _trackedTouchStartPoint = [_trackedTouch locationInView:self.view];
1075
+ _didCancelReactPressForVerticalMovement = NO;
1076
+ [self recordEvent:event];
1077
+ }
1078
+
1079
+ - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1080
+ {
1081
+ [self recordEvent:event];
1082
+ UITouch *touch = _trackedTouch;
1083
+ UIGestureRecognizer *handler = self.reactTouchHandler;
1084
+ if (touch == nil || ![touches containsObject:touch] ||
1085
+ self.state != UIGestureRecognizerStatePossible ||
1086
+ _didCancelReactPressForVerticalMovement || _multipleSurfaceTouchesObserved ||
1087
+ handler.view == nil || self.view != handler.view.superview ||
1088
+ handler.state == UIGestureRecognizerStateFailed ||
1089
+ handler.state == UIGestureRecognizerStateCancelled ||
1090
+ handler.state == UIGestureRecognizerStateEnded) return;
1091
+ CGPoint point = [touch locationInView:self.view];
1092
+ CGFloat dx = point.x - _trackedTouchStartPoint.x;
1093
+ CGFloat dy = point.y - _trackedTouchStartPoint.y;
1094
+ if (fabs(dy) < 10.0 || fabs(dy) <= fabs(dx)) return;
1095
+ NSUInteger activeTouches = 0;
1096
+ for (UITouch *activeTouch in event.allTouches) {
1097
+ if (activeTouch.phase == UITouchPhaseEnded ||
1098
+ activeTouch.phase == UITouchPhaseCancelled) continue;
1099
+ if ([activeTouch.view isDescendantOfView:handler.view]) ++activeTouches;
1100
+ }
1101
+ if (activeTouches != 1) return;
1102
+ _didCancelReactPressForVerticalMovement = YES;
1103
+ handler.enabled = NO;
1104
+ handler.enabled = YES;
1105
+ }
1106
+
1107
+ - (void)cancelPressForPan:(UIPanGestureRecognizer *)pan
1108
+ {
1109
+ UIGestureRecognizer *handler = self.reactTouchHandler;
1110
+ UITouch *touch = _trackedTouch;
1111
+ if (self.state != UIGestureRecognizerStatePossible || _multipleSurfaceTouchesObserved ||
1112
+ pan.state != UIGestureRecognizerStateBegan || touch == nil ||
1113
+ touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled ||
1114
+ handler.view == nil || self.view != handler.view.superview ||
1115
+ handler.state == UIGestureRecognizerStateFailed ||
1116
+ handler.state == UIGestureRecognizerStateCancelled ||
1117
+ handler.state == UIGestureRecognizerStateEnded) return;
1118
+
1119
+ // RN cancellation belongs to a Surface, not one finger. Do not terminate an
1120
+ // unrelated, already active touch on that Surface during a multi-touch sequence.
1121
+ NSUInteger activeTouches = 0;
1122
+ for (UITouch *activeTouch in _touchEvent.allTouches) {
1123
+ if (activeTouch.phase == UITouchPhaseEnded ||
1124
+ activeTouch.phase == UITouchPhaseCancelled) continue;
1125
+ if ([activeTouch.view isDescendantOfView:handler.view]) ++activeTouches;
1126
+ }
1127
+ if (activeTouches != 1) return;
1128
+ self.state = UIGestureRecognizerStateBegan;
1129
+ }
1130
+
1131
+ - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1132
+ {
1133
+ if (![touches containsObject:_trackedTouch]) return;
1134
+ self.state = self.state == UIGestureRecognizerStateBegan
1135
+ ? UIGestureRecognizerStateEnded : UIGestureRecognizerStateFailed;
1136
+ _trackedTouch = nil;
1137
+ _touchEvent = nil;
1138
+ _didCancelReactPressForVerticalMovement = NO;
1139
+ }
1140
+
1141
+ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1142
+ {
1143
+ if (![touches containsObject:_trackedTouch]) return;
1144
+ self.state = self.state == UIGestureRecognizerStateBegan
1145
+ ? UIGestureRecognizerStateCancelled : UIGestureRecognizerStateFailed;
1146
+ _trackedTouch = nil;
1147
+ _touchEvent = nil;
1148
+ _didCancelReactPressForVerticalMovement = NO;
1149
+ }
1150
+
1151
+ - (void)reset
1152
+ {
1153
+ [super reset];
1154
+ _trackedTouch = nil;
1155
+ _touchEvent = nil;
1156
+ _multipleSurfaceTouchesObserved = NO;
1157
+ _didCancelReactPressForVerticalMovement = NO;
1158
+ }
1159
+
1160
+ @end
1161
+
911
1162
  @interface RNCCollapsiblePagerOuterPagerPanGestureRecognizer : UIPanGestureRecognizer
912
1163
  @property (nonatomic, strong) NSHashTable<UIGestureRecognizer *> *blockedPagerGestures;
913
1164
  @end
@@ -954,7 +1205,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
954
1205
  UIPageViewControllerDelegate,
955
1206
  UIScrollViewDelegate,
956
1207
  UIGestureRecognizerDelegate,
957
- RCTMountingTransactionObserving
1208
+ RCTMountingTransactionObserving,
1209
+ RNCNativeScrollerInsetCoordinator
958
1210
  >
959
1211
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
960
1212
  - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
@@ -973,6 +1225,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
973
1225
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
974
1226
  - (void)updateSharedHeaderPressCancellationGesture;
975
1227
  - (void)detachSharedHeaderPressCancellationGesture;
1228
+ - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
1229
+ - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
976
1230
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
977
1231
  - (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer;
978
1232
  - (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer;
@@ -1023,6 +1277,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1023
1277
  NSString *_retainedPages;
1024
1278
  NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
1025
1279
  NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
1280
+ NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
1281
+ NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
1026
1282
  NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
1027
1283
  NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
1028
1284
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
@@ -1036,6 +1292,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1036
1292
  __weak UIView *_sharedHeaderPressCancellationGestureHost;
1037
1293
  __weak UIGestureRecognizer *_reactTouchHandler;
1038
1294
  UIPanGestureRecognizer *_sharedHeaderPressCancellationGesture;
1295
+ RNCCollapsiblePagerContentPressCancellationGestureRecognizer *_contentPressCancellationGesture;
1039
1296
  RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
1040
1297
  UIPanGestureRecognizer *_verticalPagerGesture;
1041
1298
  BOOL _observingContentOffset;
@@ -1066,6 +1323,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1066
1323
  _pageOffsets = [NSMutableDictionary new];
1067
1324
  _releasedPageScrollStates = [NSMutableDictionary new];
1068
1325
  _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
1326
+ _managedRefreshControls = [NSMapTable weakToWeakObjectsMapTable];
1327
+ _refreshControlObservedScrollViews = [NSHashTable weakObjectsHashTable];
1069
1328
  _appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
1070
1329
  _originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
1071
1330
  _originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
@@ -1145,6 +1404,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1145
1404
 
1146
1405
  - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
1147
1406
  {
1407
+ // Shared headers can sit above their clipped viewport after collapsing.
1408
+ // Forward only viewport touches, so invisible headers cannot cover siblings.
1409
+ if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01 ||
1410
+ ![self pointInside:point withEvent:event]) return nil;
1411
+ const auto &viewProps =
1412
+ *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
1413
+ if (viewProps.pointerEvents == PointerEventsMode::None ||
1414
+ viewProps.pointerEvents == PointerEventsMode::BoxOnly) {
1415
+ return [super hitTest:point withEvent:event];
1416
+ }
1148
1417
  if (_nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
1149
1418
  _sharedHeaderHostView.superview != _containerView) {
1150
1419
  CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
@@ -1168,6 +1437,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1168
1437
  {
1169
1438
  [super didMoveToWindow];
1170
1439
  [self updateSharedHeaderPressCancellationGesture];
1440
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1441
+ [self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight];
1442
+ }
1171
1443
  // OneKey patch: removing a decelerating page from its window can suppress
1172
1444
  // UIKit's final scroll callback. Restore the last acknowledged page on reattach.
1173
1445
  if (self.window != nil && _isPagerDragging &&
@@ -1191,6 +1463,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1191
1463
 
1192
1464
  - (void)dealloc
1193
1465
  {
1466
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1467
+ [self updateNativeScrollerViewport:scrollView stickyHeight:0];
1468
+ }
1469
+ for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
1470
+ [self releaseRefreshControlForScrollView:scrollView];
1471
+ }
1194
1472
  [self detachSharedHeaderPressCancellationGesture];
1195
1473
  }
1196
1474
 
@@ -1729,7 +2007,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1729
2007
  [self detachSharedHeaderPressCancellationGesture];
1730
2008
  [self detachScrollObserver];
1731
2009
  [self restoreSharedHeadersToContainer];
2010
+ for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
2011
+ [self releaseRefreshControlForScrollView:scrollView];
2012
+ }
1732
2013
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
2014
+ [self updateNativeScrollerViewport:scrollView stickyHeight:0];
1733
2015
  NSValue *value = [_originalInsets objectForKey:scrollView];
1734
2016
  if (value != nil) {
1735
2017
  UIEdgeInsets original = value.UIEdgeInsetsValue;
@@ -1997,6 +2279,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1997
2279
  return [(RNCCollapsiblePagerViewComponentView *)view
1998
2280
  currentVerticalScrollViewForParentPager];
1999
2281
  }
2282
+ if ([view isKindOfClass:RNCNativeScrollerComponentView.class]) {
2283
+ return ((RNCNativeScrollerComponentView *)view).scrollView;
2284
+ }
2000
2285
  if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
2001
2286
  RCTScrollViewComponentView *componentView =
2002
2287
  (RCTScrollViewComponentView *)view;
@@ -2182,6 +2467,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2182
2467
  forKeyPath:@"contentOffset"
2183
2468
  options:NSKeyValueObservingOptionNew
2184
2469
  context:RNCCollapsiblePagerContentOffsetContext];
2470
+ [candidate addObserver:self
2471
+ forKeyPath:@"contentInset"
2472
+ options:NSKeyValueObservingOptionNew
2473
+ context:RNCCollapsiblePagerContentOffsetContext];
2185
2474
  [candidate addObserver:self
2186
2475
  forKeyPath:@"contentSize"
2187
2476
  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
@@ -2204,6 +2493,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2204
2493
  [scrollView removeObserver:self
2205
2494
  forKeyPath:@"contentOffset"
2206
2495
  context:RNCCollapsiblePagerContentOffsetContext];
2496
+ [scrollView removeObserver:self
2497
+ forKeyPath:@"contentInset"
2498
+ context:RNCCollapsiblePagerContentOffsetContext];
2207
2499
  [scrollView removeObserver:self
2208
2500
  forKeyPath:@"contentSize"
2209
2501
  context:RNCCollapsiblePagerContentOffsetContext];
@@ -2211,10 +2503,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2211
2503
  }
2212
2504
  _observingContentOffset = NO;
2213
2505
  _observedScrollView = nil;
2506
+ if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
2507
+ [(RNCNativeScrollerComponentView *)scrollView.superview stopScrolling];
2508
+ }
2214
2509
  }
2215
2510
 
2216
2511
  - (void)detachSharedHeaderPressCancellationGesture
2217
2512
  {
2513
+ if (_contentPressCancellationGesture.view != nil) {
2514
+ [_contentPressCancellationGesture.view removeGestureRecognizer:_contentPressCancellationGesture];
2515
+ }
2516
+ _contentPressCancellationGesture.reactTouchHandler = nil;
2517
+ _contentPressCancellationGesture = nil;
2218
2518
  if (_sharedHeaderPressCancellationGesture.view != nil) {
2219
2519
  [_sharedHeaderPressCancellationGesture.view
2220
2520
  removeGestureRecognizer:_sharedHeaderPressCancellationGesture];
@@ -2299,6 +2599,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2299
2599
  _sharedHeaderOuterPagerGesture.maximumNumberOfTouches = 1;
2300
2600
  }
2301
2601
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
2602
+ if (_contentPressCancellationGesture == nil) {
2603
+ _contentPressCancellationGesture =
2604
+ [RNCCollapsiblePagerContentPressCancellationGestureRecognizer new];
2605
+ _contentPressCancellationGesture.delegate = self;
2606
+ // RN's external-recognizer path cancels its responder immediately only
2607
+ // when this flag is set; otherwise reset can be deferred until finger-up.
2608
+ _contentPressCancellationGesture.cancelsTouchesInView = YES;
2609
+ _contentPressCancellationGesture.delaysTouchesBegan = NO;
2610
+ _contentPressCancellationGesture.delaysTouchesEnded = NO;
2611
+ }
2612
+ _contentPressCancellationGesture.reactTouchHandler = reactTouchHandler;
2613
+ [gestureHost addGestureRecognizer:_contentPressCancellationGesture];
2302
2614
  _reactTouchHandler = reactTouchHandler;
2303
2615
  _sharedHeaderPressCancellationGestureHost = gestureHost;
2304
2616
  [gestureHost addGestureRecognizer:_sharedHeaderPressCancellationGesture];
@@ -2418,10 +2730,45 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2418
2730
  }
2419
2731
  }
2420
2732
 
2733
+ - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView
2734
+ {
2735
+ if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled) {
2736
+ [self releaseRefreshControlForScrollView:scrollView];
2737
+ return;
2738
+ }
2739
+ if (![_refreshControlObservedScrollViews containsObject:scrollView]) {
2740
+ [_refreshControlObservedScrollViews addObject:scrollView];
2741
+ [scrollView addObserver:self forKeyPath:@"refreshControl"
2742
+ options:NSKeyValueObservingOptionNew
2743
+ context:RNCScrollRefreshControlContext];
2744
+ }
2745
+ UIRefreshControl *previous = [_managedRefreshControls objectForKey:scrollView];
2746
+ UIRefreshControl *current = scrollView.refreshControl;
2747
+ if (previous != current) {
2748
+ [RNCRefreshControlOffsetState removeControl:previous owner:self];
2749
+ [_managedRefreshControls removeObjectForKey:scrollView];
2750
+ if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
2751
+ }
2752
+ [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
2753
+ forControl:current owner:self];
2754
+ }
2755
+
2756
+ - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
2757
+ {
2758
+ if ([_refreshControlObservedScrollViews containsObject:scrollView]) {
2759
+ [scrollView removeObserver:self forKeyPath:@"refreshControl" context:RNCScrollRefreshControlContext];
2760
+ [_refreshControlObservedScrollViews removeObject:scrollView];
2761
+ }
2762
+ [RNCRefreshControlOffsetState removeControl:[_managedRefreshControls objectForKey:scrollView] owner:self];
2763
+ [_managedRefreshControls removeObjectForKey:scrollView];
2764
+ }
2765
+
2421
2766
  // OneKey patch: release observers, headers and saved state while the scroll view
2422
2767
  // still represents the old list, before Fabric applies its next owner's props.
2423
2768
  - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
2424
2769
  {
2770
+ [self updateNativeScrollerViewport:scrollView stickyHeight:0];
2771
+ [self releaseRefreshControlForScrollView:scrollView];
2425
2772
  if (scrollView == _observedScrollView) {
2426
2773
  [self detachScrollObserver];
2427
2774
  if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
@@ -2538,6 +2885,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2538
2885
  // This host already includes the fixed header and safe area in its frame.
2539
2886
  // UIKit automatic adjustment would add them a second time to short pages.
2540
2887
  scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
2888
+ [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
2889
+ [self updateRefreshControlForScrollView:scrollView];
2541
2890
  CGFloat previousTopInset = scrollView.contentInset.top;
2542
2891
  CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
2543
2892
  NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
@@ -2580,6 +2929,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2580
2929
  }
2581
2930
  targetOffset = restoredLogicalOffset - next.top;
2582
2931
  targetOffset = MAX(targetOffset, -next.top + _headerOffset);
2932
+ // An inactive list may shrink after its offset observer was detached.
2933
+ // Do not overwrite its corrected offset with a now-unreachable cached one.
2934
+ targetOffset = RNCLimitRestoredScrollOffset(
2935
+ targetOffset,
2936
+ scrollView.contentSize.height,
2937
+ scrollView.bounds.size.height,
2938
+ next.top,
2939
+ next.bottom
2940
+ );
2583
2941
  }
2584
2942
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
2585
2943
  if (restore || previousTopInset != next.top) {
@@ -2588,6 +2946,32 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2588
2946
  if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
2589
2947
  }
2590
2948
 
2949
+ - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
2950
+ {
2951
+ if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
2952
+ [(RNCNativeScrollerComponentView *)scrollView.superview setPagerStickyHeight:height forOwner:self];
2953
+ }
2954
+ }
2955
+
2956
+ - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
2957
+ callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next
2958
+ {
2959
+ UIScrollView *scrollView = scroller.scrollView;
2960
+ NSValue *stored = [_originalInsets objectForKey:scrollView];
2961
+ if (stored == nil) return;
2962
+ UIEdgeInsets original = stored.UIEdgeInsetsValue;
2963
+ original.top += next.top - previous.top;
2964
+ original.left += next.left - previous.left;
2965
+ original.bottom += next.bottom - previous.bottom;
2966
+ original.right += next.right - previous.right;
2967
+ [_originalInsets setObject:[NSValue valueWithUIEdgeInsets:original] forKey:scrollView];
2968
+ NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
2969
+ if (appliedTop != nil) {
2970
+ [_appliedTopInsets setObject:@(appliedTop.doubleValue + next.top - previous.top) forKey:scrollView];
2971
+ }
2972
+ if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
2973
+ }
2974
+
2591
2975
  - (void)reapplyInsetsToObservedScrollView
2592
2976
  {
2593
2977
  [self restoreDetachedScrollInsets];
@@ -2617,7 +3001,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2617
3001
  change:(NSDictionary<NSKeyValueChangeKey,id> *)change
2618
3002
  context:(void *)context
2619
3003
  {
3004
+ if (context == RNCScrollRefreshControlContext) {
3005
+ UIScrollView *scrollView = (UIScrollView *)object;
3006
+ [self updateRefreshControlForScrollView:scrollView];
3007
+ if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
3008
+ return;
3009
+ }
2620
3010
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
3011
+ if ([keyPath isEqualToString:@"contentInset"]) {
3012
+ [self updateHeaderForScrollView:(UIScrollView *)object];
3013
+ return;
3014
+ }
2621
3015
  if ([keyPath isEqualToString:@"contentSize"]) {
2622
3016
  NSValue *oldSize = change[NSKeyValueChangeOldKey];
2623
3017
  NSValue *newSize = change[NSKeyValueChangeNewKey];
@@ -2648,7 +3042,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2648
3042
  BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
2649
3043
  _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
2650
3044
  if (attachedToList) {
2651
- _sharedHeaderHostView.transform = CGAffineTransformIdentity;
3045
+ // UIRefreshControl temporarily adds a top inset while refreshing. The
3046
+ // shared header lives in that scroll view, so exclude only that inset from
3047
+ // its visual position while keeping the list's pull and indicator intact.
3048
+ NSNumber *pagerTopInset = [_appliedTopInsets objectForKey:_sharedHeaderScrollView];
3049
+ CGFloat refreshInset = _sharedHeaderScrollView.refreshControl != nil && pagerTopInset != nil
3050
+ ? MAX(0, _sharedHeaderScrollView.contentInset.top - pagerTopInset.doubleValue)
3051
+ : 0;
3052
+ _sharedHeaderHostView.transform = CGAffineTransformMakeTranslation(0, -refreshInset);
2652
3053
  _headerView.transform = CGAffineTransformIdentity;
2653
3054
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight);
2654
3055
  CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
@@ -2849,6 +3250,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2849
3250
 
2850
3251
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
2851
3252
  {
3253
+ if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3254
+ [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3255
+ }
2852
3256
  if (_directNativePagerEnabled) {
2853
3257
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex - 1];
2854
3258
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex + 1];
@@ -3194,6 +3598,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3194
3598
  (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
3195
3599
  return YES;
3196
3600
  }
3601
+ if (gestureRecognizer == _contentPressCancellationGesture) {
3602
+ [_contentPressCancellationGesture observeIncomingTouch:touch];
3603
+ UIView *touchView = touch.view;
3604
+ return _nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
3605
+ _contentPressCancellationGesture.view != nil &&
3606
+ [touchView isDescendantOfView:_pagerScrollView] &&
3607
+ ![touchView isDescendantOfView:_sharedHeaderHostView];
3608
+ }
3197
3609
  if (gestureRecognizer == _sharedHeaderPressCancellationGesture) {
3198
3610
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
3199
3611
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
@@ -3234,6 +3646,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3234
3646
  ![other isKindOfClass:RCTTouchHandler.class] &&
3235
3647
  ![_sharedHeaderOuterPagerGesture.blockedPagerGestures containsObject:other];
3236
3648
  }
3649
+ if (gestureRecognizer == _contentPressCancellationGesture ||
3650
+ otherGestureRecognizer == _contentPressCancellationGesture) {
3651
+ UIGestureRecognizer *other = gestureRecognizer == _contentPressCancellationGesture
3652
+ ? otherGestureRecognizer : gestureRecognizer;
3653
+ return other != _contentPressCancellationGesture.reactTouchHandler;
3654
+ }
3237
3655
  if (gestureRecognizer == _sharedHeaderPressCancellationGesture ||
3238
3656
  otherGestureRecognizer == _sharedHeaderPressCancellationGesture) {
3239
3657
  UIGestureRecognizer *other = gestureRecognizer == _sharedHeaderPressCancellationGesture
@@ -0,0 +1,17 @@
1
+ #import <React/RCTViewComponentView.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @interface RNCNativeScrollerComponentView : RCTViewComponentView
6
+ @property (nonatomic, readonly) UIScrollView *scrollView;
7
+ - (void)stopScrolling;
8
+ - (void)setPagerStickyHeight:(CGFloat)height forOwner:(id)owner;
9
+ @end
10
+
11
+ // Native inset ownership stays separate from Fabric/Yoga content layout.
12
+ @protocol RNCNativeScrollerInsetCoordinator <NSObject>
13
+ - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
14
+ callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next;
15
+ @end
16
+
17
+ NS_ASSUME_NONNULL_END