@onekeyfe/react-native-pager-view 3.0.133 → 3.0.135

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.
@@ -184,10 +184,12 @@ private class CollapsiblePagerHorizontalItemsView(
184
184
  }
185
185
  }
186
186
 
187
- private fun selectedBackground(selected: Boolean) = GradientDrawable().apply {
187
+ private fun selectedBackground(selected: Boolean, height: Int) = GradientDrawable().apply {
188
188
  shape = GradientDrawable.RECTANGLE
189
189
  setColor(if (selected) selectedBackgroundColor else Color.TRANSPARENT)
190
- cornerRadius = dp(10.0).toFloat()
190
+ // OneKey patch: match the pill shape of the React category filters.
191
+ // cornerRadius = dp(10.0).toFloat()
192
+ cornerRadius = height / 2f
191
193
  }
192
194
 
193
195
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -278,7 +280,7 @@ private class CollapsiblePagerHorizontalItemsView(
278
280
  buttons.forEachIndexed { index, button ->
279
281
  val selected = index == selectedIndex
280
282
  button.setTextColor(if (selected) activeTextColor else inactiveTextColor)
281
- button.background = selectedBackground(selected)
283
+ button.background = selectedBackground(selected, button.height)
282
284
  button.isSelected = selected
283
285
  }
284
286
  }
@@ -0,0 +1,10 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ FOUNDATION_EXPORT void RNCPruneReleasedPageStates(
6
+ NSMutableDictionary<NSString *, id> *releasedStates,
7
+ NSArray<NSString *> *validPageKeys
8
+ );
9
+
10
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,14 @@
1
+ #import "RNCCollapsiblePagerReleasedStatePolicy.h"
2
+
3
+ void RNCPruneReleasedPageStates(
4
+ NSMutableDictionary<NSString *, id> *releasedStates,
5
+ NSArray<NSString *> *validPageKeys
6
+ )
7
+ {
8
+ NSSet<NSString *> *validKeys = [NSSet setWithArray:validPageKeys];
9
+ for (NSString *pageKey in releasedStates.allKeys) {
10
+ if (![validKeys containsObject:pageKey]) {
11
+ [releasedStates removeObjectForKey:pageKey];
12
+ }
13
+ }
14
+ }
@@ -1,4 +1,5 @@
1
1
  #import "RNCCollapsiblePagerViewComponentView.h"
2
+ #import "RNCCollapsiblePagerReleasedStatePolicy.h"
2
3
 
3
4
  #import <react/renderer/components/pagerview/ComponentDescriptors.h>
4
5
  #import <react/renderer/components/pagerview/EventEmitters.h>
@@ -11,12 +12,39 @@
11
12
  #import "React/RCTSurfaceTouchHandler.h"
12
13
  #import "React/RCTTouchHandler.h"
13
14
 
15
+ // OneKey patch: deduplicate structural mutation parents before scanning the page.
16
+ #include <unordered_set>
17
+
14
18
  using namespace facebook::react;
15
19
 
16
20
  static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
17
21
  static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
18
22
  @"rnc-collapsible-pager-native-scroller:";
19
23
 
24
+ static const CFTimeInterval RNCCollapsiblePagerFallbackDebounceInterval = 0.5;
25
+
26
+ // OneKey patch: native scroll views can survive a move or be reused for a new
27
+ // Fabric component. Keep weak identity until their page is attached again.
28
+ @interface RNCCollapsiblePagerReleasedScrollState : NSObject
29
+ @property (nonatomic, weak) UIScrollView *scrollView;
30
+ @property (nonatomic, weak) UIView *componentView;
31
+ @property (nonatomic, assign) NSInteger componentTag;
32
+ @property (nonatomic, weak) UIScrollView *pendingFallbackScrollView;
33
+ @property (nonatomic, assign) CFTimeInterval pendingFallbackDetectedAt;
34
+ @end
35
+
36
+ @implementation RNCCollapsiblePagerReleasedScrollState
37
+ @end
38
+
39
+ static UIView *RNCScrollComponentView(UIScrollView *scrollView)
40
+ {
41
+ for (UIView *view = scrollView; view != nil; view = view.superview) {
42
+ // UIView's Fabric category adopts the protocol even for unregistered views.
43
+ if ([view isKindOfClass:RCTViewComponentView.class] && view.tag > 0) return view;
44
+ }
45
+ return nil;
46
+ }
47
+
20
48
  typedef void (^RNCCollapsiblePagerNativeTabPressHandler)(NSInteger index, NSString *key);
21
49
 
22
50
  static CGFloat RNCClamp(CGFloat value, CGFloat minimum, CGFloat maximum)
@@ -716,7 +744,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
716
744
  NSString *key = [item[@"key"] isKindOfClass:NSString.class] ? item[@"key"] : @"";
717
745
  BOOL selected = [key isEqualToString:self->_selectedKey];
718
746
  button.backgroundColor = selected ? self->_selectedBackgroundColor : UIColor.clearColor;
719
- button.layer.cornerRadius = 10;
747
+ // OneKey patch: match the pill shape of the React category filters.
748
+ // button.layer.cornerRadius = 10;
749
+ button.layer.cornerRadius = CGRectGetHeight(button.bounds) / 2;
720
750
  [button setTitleColor:selected ? self->_activeTextColor : self->_inactiveTextColor
721
751
  forState:UIControlStateNormal];
722
752
  button.accessibilityTraits = selected
@@ -989,6 +1019,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
989
1019
  NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
990
1020
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
991
1021
  NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
1022
+ // OneKey patch: pair pre-mount release with recovery, including deferred mounts.
1023
+ NSMutableDictionary<NSString *, RNCCollapsiblePagerReleasedScrollState *> *_releasedPageScrollStates;
1024
+ BOOL _needsScrollObserverReattachAfterMount;
992
1025
  __weak UIScrollView *_observedScrollView;
993
1026
  __weak UIScrollView *_sharedHeaderScrollView;
994
1027
  __weak UIScrollView *_pendingFallbackScrollView;
@@ -1023,6 +1056,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1023
1056
  _logicalChildren = [NSMutableArray new];
1024
1057
  _pageControllers = [NSMutableArray new];
1025
1058
  _pageOffsets = [NSMutableDictionary new];
1059
+ _releasedPageScrollStates = [NSMutableDictionary new];
1026
1060
  _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
1027
1061
  _appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
1028
1062
  _originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
@@ -1132,6 +1166,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1132
1166
  !_pagerScrollView.dragging && !_pagerScrollView.decelerating) {
1133
1167
  [self finishPagerScrollEmittingSelection:NO];
1134
1168
  }
