@onekeyfe/react-native-pager-view 6.7.0-alpha.428 → 6.7.0-alpha.432

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.
@@ -185,6 +185,25 @@ struct RNCScrollCandidateResolution {
185
185
  @implementation RNCCollapsiblePagerReleasedScrollState
186
186
  @end
187
187
 
188
+ @interface RNCCollapsiblePagerTransitionEdgeOwner : NSObject
189
+ @property (nonatomic, weak) UIScrollView *scrollView;
190
+ @property (nonatomic, weak) UIView *page;
191
+ @property (nonatomic, strong) UIScrollEdgeEffectStyle *originalStyle;
192
+ @property (nonatomic, strong) UIScrollEdgeElementContainerInteraction *topInteraction;
193
+ @property (nonatomic, strong) UIScrollEdgeElementContainerInteraction *stickyInteraction;
194
+ @property (nonatomic, weak) UIView *topHost;
195
+ @property (nonatomic, weak) UIView *stickyHost;
196
+ @property (nonatomic, strong) UILabel *topMarker;
197
+ @property (nonatomic, strong) UILabel *stickyMarker;
198
+ @property (nonatomic, assign) BOOL borrowsPinnedStickyHost;
199
+ @property (nonatomic, assign) BOOL ownsTransitionHosts;
200
+ @property (nonatomic, strong) UIView *ownedTopHost;
201
+ @property (nonatomic, strong) UIView *ownedStickyHost;
202
+ @end
203
+
204
+ @implementation RNCCollapsiblePagerTransitionEdgeOwner
205
+ @end
206
+
188
207
  static UIView *RNCScrollComponentView(UIScrollView *scrollView)
189
208
  {
190
209
  for (UIView *view = scrollView; view != nil; view = view.superview) {
@@ -1526,6 +1545,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1526
1545
  RNCPagerViewNestedOverlayObserver
1527
1546
  >
1528
1547
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
1548
+ - (void)cancelPendingNativePagerScroll;
1529
1549
  - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
1530
1550
  transition:(NSUInteger)transition;
1531
1551
  - (void)setDirectNativePagerEnabled:(BOOL)enabled;
@@ -1556,6 +1576,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1556
1576
  - (void)syncScrollEdgeAppearance;
1557
1577
  - (void)updateHardEdgeSeparatorCovers;
1558
1578
  - (void)updateTopOverlayThemeBackground;
1579
+ - (BOOL)canPreserveHorizontalScrollEdgesAllowExternalOverlay:(BOOL)allowExternalOverlay;
1580
+ - (void)beginProgrammaticScrollEdgesToIndex:(NSInteger)index allowExternalOverlay:(BOOL)allowExternalOverlay;
1581
+ - (void)adoptCurrentScrollEdgeForProgrammaticTransition:(BOOL)allowExternalOverlay;
1582
+ - (RNCCollapsiblePagerTransitionEdgeOwner *)borrowedPinnedStickyOwner;
1583
+ - (void)releaseProgrammaticScrollEdge:(UIScrollView *)scrollView;
1584
+ - (void)releaseProgrammaticScrollEdgeOwner:(RNCCollapsiblePagerTransitionEdgeOwner *)owner;
1585
+ - (void)clearProgrammaticScrollEdges;
1586
+ - (void)finishProgrammaticScrollEdges;
1587
+ - (BOOL)hasProgrammaticScrollEdgeForPage:(UIView *)page;
1559
1588
  - (void)updateScrollEdgeContentMasks;
1560
1589
  - (void)restoreScrollEdgeContentMasks;
1561
1590
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
@@ -1639,6 +1668,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1639
1668
  BOOL _hasPendingDirectNativePagerChange;
1640
1669
  BOOL _hasReceivedPageCommand;
1641
1670
  NSUInteger _transitionId;
1671
+ void (^_pendingNativePagerScrollCompletion)(BOOL);
1672
+ NSUInteger _pendingNativePagerScrollGeneration;
1673
+ NSUInteger _pendingNativePagerScrollTransition;
1674
+ CGFloat _pendingNativePagerScrollTarget;
1675
+ NSIndexSet *_programmaticPageMaskIndexes;
1676
+ NSMapTable<UIScrollView *, RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdges;
1677
+ NSMutableArray<RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdgeOwners;
1642
1678
  // OneKey patch: Retain the latest tab tap while an animation is in flight.
1643
1679
  NSInteger _pendingGoToIndex;
1644
1680
  BOOL _pendingGoToAnimated;
@@ -1875,11 +1911,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1875
1911
  if (hitView != nil && !sharedHeaderPointIsVisible &&
1876
1912
  (hitView == _sharedHeaderHostView ||
1877
1913
  [hitView isDescendantOfView:_sharedHeaderHostView])) return nil;
1878
- if (hitView != nil && _ownedPageMasks.count > 0 &&
1914
+ if (hitView != nil && (_ownedPageMasks.count > 0 || _programmaticScrollEdges.count > 0) &&
1879
1915
  [_containerView convertPoint:point fromView:self].y < _pageMaskBoundary) {
1880
1916
  for (UIView *page in _ownedPageMasks.keyEnumerator.allObjects) {
1881
1917
  if ([hitView isDescendantOfView:page]) return nil;
1882
1918
  }
1919
+ for (RNCCollapsiblePagerTransitionEdgeOwner *owner in _programmaticScrollEdges.objectEnumerator) {
1920
+ if (owner.page != nil && [hitView isDescendantOfView:owner.page]) return nil;
1921
+ }
1883
1922
  }
1884
1923
  return hitView;
1885
1924
  }
@@ -1896,6 +1935,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1896
1935
  - (void)didMoveToWindow
1897
1936
  {
1898
1937
  [super didMoveToWindow];
1938
+ if (self.window == nil && _pendingNativePagerScrollCompletion != nil) {
1939
+ [self cancelPendingNativePagerScroll];
1940
+ _pagerScrollView.scrollEnabled = _scrollEnabled;
1941
+ [self finishPagerScrollEmittingSelection:NO];
1942
+ }
1899
1943
  [self registerExternalOuterPager];
1900
1944
  BOOL externalHeightChanged = [self resolveExternalTopOverlay];
1901
1945
  if (self.window == nil) [self restoreScrollEdgeContentMasks];
@@ -1956,6 +2000,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1956
2000
  }
1957
2001
  }
1958
2002
  if (_externalOuterPager == outer) return;
2003
+ if (_topOverlayView == nil && _externalTopOverlayTag > 0) {
2004
+ [self clearProgrammaticScrollEdges];
2005
+ }
1959
2006
  [_externalOuterPager removeNestedOverlayObserver:self];
1960
2007
  _externalOuterPager = outer;
1961
2008
  _externalOuterPageActive = NO;
@@ -1987,6 +2034,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1987
2034
  }
1988
2035
  BOOL changed = _externalTopOverlayView != nextView ||
1989
2036
  fabs(_externalTopOverlayHeight - nextHeight) > 0.5;
2037
+ if (changed && _topOverlayView == nil && _externalTopOverlayTag > 0) {
2038
+ [self clearProgrammaticScrollEdges];
2039
+ }
1990
2040
  _externalTopOverlayView = nextView;
1991
2041
  _externalTopOverlayHeight = nextHeight;
1992
2042
  _topOverlayHostView.hidden = !self.hasFixedTopOverlay;
@@ -1998,6 +2048,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1998
2048
  {
1999
2049
  if (_isBeingRecycled || _externalOuterPager == nil || _externalTopOverlayTag <= 0) return;
2000
2050
  if (_externalOuterPageActive == active && _externalOuterTransitioning == transitioning) return;
2051
+ if ((transitioning || !active) && _topOverlayView == nil) {
2052
+ [self clearProgrammaticScrollEdges];
2053
+ }
2001
2054
  BOOL wasTransitioning = _externalOuterTransitioning;
2002
2055
  _externalOuterPageActive = active;
2003
2056
  _externalOuterTransitioning = transitioning;
@@ -2045,6 +2098,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2045
2098
 
2046
2099
  - (void)restoreScrollEdgeContentMasks
2047
2100
  {
2101
+ [self clearProgrammaticScrollEdges];
2102
+ _programmaticPageMaskIndexes = nil;
2048
2103
  if (_sharedHeaderHostView.maskView == _sharedHeaderMaskView) {
2049
2104
  _sharedHeaderHostView.maskView = _originalSharedHeaderMaskView;
2050
2105
  }
@@ -2109,11 +2164,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2109
2164
  _pageMaskBoundary = boundary;
2110
2165
  NSMutableSet<UIView *> *visiblePages = [NSMutableSet new];
2111
2166
  if (transitioning) {
2112
- for (UIViewController *controller in _pageControllers) {
2113
- UIView *page = controller.view;
2167
+ for (NSUInteger index = 0; index < _pageControllers.count; index++) {
2168
+ UIView *page = _pageControllers[index].view;
2169
+ BOOL programmaticParticipant = [_programmaticPageMaskIndexes containsIndex:index];
2170
+ if ([self hasProgrammaticScrollEdgeForPage:page]) continue;
2114
2171
  if (page.window != self.window ||
2115
- !CGRectIntersectsRect([page convertRect:page.bounds toView:_containerView],
2116
- _containerView.bounds)) continue;
2172
+ (!programmaticParticipant &&
2173
+ !CGRectIntersectsRect([page convertRect:page.bounds toView:_containerView],
2174
+ _containerView.bounds))) continue;
2117
2175
  [visiblePages addObject:page];
2118
2176
  UIView *mask = [_ownedPageMasks objectForKey:page];
2119
2177
  if (mask == nil) {
@@ -2125,8 +2183,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2125
2183
  }
2126
2184
  CGFloat localTop = [_containerView convertPoint:CGPointMake(0, boundary)
2127
2185
  toView:page].y;
2128
- mask.frame = CGRectMake(0, localTop, CGRectGetWidth(page.bounds),
2186
+ CGRect maskFrame = CGRectMake(0, localTop, CGRectGetWidth(page.bounds),
2129
2187
  MAX(0, CGRectGetHeight(page.bounds) - localTop));
2188
+ if (programmaticParticipant) {
2189
+ [UIView performWithoutAnimation:^{ mask.frame = maskFrame; }];
2190
+ } else {
2191
+ mask.frame = maskFrame;
2192
+ }
2130
2193
  }
2131
2194
  }
2132
2195
  for (UIView *page in _ownedPageMasks.keyEnumerator.allObjects) {
@@ -2253,10 +2316,227 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2253
2316
  emitter->onScrollEdgeCapability({.available = (bool)available});
2254
2317
  }
2255
2318
 
2319
+ - (RNCCollapsiblePagerTransitionEdgeOwner *)borrowedPinnedStickyOwner
2320
+ {
2321
+ for (RNCCollapsiblePagerTransitionEdgeOwner *owner in _programmaticScrollEdgeOwners) {
2322
+ if (owner.borrowsPinnedStickyHost) return owner;
2323
+ }
2324
+ return nil;
2325
+ }
2326
+
2327
+ - (BOOL)canPreserveHorizontalScrollEdgesAllowExternalOverlay:(BOOL)allowExternalOverlay
2328
+ {
2329
+ if (@available(iOS 27.0, *)) {
2330
+ if (!_directNativePagerEnabled || !_topScrollEdgeEnabled ||
2331
+ !_nativeSmoothHeaderScrollEnabled ||
2332
+ _topScrollEdgeEffect != RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard ||
2333
+ self.window == nil || _isBeingRecycled) return NO;
2334
+ if (_topOverlayView != nil && _externalTopOverlayTag <= 0) return YES;
2335
+ return allowExternalOverlay && _topOverlayView == nil && _externalTopOverlayTag > 0 &&
2336
+ _externalTopOverlayView.window == self.window && _externalTopOverlayHeight > 0 &&
2337
+ _topOverlayHostView.window == self.window && !_topOverlayHostView.hidden &&
2338
+ (_externalOuterPager == nil || (_externalOuterPageActive && !_externalOuterTransitioning));
2339
+ }
2340
+ return NO;
2341
+ }
2342
+
2343
+ - (void)adoptCurrentScrollEdgeForProgrammaticTransition:(BOOL)allowExternalOverlay
2344
+ {
2345
+ if (@available(iOS 27.0, *)) {
2346
+ UIScrollView *source = _observedScrollView;
2347
+ if (![self canPreserveHorizontalScrollEdgesAllowExternalOverlay:allowExternalOverlay] ||
2348
+ source == nil || source != _topEdgeScrollView ||
2349
+ _topEdgeInteraction == nil || _topEdgeInteraction.scrollView != source ||
2350
+ _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
2351
+
2352
+ RNCCollapsiblePagerTransitionEdgeOwner *existing =
2353
+ [_programmaticScrollEdges objectForKey:source];
2354
+ if (existing != nil && existing.topInteraction.scrollView == source) return;
2355
+ if (existing != nil) [self releaseProgrammaticScrollEdgeOwner:existing];
2356
+ if (_programmaticScrollEdges == nil) {
2357
+ _programmaticScrollEdges = [NSMapTable weakToStrongObjectsMapTable];
2358
+ _programmaticScrollEdgeOwners = [NSMutableArray new];
2359
+ }
2360
+ RNCCollapsiblePagerTransitionEdgeOwner *owner = [RNCCollapsiblePagerTransitionEdgeOwner new];
2361
+ owner.scrollView = source;
2362
+ owner.page = _pageControllers[_currentIndex].view;
2363
+ owner.originalStyle = _originalTopEdgeStyle;
2364
+ owner.topInteraction = _topEdgeInteraction;
2365
+ owner.topHost = _topEdgeInteractionHost;
2366
+ owner.topMarker = _topEdgeMarker;
2367
+ owner.stickyInteraction = _stickyEdgeInteraction;
2368
+ owner.stickyHost = _stickyEdgeInteractionHost;
2369
+ owner.stickyMarker = _stickyEdgeMarker;
2370
+ owner.borrowsPinnedStickyHost = _stickyHeaderIsPinned &&
2371
+ owner.stickyHost == _pinnedStickyHostView &&
2372
+ owner.stickyInteraction.scrollView == source;
2373
+ [_programmaticScrollEdges setObject:owner forKey:source];
2374
+ [_programmaticScrollEdgeOwners addObject:owner];
2375
+
2376
+ // Transfer ownership without detaching UIKit's existing edge resources.
2377
+ _topEdgeInteraction = nil;
2378
+ _stickyEdgeInteraction = nil;
2379
+ _topEdgeMarker = nil;
2380
+ _stickyEdgeMarker = nil;
2381
+ _topEdgeInteractionHost = nil;
2382
+ _stickyEdgeInteractionHost = nil;
2383
+ _topEdgeScrollView = nil;
2384
+ _originalTopEdgeStyle = nil;
2385
+ }
2386
+ }
2387
+
2388
+ - (void)beginProgrammaticScrollEdgesToIndex:(NSInteger)index allowExternalOverlay:(BOOL)allowExternalOverlay
2389
+ {
2390
+ if (@available(iOS 27.0, *)) {
2391
+ if (![self canPreserveHorizontalScrollEdgesAllowExternalOverlay:allowExternalOverlay]) return;
2392
+ if (_programmaticScrollEdges == nil) {
2393
+ _programmaticScrollEdges = [NSMapTable weakToStrongObjectsMapTable];
2394
+ _programmaticScrollEdgeOwners = [NSMutableArray new];
2395
+ }
2396
+ NSInteger first = MIN(_currentIndex, index);
2397
+ NSInteger last = MAX(_currentIndex, index);
2398
+ for (NSInteger pageIndex = first; pageIndex <= last; pageIndex++) {
2399
+ [self preparePageForHorizontalTransitionAtIndex:pageIndex];
2400
+ UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:pageIndex];
2401
+ if (scrollView == nil || scrollView.window != self.window) continue;
2402
+ if ([_programmaticScrollEdges objectForKey:scrollView] != nil) continue;
2403
+ RNCCollapsiblePagerTransitionEdgeOwner *owner = [RNCCollapsiblePagerTransitionEdgeOwner new];
2404
+ owner.scrollView = scrollView;
2405
+ owner.page = _pageControllers[pageIndex].view;
2406
+ owner.originalStyle = scrollView.topEdgeEffect.style;
2407
+ scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
2408
+ UILabel *(^markerForHost)(UIView *) = ^UILabel *(UIView *host) {
2409
+ UILabel *marker = [UILabel new];
2410
+ marker.frame = host.bounds;
2411
+ marker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
2412
+ marker.text = @" ";
2413
+ marker.textColor = UIColor.clearColor;
2414
+ marker.backgroundColor = UIColor.clearColor;
2415
+ marker.userInteractionEnabled = NO;
2416
+ marker.isAccessibilityElement = NO;
2417
+ marker.accessibilityElementsHidden = YES;
2418
+ [host addSubview:marker];
2419
+ return marker;
2420
+ };
2421
+ owner.ownsTransitionHosts = allowExternalOverlay && _topOverlayView == nil &&
2422
+ _externalTopOverlayTag > 0 && pageIndex != _currentIndex;
2423
+ UIView *(^transitionHost)(UIView *) = ^UIView *(UIView *referenceHost) {
2424
+ CGRect frame = [referenceHost convertRect:referenceHost.bounds toView:owner.page];
2425
+ CGRect viewport = [scrollView convertRect:scrollView.bounds toView:owner.page];
2426
+ frame.origin.x = CGRectGetMinX(viewport);
2427
+ UIView *host = [[UIView alloc] initWithFrame:frame];
2428
+ host.autoresizingMask = UIViewAutoresizingFlexibleWidth;
2429
+ host.userInteractionEnabled = NO;
2430
+ host.accessibilityElementsHidden = YES;
2431
+ [owner.page addSubview:host];
2432
+ return host;
2433
+ };
2434
+ if (owner.ownsTransitionHosts) {
2435
+ owner.ownedTopHost = transitionHost(_topOverlayHostView);
2436
+ owner.topHost = owner.ownedTopHost;
2437
+ } else {
2438
+ owner.topHost = _topOverlayHostView;
2439
+ }
2440
+ owner.topMarker = markerForHost(owner.topHost);
2441
+ owner.topInteraction = [UIScrollEdgeElementContainerInteraction new];
2442
+ owner.topInteraction.edge = UIRectEdgeTop;
2443
+ owner.topInteraction.scrollView = scrollView;
2444
+ [owner.topHost addInteraction:owner.topInteraction];
2445
+ if (_stickyHeaderView != nil && _headerOffset >= _headerHeight - 0.5) {
2446
+ if (owner.ownsTransitionHosts) {
2447
+ owner.ownedStickyHost = transitionHost(_stickyHeaderView);
2448
+ owner.stickyHost = owner.ownedStickyHost;
2449
+ } else {
2450
+ owner.stickyHost = _stickyHeaderView;
2451
+ }
2452
+ owner.stickyMarker = markerForHost(owner.stickyHost);
2453
+ owner.stickyInteraction = [UIScrollEdgeElementContainerInteraction new];
2454
+ owner.stickyInteraction.edge = UIRectEdgeTop;
2455
+ owner.stickyInteraction.scrollView = scrollView;
2456
+ [owner.stickyHost addInteraction:owner.stickyInteraction];
2457
+ }
2458
+ [_programmaticScrollEdges setObject:owner forKey:scrollView];
2459
+ [_programmaticScrollEdgeOwners addObject:owner];
2460
+ }
2461
+ [self updateScrollEdgeContentMasks];
2462
+ }
2463
+ }
2464
+
2465
+ - (void)releaseProgrammaticScrollEdge:(UIScrollView *)scrollView
2466
+ {
2467
+ if (scrollView == nil) return;
2468
+ [self releaseProgrammaticScrollEdgeOwner:[_programmaticScrollEdges objectForKey:scrollView]];
2469
+ }
2470
+
2471
+ - (void)releaseProgrammaticScrollEdgeOwner:(RNCCollapsiblePagerTransitionEdgeOwner *)owner
2472
+ {
2473
+ if (@available(iOS 26.0, *)) {
2474
+ if (owner == nil) return;
2475
+ UIScrollView *scrollView = owner.scrollView;
2476
+ [owner.topHost removeInteraction:owner.topInteraction];
2477
+ owner.topInteraction.scrollView = nil;
2478
+ [owner.stickyHost removeInteraction:owner.stickyInteraction];
2479
+ owner.stickyInteraction.scrollView = nil;
2480
+ [owner.topMarker removeFromSuperview];
2481
+ [owner.stickyMarker removeFromSuperview];
2482
+ if (owner.ownsTransitionHosts) {
2483
+ [owner.ownedTopHost removeFromSuperview];
2484
+ [owner.ownedStickyHost removeFromSuperview];
2485
+ owner.ownedTopHost = nil;
2486
+ owner.ownedStickyHost = nil;
2487
+ }
2488
+ scrollView.topEdgeEffect.style = owner.originalStyle;
2489
+ if (owner.borrowsPinnedStickyHost && !_stickyHeaderIsPinned &&
2490
+ (_pinnedStickyHostView.superview == scrollView ||
2491
+ _pinnedStickyHostView.superview == nil)) {
2492
+ _pinnedStickyHostView.hidden = YES;
2493
+ _pinnedStickyHostView.layer.zPosition = 0;
2494
+ [_containerView addSubview:_pinnedStickyHostView];
2495
+ }
2496
+ if (scrollView != nil) [_programmaticScrollEdges removeObjectForKey:scrollView];
2497
+ [_programmaticScrollEdgeOwners removeObjectIdenticalTo:owner];
2498
+ }
2499
+ }
2500
+
2501
+ - (void)clearProgrammaticScrollEdges
2502
+ {
2503
+ for (RNCCollapsiblePagerTransitionEdgeOwner *owner in _programmaticScrollEdgeOwners.copy) {
2504
+ [self releaseProgrammaticScrollEdgeOwner:owner];
2505
+ }
2506
+ _programmaticScrollEdges = nil;
2507
+ _programmaticScrollEdgeOwners = nil;
2508
+ }
2509
+
2510
+ - (void)finishProgrammaticScrollEdges
2511
+ {
2512
+ if (_programmaticScrollEdgeOwners.count == 0) return;
2513
+ [self clearProgrammaticScrollEdges];
2514
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2515
+ }
2516
+
2517
+ - (BOOL)hasProgrammaticScrollEdgeForPage:(UIView *)page
2518
+ {
2519
+ if (@available(iOS 26.0, *)) {
2520
+ for (RNCCollapsiblePagerTransitionEdgeOwner *owner in _programmaticScrollEdges.objectEnumerator) {
2521
+ if (owner.page == page && owner.scrollView.window == self.window &&
2522
+ owner.topInteraction.scrollView == owner.scrollView) return YES;
2523
+ }
2524
+ }
2525
+ return NO;
2526
+ }
2527
+
2256
2528
  - (void)updateTopScrollEdgeForScrollView:(UIScrollView *)scrollView
2257
2529
  {
2258
2530
  [self syncScrollEdgeAppearance];
2259
2531
  if (@available(iOS 26.0, *)) {
2532
+ if (_programmaticScrollEdgeOwners.count > 0 &&
2533
+ ![self canPreserveHorizontalScrollEdgesAllowExternalOverlay:YES]) {
2534
+ [self clearProgrammaticScrollEdges];
2535
+ }
2536
+ if (scrollView != nil && [_programmaticScrollEdges objectForKey:scrollView] != nil) {
2537
+ [self updateScrollEdgeContentMasks];
2538
+ return;
2539
+ }
2260
2540
  if (_pendingTopOverlayAppearanceCommit) scrollView = nil;
2261
2541
  BOOL externalOwnerReady = _externalTopOverlayTag <= 0 ||
2262
2542
  _externalOuterPager == nil ||
@@ -2400,6 +2680,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2400
2680
 
2401
2681
  - (void)dealloc
2402
2682
  {
2683
+ [self clearProgrammaticScrollEdges];
2403
2684
  [self updateTopScrollEdgeForScrollView:nil];
2404
2685
  [self cancelHeaderSettledCheck];
2405
2686
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
@@ -2453,6 +2734,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2453
2734
  }
2454
2735
  _hasPendingDirectNativePagerChange = NO;
2455
2736
  if (_directNativePagerEnabled == enabled) return;
2737
+ [self cancelPendingNativePagerScroll];
2456
2738
 
2457
2739
  [self restoreScrollEdgeContentMasks];
2458
2740
  [self detachScrollObserver];
@@ -2533,6 +2815,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2533
2815
  - (void)restorePinnedStickyHeader
2534
2816
  {
2535
2817
  if (!_stickyHeaderIsPinned) return;
2818
+ BOOL borrowedHost = [self borrowedPinnedStickyOwner] != nil;
2536
2819
  _stickyHeaderIsPinned = NO;
2537
2820
  _stickyHeaderView.transform = CGAffineTransformIdentity;
2538
2821
  _nativeTabBarView.transform = CGAffineTransformIdentity;
@@ -2543,9 +2826,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2543
2826
  if (_stickyHeaderView != nil) [_sharedHeaderHostView addSubview:_stickyHeaderView];
2544
2827
  [_sharedHeaderHostView addSubview:_nativeTabBarView];
2545
2828
  [_sharedHeaderHostView addSubview:_nativeSubHeaderView];
2546
- _pinnedStickyHostView.hidden = YES;
2547
- _pinnedStickyHostView.layer.zPosition = 0;
2548
- [_containerView addSubview:_pinnedStickyHostView];
2829
+ if (!borrowedHost) {
2830
+ _pinnedStickyHostView.hidden = YES;
2831
+ _pinnedStickyHostView.layer.zPosition = 0;
2832
+ [_containerView addSubview:_pinnedStickyHostView];
2833
+ }
2549
2834
  if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
2550
2835
  ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView = _sharedHeaderHostView;
2551
2836
  }
@@ -2647,8 +2932,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2647
2932
  );
2648
2933
  CGFloat stickyY = _headerHeight;
2649
2934
  _headerView.frame = CGRectMake(0, 0, width, _headerHeight);
2650
- CGPoint pinnedOrigin = _stickyHeaderIsPinned
2651
- ? [_sharedHeaderScrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight) fromView:_containerView]
2935
+ RNCCollapsiblePagerTransitionEdgeOwner *borrowedOwner = [self borrowedPinnedStickyOwner];
2936
+ UIScrollView *pinnedScrollView = borrowedOwner.scrollView ?: _sharedHeaderScrollView;
2937
+ CGPoint pinnedOrigin = (borrowedOwner != nil || _stickyHeaderIsPinned) && pinnedScrollView != nil
2938
+ ? [pinnedScrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight)
2939
+ fromView:_containerView]
2652
2940
  : CGPointMake(0, self.effectiveTopOverlayHeight);
2653
2941
  _pinnedStickyHostView.frame = CGRectMake(
2654
2942
  pinnedOrigin.x, pinnedOrigin.y, width, _stickyHeaderHeight);
@@ -3083,6 +3371,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3083
3371
  }
3084
3372
  _needsSlotRebuild = NO;
3085
3373
  _deferTopEdgeSlotRebuild = NO;
3374
+ [self cancelPendingNativePagerScroll];
3086
3375
  [self restoreScrollEdgeContentMasks];
3087
3376
  [self detachScrollObserver];
3088
3377
  [self restoreSharedHeadersToContainer];
@@ -3173,6 +3462,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3173
3462
  - (void)layoutSubviews
3174
3463
  {
3175
3464
  [super layoutSubviews];
3465
+ void (^resizedPagerCompletion)(BOOL) = nil;
3466
+ NSInteger resizedPagerDestination = -1;
3467
+ NSUInteger resizedPagerGeneration = 0;
3468
+ NSUInteger resizedPagerTransition = 0;
3469
+ if (_directNativePagerEnabled && _pendingNativePagerScrollCompletion != nil &&
3470
+ CGRectGetWidth(_pagerScrollView.bounds) != CGRectGetWidth(self.bounds)) {
3471
+ resizedPagerCompletion = [_pendingNativePagerScrollCompletion copy];
3472
+ resizedPagerDestination = _destinationIndex;
3473
+ resizedPagerGeneration = _pendingNativePagerScrollGeneration;
3474
+ resizedPagerTransition = _pendingNativePagerScrollTransition;
3475
+ // Clear the old target before frame changes can deliver an animation end.
3476
+ [self cancelPendingNativePagerScroll];
3477
+ }
3176
3478
  [self syncScrollEdgeAppearance];
3177
3479
  _containerView.frame = self.bounds;
3178
3480
  [self registerExternalOuterPager];
@@ -3245,10 +3547,20 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3245
3547
  [self updateHeaderSettledStateForceBusy:NO];
3246
3548
  [self logHomeVisualGeometry:@"layout" scrollView:_observedScrollView];
3247
3549
  [self scheduleCoalescedScrollReconcile];
3550
+ if (resizedPagerCompletion != nil && _directNativePagerEnabled &&
3551
+ !_isBeingRecycled && !_isPagerDragging && _generation == resizedPagerGeneration &&
3552
+ _transitionId == resizedPagerTransition && resizedPagerDestination >= 0 &&
3553
+ resizedPagerDestination < _pageControllers.count) {
3554
+ [_pagerScrollView setContentOffset:CGPointMake(
3555
+ [self directPagerOffsetForIndex:resizedPagerDestination], 0
3556
+ ) animated:NO];
3557
+ resizedPagerCompletion(YES);
3558
+ }
3248
3559
  }
3249
3560
 
3250
3561
  - (void)prepareForRecycle
3251
3562
  {
3563
+ [self cancelPendingNativePagerScroll];
3252
3564
  [_externalOuterPager removeNestedOverlayObserver:self];
3253
3565
  _externalOuterPager = nil;
3254
3566
  [self cancelTopInsetReboundCheck];
@@ -3501,6 +3813,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3501
3813
  }
3502
3814
  if (_needsPropsReapply ||
3503
3815
  oldViewProps.externalTopOverlayTag != newViewProps.externalTopOverlayTag) {
3816
+ if (_topOverlayView == nil && _externalTopOverlayTag > 0 &&
3817
+ _externalTopOverlayTag != newViewProps.externalTopOverlayTag) {
3818
+ [self clearProgrammaticScrollEdges];
3819
+ }
3504
3820
  _externalTopOverlayTag = newViewProps.externalTopOverlayTag;
3505
3821
  [self registerExternalOuterPager];
3506
3822
  insetsChanged = [self resolveExternalTopOverlay] || insetsChanged;
@@ -4439,6 +4755,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4439
4755
  // still represents the old list, before Fabric applies its next owner's props.
4440
4756
  - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
4441
4757
  {
4758
+ [self releaseProgrammaticScrollEdge:scrollView];
4442
4759
  [self updateNativeScrollerViewport:scrollView stickyHeight:0];
4443
4760
  [self releaseRefreshControlForScrollView:scrollView];
4444
4761
  if (scrollView == _observedScrollView) {
@@ -5005,6 +5322,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5005
5322
 
5006
5323
  #pragma mark - Pager navigation
5007
5324
 
5325
+ - (void)cancelPendingNativePagerScroll
5326
+ {
5327
+ if (_pendingNativePagerScrollCompletion == nil) return;
5328
+ _pendingNativePagerScrollCompletion = nil;
5329
+ _pendingNativePagerScrollGeneration = 0;
5330
+ _pendingNativePagerScrollTransition = 0;
5331
+ _pendingNativePagerScrollTarget = 0;
5332
+ CGPoint offset = _pagerScrollView.contentOffset;
5333
+ [_pagerScrollView setContentOffset:offset animated:NO];
5334
+ }
5335
+
5008
5336
  - (BOOL)isLtrLayout
5009
5337
  {
5010
5338
  return [_layoutDirection isEqualToString:@"ltr"];
@@ -5038,11 +5366,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5038
5366
  return;
5039
5367
  }
5040
5368
 
5369
+ if (_directNativePagerEnabled && animated && index != _currentIndex) {
5370
+ [self adoptCurrentScrollEdgeForProgrammaticTransition:NO];
5371
+ }
5041
5372
  [self beginPageStateTransition];
5042
5373
  [self preparePageForHorizontalTransitionAtIndex:index];
5043
5374
  _destinationIndex = index;
5044
5375
  [self liftSharedHeadersForPagerTransition:@"programmatic"];
5045
5376
  [self detachScrollObserver];
5377
+ if (_directNativePagerEnabled && animated && index != _currentIndex) {
5378
+ [self beginProgrammaticScrollEdgesToIndex:index allowExternalOverlay:NO];
5379
+ }
5046
5380
  BOOL forward = (index > _currentIndex && self.isLtrLayout) ||
5047
5381
  (index < _currentIndex && !self.isLtrLayout);
5048
5382
  UIPageViewControllerNavigationDirection direction = forward
@@ -5082,13 +5416,41 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5082
5416
  };
5083
5417
 
5084
5418
  if (animated && index != _currentIndex) {
5419
+ BOOL useNativeAnimatedScroll = NO;
5420
+ if (@available(iOS 27.0, *)) {
5421
+ useNativeAnimatedScroll = _nativeSmoothHeaderScrollEnabled && _topScrollEdgeEnabled &&
5422
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
5423
+ _topOverlayView != nil && _externalTopOverlayTag <= 0 && self.window != nil;
5424
+ }
5425
+ if (useNativeAnimatedScroll) {
5426
+ // Keep the traversed pages owned until UIKit finishes scrolling.
5427
+ NSInteger first = MIN(_currentIndex, index);
5428
+ NSInteger last = MAX(_currentIndex, index);
5429
+ _programmaticPageMaskIndexes = [NSIndexSet indexSetWithIndexesInRange:
5430
+ NSMakeRange(first, last - first + 1)];
5431
+ [self updateScrollEdgeContentMasks];
5432
+ }
5085
5433
  _pagerScrollView.scrollEnabled = NO;
5086
- [UIView animateWithDuration:0.28
5087
- delay:0
5088
- options:UIViewAnimationOptionCurveEaseInOut |
5089
- UIViewAnimationOptionAllowUserInteraction
5090
- animations:animations
5091
- completion:completion];
5434
+ if (useNativeAnimatedScroll) {
5435
+ if (fabs(_pagerScrollView.contentOffset.x - targetOffset) <= 0.5) {
5436
+ [_nativeTabBarView setProgress:index];
5437
+ [self updatePageStateTransitionProgress:index];
5438
+ completion(YES);
5439
+ } else {
5440
+ _pendingNativePagerScrollCompletion = [completion copy];
5441
+ _pendingNativePagerScrollGeneration = capturedGeneration;
5442
+ _pendingNativePagerScrollTransition = capturedTransition;
5443
+ _pendingNativePagerScrollTarget = targetOffset;
5444
+ [_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0) animated:YES];
5445
+ }
5446
+ } else {
5447
+ [UIView animateWithDuration:0.28
5448
+ delay:0
5449
+ options:UIViewAnimationOptionCurveEaseInOut |
5450
+ UIViewAnimationOptionAllowUserInteraction
5451
+ animations:animations
5452
+ completion:completion];
5453
+ }
5092
5454
  } else {
5093
5455
  [UIView performWithoutAnimation:animations];
5094
5456
  completion(YES);
@@ -5130,7 +5492,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5130
5492
  if (self->_generation != generation || self->_transitionId != transition ||
5131
5493
  self->_isBeingRecycled) return;
5132
5494
  [self finishPageStateTransition];
5495
+ self->_programmaticPageMaskIndexes = nil;
5133
5496
  self->_transitioning = NO;
5497
+ BOOL hadProgrammaticScrollEdges = self->_programmaticScrollEdgeOwners.count > 0;
5498
+ [self finishProgrammaticScrollEdges];
5134
5499
  if (self->_hasPendingDirectNativePagerChange) {
5135
5500
  BOOL enabled = self->_pendingDirectNativePagerEnabled;
5136
5501
  self->_hasPendingDirectNativePagerChange = NO;
@@ -5139,8 +5504,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5139
5504
  [self rebuildSlots];
5140
5505
  }
5141
5506
  if (!self->_hasAppliedInitialPage) [self setNeedsLayout];
5507
+ if (hadProgrammaticScrollEdges && !self->_transitioning && !self->_needsSlotRebuild) {
5508
+ [self attachScrollObserverForCurrentPage];
5509
+ }
5142
5510
  [self drainPendingGoTo];
5143
- if (!self->_transitioning && !self->_needsSlotRebuild) {
5511
+ if (!hadProgrammaticScrollEdges && !self->_transitioning && !self->_needsSlotRebuild) {
5144
5512
  [self attachScrollObserverForCurrentPage];
5145
5513
  }
5146
5514
  [self updateScrollEdgeContentMasks];
@@ -5194,21 +5562,30 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5194
5562
 
5195
5563
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
5196
5564
  {
5565
+ if (scrollView == _pagerScrollView) [self cancelPendingNativePagerScroll];
5566
+ ++_transitionId;
5197
5567
  [self updateHeaderSettledStateForceBusy:YES];
5198
5568
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
5199
5569
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
5200
5570
  }
5571
+ if (_directNativePagerEnabled) [self adoptCurrentScrollEdgeForProgrammaticTransition:YES];
5201
5572
  [self beginPageStateTransition];
5202
5573
  if (_directNativePagerEnabled) {
5203
5574
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex - 1];
5204
5575
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex + 1];
5205
5576
  }
5206
5577
  // A user drag owns the result even if UIKit later completes the old animation.
5207
- ++_transitionId;
5578
+ _programmaticPageMaskIndexes = nil;
5208
5579
  [self liftSharedHeadersForPagerTransition:@"interactive"];
5209
5580
  [self detachScrollObserver];
5210
5581
  _isPagerDragging = YES;
5211
5582
  _transitioning = YES;
5583
+ if (_directNativePagerEnabled) {
5584
+ if (_currentIndex > 0) [self beginProgrammaticScrollEdgesToIndex:_currentIndex - 1 allowExternalOverlay:YES];
5585
+ if (_currentIndex + 1 < _pageControllers.count) {
5586
+ [self beginProgrammaticScrollEdgesToIndex:_currentIndex + 1 allowExternalOverlay:YES];
5587
+ }
5588
+ }
5212
5589
  if (_nativeSmoothHeaderScrollEnabled) {
5213
5590
  RNCCollapsiblePagerLog([NSString stringWithFormat:
5214
5591
  @"gesture-owner=pager page=%ld",
@@ -5341,6 +5718,20 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5341
5718
  [self finishPagerScrollEmittingSelection:YES];
5342
5719
  }
5343
5720
 
5721
+ - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
5722
+ {
5723
+ if (scrollView != _pagerScrollView || _pendingNativePagerScrollCompletion == nil ||
5724
+ scrollView.isDragging || _generation != _pendingNativePagerScrollGeneration ||
5725
+ _transitionId != _pendingNativePagerScrollTransition ||
5726
+ fabs(scrollView.contentOffset.x - _pendingNativePagerScrollTarget) > 0.5) return;
5727
+ void (^completion)(BOOL) = _pendingNativePagerScrollCompletion;
5728
+ _pendingNativePagerScrollCompletion = nil;
5729
+ _pendingNativePagerScrollGeneration = 0;
5730
+ _pendingNativePagerScrollTransition = 0;
5731
+ _pendingNativePagerScrollTarget = 0;
5732
+ completion(YES);
5733
+ }
5734
+
5344
5735
  - (void)scrollViewDidScroll:(UIScrollView *)scrollView
5345
5736
  {
5346
5737
  CGFloat width = scrollView.bounds.size.width;