1169
+ // OneKey patch: a detached pager can return without a new Fabric transaction.
1170
+ // Wait until the current mount has finished before inspecting its new lists.
1171
+ if (self.window != nil &&
1172
+ (_needsScrollObserverReattachAfterMount || _releasedPageScrollStates.count > 0)) {
1173
+ NSUInteger generation = _generation;
1174
+ dispatch_async(dispatch_get_main_queue(), ^{
1175
+ if (self->_generation == generation &&
1176
+ (self->_needsScrollObserverReattachAfterMount || self->_releasedPageScrollStates.count > 0)) {
1177
+ self->_needsScrollObserverReattachAfterMount = YES;
1178
+ [self attachScrollObserverForCurrentPage];
1179
+ }
1180
+ });
1181
+ }
1135
1182
  }
1136
1183
 
1137
1184
  - (void)dealloc
@@ -1375,6 +1422,63 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1375
1422
 
1376
1423
  #pragma mark - React mounting
1377
1424
 
1425
+ // OneKey patch: Fabric can recycle a removed list as a descendant of our header
1426
+ // within the same transaction. Release native ownership before that reuse can
1427
+ // form a cycle; did-mount recovery runs too late to prevent it.
1428
+ - (void)mountingTransactionWillMount:(const MountingTransaction &)transaction
1429
+ withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
1430
+ {
1431
+ if (_isBeingRecycled || (_originalInsets.count == 0 && _releasedPageScrollStates.count == 0)) return;
1432
+ std::unordered_set<NSInteger> removedTags;
1433
+ std::unordered_set<NSInteger> deletedTags;
1434
+ for (const auto &mutation : transaction.getMutations()) {
1435
+ if ((mutation.type == ShadowViewMutation::Remove ||
1436
+ mutation.type == ShadowViewMutation::Delete) && mutation.oldChildShadowView.tag > 0) {
1437
+ removedTags.insert(mutation.oldChildShadowView.tag);
1438
+ if (mutation.type == ShadowViewMutation::Delete) {
1439
+ deletedTags.insert(mutation.oldChildShadowView.tag);
1440
+ }
1441
+ }
1442
+ }
1443
+ // A transition can defer reattachment across several mounting transactions.
1444
+ for (RNCCollapsiblePagerReleasedScrollState *state in _releasedPageScrollStates.allValues) {
1445
+ if (deletedTags.find(state.componentTag) != deletedTags.end()) state.componentView = nil;
1446
+ }
1447
+ if (removedTags.empty()) return;
1448
+
1449
+ // Inspect only the managed scroll views' ancestor chains, including wrappers
1450
+ // above the pager. Row removals and unrelated commits keep ownership intact.
1451
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
1452
+ for (UIView *ancestor = scrollView; ancestor != nil; ancestor = ancestor.superview) {
1453
+ if (removedTags.find(ancestor.tag) == removedTags.end()) continue;
1454
+ BOOL belongsToPage = NO;
1455
+ for (NSInteger index = 0; index < _pageControllers.count; index++) {
1456
+ if (![scrollView isDescendantOfView:_pageControllers[index].view]) continue;
1457
+ belongsToPage = YES;
1458
+ NSString *pageKey = [self pageKeyForIndex:index];
1459
+ UIView *componentView = RNCScrollComponentView(scrollView);
1460
+ RNCCollapsiblePagerReleasedScrollState *state = [RNCCollapsiblePagerReleasedScrollState new];
1461
+ state.scrollView = scrollView;
1462
+ state.componentTag = componentView.tag;
1463
+ // A deleted component can reuse both the native object and its tag.
1464
+ if (deletedTags.find(componentView.tag) == deletedTags.end()) {
1465
+ state.componentView = componentView;
1466
+ }
1467
+ _releasedPageScrollStates[pageKey] = state;
1468
+ // Retained pages may be visually aligned to the expanded header while
1469
+ // their saved offsets still hold the user's independent list position.
1470
+ if (scrollView == _observedScrollView || _pageOffsets[pageKey] == nil) {
1471
+ _pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
1472
+ }
1473
+ break;
1474
+ }
1475
+ _needsScrollObserverReattachAfterMount = YES;
1476
+ [self restoreScrollViewState:scrollView resetSavedOffset:!belongsToPage];
1477
+ break;
1478
+ }
1479
+ }
1480
+ }
1481
+
1378
1482
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
1379
1483
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
1380
1484
  {
@@ -1382,16 +1486,55 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1382
1486
  _isPagerDragging || _isBeingRecycled || self.window == nil ||
1383
1487
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
1384
1488
 
1385
- UIView *pageView = _pageControllers[_currentIndex].view;
1386
- BOOL shouldResolve = _observedScrollView == nil ||
1387
- ![_observedScrollView isDescendantOfView:pageView];
1388
- if (!shouldResolve &&
1389
- [_observedScrollView isKindOfClass:UICollectionView.class]) {
1390
- UIScrollView *preferred = [self findExplicitNativeScrollerInView:pageView];
1391
- if (preferred == nil) preferred = [self findFabricScrollViewInView:pageView];
1392
- shouldResolve = preferred != nil && preferred != _observedScrollView;
1489
+ // OneKey patch: a release must recover even when the changed parent is above
1490
+ // the page subtree. Missing lists keep their identity for a later insertion.
1491
+ if (_needsScrollObserverReattachAfterMount) {
1492
+ [self attachScrollObserverForCurrentPage];
1493
+ return;
1494
+ }
1495
+ UIView *page = _pageControllers[_currentIndex].view;
1496
+ BOOL headerIsAttached = [_sharedHeaderHostView isDescendantOfView:self];
1497
+ BOOL currentListIsAttached = _observedScrollView != nil &&
1498
+ [_observedScrollView isDescendantOfView:page] && headerIsAttached;
1499
+ BOOL currentListIsFallback = currentListIsAttached &&
1500
+ [_observedScrollView isKindOfClass:UICollectionView.class];
1501
+ if (currentListIsAttached && !currentListIsFallback &&
1502
+ _releasedPageScrollStates.count == 0) return;
1503
+ if (!headerIsAttached) {
1504
+ [self attachScrollObserverForCurrentPage];
1505
+ return;
1506
+ }
1507
+ // Property-only updates cannot replace a list. Collect changed parents first
1508
+ // so a burst of unrelated mutations scans the current page at most once.
1509
+ std::unordered_set<NSInteger> changedParentTags;
1510
+ for (const auto &mutation : transaction.getMutations()) {
1511
+ if ((mutation.type == ShadowViewMutation::Insert ||
1512
+ mutation.type == ShadowViewMutation::Remove) && mutation.parentTag > 0) {
1513
+ changedParentTags.insert(mutation.parentTag);
1514
+ }
1515
+ }
1516
+ if (changedParentTags.empty()) return;
1517
+
1518
+ // A missing list can return in a later transaction. Inspect only the current
1519
+ // page needing an observer and pages whose released identities are retained.
1520
+ for (NSInteger index = 0; index < _pageControllers.count; index++) {
1521
+ BOOL hasReleasedList = _releasedPageScrollStates[[self pageKeyForIndex:index]] != nil;
1522
+ BOOL currentPageNeedsResolve = index == _currentIndex &&
1523
+ (!currentListIsAttached || currentListIsFallback);
1524
+ if (!hasReleasedList && !currentPageNeedsResolve) continue;
1525
+ NSMutableArray<UIView *> *pendingViews =
1526
+ [NSMutableArray arrayWithObject:_pageControllers[index].view];
1527
+ while (pendingViews.count > 0) {
1528
+ UIView *view = pendingViews.lastObject;
1529
+ [pendingViews removeLastObject];
1530
+ if (changedParentTags.find(view.tag) != changedParentTags.end()) {
1531
+ if (hasReleasedList) _needsScrollObserverReattachAfterMount = YES;
1532
+ [self attachScrollObserverForCurrentPage];
1533
+ return;
1534
+ }
1535
+ [pendingViews addObjectsFromArray:view.subviews];
1536
+ }
1393
1537
  }
1394
- if (shouldResolve) [self attachScrollObserverForCurrentPage];
1395
1538
  }
1396
1539
 
1397
1540
  - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
@@ -1573,6 +1716,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1573
1716
  {
1574
1717
  _isBeingRecycled = YES;
1575
1718
  _generation++;
1719
+ _needsScrollObserverReattachAfterMount = NO;
1720
+ [_releasedPageScrollStates removeAllObjects];
1576
1721
  [self detachSharedHeaderPressCancellationGesture];
1577
1722
  [self detachScrollObserver];
1578
1723
  [self restoreSharedHeadersToContainer];
@@ -1732,7 +1877,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1732
1877
  NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
1733
1878
  NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
1734
1879
  id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
1735
- _pageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
1880
+ NSArray<NSString *> *nextPageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
1881
+ // Stable keys keep their released identity across reorders. A missing key
1882
+ // represents an unmounted React page and must not be reused by a later page.
1883
+ RNCPruneReleasedPageStates(_releasedPageScrollStates, nextPageKeys);
1884
+ _pageKeys = nextPageKeys;
1736
1885
  }
1737
1886
  if (_needsPropsReapply || oldViewProps.retainedPages != newViewProps.retainedPages) {
1738
1887
  _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
@@ -1943,6 +2092,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1943
2092
 
1944
2093
  - (void)attachScrollObserverForCurrentPage
1945
2094
  {
2095
+ [self attachScrollObserverForCurrentPageAfterTransition:NO];
2096
+ }
2097
+
2098
+ // OneKey patch: selection completion restores list state synchronously, before
2099
+ // the transition flags are cleared on the next run loop. Other callers wait.
2100
+ - (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled
2101
+ {
2102
+ if (_isBeingRecycled) return;
2103
+ if (_needsScrollObserverReattachAfterMount) {
2104
+ if (self.window == nil || ((_transitioning || _isPagerDragging) && !selectionSettled)) return;
2105
+ [self prepareAdjacentPageInsets];
2106
+ }
1946
2107
  [self restoreDetachedScrollInsets];
1947
2108
  if (_scrollResolveRetryPageIndex != _currentIndex) {
1948
2109
  _scrollResolveRetryToken += 1;
@@ -1956,7 +2117,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1956
2117
  if (candidate == nil) {
1957
2118
  [self detachScrollObserver];
1958
2119
  [self restoreSharedHeadersToContainer];
1959
- [self scheduleScrollObserverRetryForCurrentPage];
2120
+ // Stop retrying released lists on unrelated commits without discarding a
2121
+ // surviving identity or offset. A relevant insertion resumes recovery.
2122
+ if (_needsScrollObserverReattachAfterMount) {
2123
+ _needsScrollObserverReattachAfterMount = NO;
2124
+ _scrollResolveRetryToken += 1;
2125
+ _scrollResolveRetryScheduled = NO;
2126
+ _scrollResolveRetryAttempt = 0;
2127
+ _pendingFallbackScrollView = nil;
2128
+ _pendingFallbackScrollViewDetectedAt = 0;
2129
+ } else {
2130
+ [self scheduleScrollObserverRetryForCurrentPage];
2131
+ }
1960
2132
  return;
1961
2133
  }
1962
2134
  if ([candidate isKindOfClass:UICollectionView.class] &&
@@ -1966,7 +2138,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1966
2138
  _pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
1967
2139
  _scrollResolveRetryAttempt = 0;
1968
2140
  }
1969
- if (CACurrentMediaTime() - _pendingFallbackScrollViewDetectedAt < 0.5) {
2141
+ if (CACurrentMediaTime() - _pendingFallbackScrollViewDetectedAt <
2142
+ RNCCollapsiblePagerFallbackDebounceInterval) {
1970
2143
  [self scheduleScrollObserverRetryForCurrentPage];
1971
2144
  return;
1972
2145
  }
@@ -1983,6 +2156,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1983
2156
  [candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
1984
2157
  }
1985
2158
  if (candidate == _observedScrollView) {
2159
+ _needsScrollObserverReattachAfterMount = NO;
1986
2160
  [self attachSharedHeadersToScrollView:candidate];
1987
2161
  [self updateSharedHeaderPressCancellationGesture];
1988
2162
  return;
@@ -2006,6 +2180,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2006
2180
  context:RNCCollapsiblePagerContentOffsetContext];
2007
2181
  [candidate.panGestureRecognizer addTarget:self action:@selector(observedListPanChanged:)];
2008
2182
  _observingContentOffset = YES;
2183
+ _needsScrollObserverReattachAfterMount = NO;
2009
2184
  _currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
2010
2185
  [self attachSharedHeadersToScrollView:candidate];
2011
2186
  [self updateSharedHeaderPressCancellationGesture];
@@ -2235,6 +2410,39 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2235
2410
  }
2236
2411
  }
2237
2412
 
2413
+ // OneKey patch: release observers, headers and saved state while the scroll view
2414
+ // still represents the old list, before Fabric applies its next owner's props.
2415
+ - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
2416
+ {
2417
+ if (scrollView == _observedScrollView) {
2418
+ [self detachScrollObserver];
2419
+ if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2420
+ }
2421
+ if (scrollView == _sharedHeaderScrollView) {
2422
+ [self restoreSharedHeadersToContainer];
2423
+ }
2424
+ CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
2425
+ UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
2426
+ NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
2427
+ if (appliedTop != nil && scrollView.refreshControl != nil) {
2428
+ original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2429
+ }
2430
+ scrollView.contentInset = original;
2431
+ scrollView.verticalScrollIndicatorInsets =
2432
+ [_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
2433
+ scrollView.alwaysBounceVertical =
2434
+ [_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
2435
+ scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
2436
+ [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
2437
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
2438
+ animated:NO];
2439
+ [_originalInsets removeObjectForKey:scrollView];
2440
+ [_appliedTopInsets removeObjectForKey:scrollView];
2441
+ [_originalIndicatorInsets removeObjectForKey:scrollView];
2442
+ [_originalAlwaysBounceVertical removeObjectForKey:scrollView];
2443
+ [_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
2444
+ }
2445
+
2238
2446
  - (void)restoreDetachedScrollInsets
2239
2447
  {
2240
2448
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
@@ -2246,32 +2454,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2246
2454
  }
2247
2455
  }
2248
2456
  if (belongsToPage) continue;
2249
- if (scrollView == _observedScrollView) {
2250
- [self detachScrollObserver];
2251
- _pageOffsets[self.currentPageKey] = @(_headerOffset);
2252
- }
2253
- // Fabric can reuse an empty RN ScrollView in another screen. Release all
2254
- // pager-owned insets before that view is used as a search or dialog list.
2255
- CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
2256
- UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
2257
- NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
2258
- if (appliedTop != nil && scrollView.refreshControl != nil) {
2259
- original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2260
- }
2261
- scrollView.contentInset = original;
2262
- scrollView.verticalScrollIndicatorInsets =
2263
- [_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
2264
- scrollView.alwaysBounceVertical =
2265
- [_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
2266
- scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
2267
- [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
2268
- [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
2269
- animated:NO];
2270
- [_originalInsets removeObjectForKey:scrollView];
2271
- [_appliedTopInsets removeObjectForKey:scrollView];
2272
- [_originalIndicatorInsets removeObjectForKey:scrollView];
2273
- [_originalAlwaysBounceVertical removeObjectForKey:scrollView];
2274
- [_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
2457
+ // OneKey patch: share the full release path with pre-mount recycling.
2458
+ [self restoreScrollViewState:scrollView resetSavedOffset:YES];
2275
2459
  }
2276
2460
  }
2277
2461
 
@@ -2279,6 +2463,57 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2279
2463
  pageIndex:(NSInteger)pageIndex
2280
2464
  restore:(BOOL)restore
2281
2465
  {
2466
+ // OneKey patch: a surviving list keeps its full offset. A replacement keeps
2467
+ // only header collapse, even if Fabric reused the same native scroll object.
2468
+ NSString *pageKey = [self pageKeyForIndex:pageIndex];
2469
+ RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
2470
+ BOOL deferredReleasedIdentity = NO;
2471
+ if (released != nil) {
2472
+ UIView *componentView = RNCScrollComponentView(scrollView);
2473
+ BOOL sameList = released.scrollView == scrollView && componentView != nil &&
2474
+ released.componentView == componentView && released.componentTag == componentView.tag;
2475
+ // Adjacent Nitro lists are visible only as generic UICollectionViews, so
2476
+ // debounce each replacement candidate independently before rejecting the
2477
+ // released identity. A surviving native list is safe to accept immediately.
2478
+ BOOL needsFallbackDebounce = !sameList &&
2479
+ [scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView;
2480
+ if (needsFallbackDebounce) {
2481
+ CFTimeInterval now = CACurrentMediaTime();
2482
+ if (released.pendingFallbackScrollView != scrollView) {
2483
+ released.pendingFallbackScrollView = scrollView;
2484
+ released.pendingFallbackDetectedAt = now;
2485
+ __weak __typeof__(self) weakSelf = self;
2486
+ __weak RNCCollapsiblePagerReleasedScrollState *weakReleased = released;
2487
+ __weak UIScrollView *weakScrollView = scrollView;
2488
+ NSString *capturedPageKey = [pageKey copy];
2489
+ NSUInteger capturedGeneration = _generation;
2490
+ dispatch_after(
2491
+ dispatch_time(
2492
+ DISPATCH_TIME_NOW,
2493
+ (int64_t)(RNCCollapsiblePagerFallbackDebounceInterval * NSEC_PER_SEC)
2494
+ ),
2495
+ dispatch_get_main_queue(),
2496
+ ^{
2497
+ RNCCollapsiblePagerViewComponentView *strongSelf = weakSelf;
2498
+ RNCCollapsiblePagerReleasedScrollState *strongReleased = weakReleased;
2499
+ UIScrollView *strongScrollView = weakScrollView;
2500
+ if (strongSelf == nil || strongReleased == nil || strongScrollView == nil ||
2501
+ strongSelf->_isBeingRecycled || strongSelf.window == nil ||
2502
+ strongSelf->_generation != capturedGeneration ||
2503
+ strongSelf->_releasedPageScrollStates[capturedPageKey] != strongReleased ||
2504
+ strongReleased.pendingFallbackScrollView != strongScrollView) return;
2505
+ [strongSelf prepareAdjacentPageInsets];
2506
+ }
2507
+ );
2508
+ }
2509
+ deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
2510
+ RNCCollapsiblePagerFallbackDebounceInterval;
2511
+ }
2512
+ if (!deferredReleasedIdentity) {
2513
+ if (!sameList) _pageOffsets[pageKey] = @(_headerOffset);
2514
+ [_releasedPageScrollStates removeObjectForKey:pageKey];
2515
+ }
2516
+ }
2282
2517
  NSValue *storedOriginal = [_originalInsets objectForKey:scrollView];
2283
2518
  UIEdgeInsets original = storedOriginal == nil
2284
2519
  ? scrollView.contentInset
@@ -2327,7 +2562,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2327
2562
  NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
2328
2563
  // The sticky slot can change height between pages. Cache offsets relative
2329
2564
  // to content start so a round trip does not add that height difference.
2330
- CGFloat restoredLogicalOffset = saved == nil ? _headerOffset : saved.doubleValue;
2565
+ CGFloat restoredLogicalOffset = deferredReleasedIdentity || saved == nil
2566
+ ? _headerOffset
2567
+ : saved.doubleValue;
2331
2568
  // While the shared header is visible, retained pages must agree on its
2332
2569
  // collapse position. Deep offsets remain independent after full collapse.
2333
2570
  if (_nativeSmoothHeaderScrollEnabled && _headerOffset < _headerHeight) {
@@ -2483,7 +2720,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2483
2720
  self->_currentIndex = index;
2484
2721
  self->_destinationIndex = index;
2485
2722
  [self->_nativeTabBarView setProgress:index];
2486
- [self attachScrollObserverForCurrentPage];
2723
+ [self attachScrollObserverForCurrentPageAfterTransition:YES];
2487
2724
  [self emitPageSelected:index];
2488
2725
  [self emitDiagnostics:@"page-selected"];
2489
2726
  } else if (self->_pendingGoToIndex < 0) {
@@ -2520,7 +2757,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2520
2757
  self->_currentIndex = index;
2521
2758
  self->_destinationIndex = index;
2522
2759
  [self->_nativeTabBarView setProgress:index];
2523
- [self attachScrollObserverForCurrentPage];
2760
+ [self attachScrollObserverForCurrentPageAfterTransition:YES];
2524
2761
  [self emitPageSelected:index];
2525
2762
  [self emitDiagnostics:@"page-selected"];
2526
2763
  } else {
@@ -2660,7 +2897,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2660
2897
  0
2661
2898
  ) animated:NO];
2662
2899
  [_nativeTabBarView setProgress:index];
2663
- [self attachScrollObserverForCurrentPage];
2900
+ [self attachScrollObserverForCurrentPageAfterTransition:YES];
2664
2901
  if (emitSelection &&
2665
2902
  (_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
2666
2903
  [self emitPageSelected:index];
@@ -2711,7 +2948,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2711
2948
  _currentIndex = index;
2712
2949
  _destinationIndex = index;
2713
2950
  [_nativeTabBarView setProgress:index];
2714
- [self attachScrollObserverForCurrentPage];
2951
+ [self attachScrollObserverForCurrentPageAfterTransition:YES];
2715
2952
  // A newer tab tap remains authoritative while its queued command runs.
2716
2953
  if (emitSelection &&
2717
2954
  (_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
@@ -0,0 +1,36 @@
1
+ #import <XCTest/XCTest.h>
2
+
3
+ #import "RNCCollapsiblePagerReleasedStatePolicy.h"
4
+
5
+ @interface RNCCollapsiblePagerReleasedStatePolicyTests : XCTestCase
6
+ @end
7
+
8
+ @implementation RNCCollapsiblePagerReleasedStatePolicyTests
9
+
10
+ - (void)testPrunesRemovedKeysAndKeepsStableKeysAcrossReorders
11
+ {
12
+ NSObject *alpha = [NSObject new];
13
+ NSObject *beta = [NSObject new];
14
+ NSMutableDictionary<NSString *, id> *releasedStates = [@{
15
+ @"alpha": alpha,
16
+ @"beta": beta,
17
+ } mutableCopy];
18
+
19
+ RNCPruneReleasedPageStates(releasedStates, @[@"beta", @"gamma"]);
20
+
21
+ XCTAssertEqualObjects(releasedStates, (@{ @"beta": beta }));
22
+ }
23
+
24
+ - (void)testClearsEveryReleasedStateWhenAllPagesUnmount
25
+ {
26
+ NSMutableDictionary<NSString *, id> *releasedStates = [@{
27
+ @"page-0": [NSObject new],
28
+ @"page-1": [NSObject new],
29
+ } mutableCopy];
30
+
31
+ RNCPruneReleasedPageStates(releasedStates, @[]);
32
+
33
+ XCTAssertEqual(releasedStates.count, 0);
34
+ }
35
+
36
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.133",
3
+ "version": "3.0.135",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -66,7 +66,7 @@
66
66
  "typescript": "^5.9.2"
67
67
  },
68
68
  "peerDependencies": {
69
- "@onekeyfe/react-native-native-logger": "3.0.133",
69
+ "@onekeyfe/react-native-native-logger": "3.0.135",
70
70
  "react": "*",
71
71
  "react-native": "*"
72
72
  },
@@ -14,6 +14,11 @@ Pod::Spec.new do |s|
14
14
  s.source = { :git => "https://github.com/nicholasxuu/app-modules.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm}"
17
+ s.exclude_files = "ios/tests/**/*"
18
+
19
+ s.test_spec "Tests" do |test_spec|
20
+ test_spec.source_files = "ios/tests/**/*.{m,mm}"
21
+ end
17
22
 
18
23
  install_modules_dependencies(s)
19
24