@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-20

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 +14 -0
  2. package/android/src/androidTest/AndroidManifest.xml +7 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +90 -21
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
  8. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
  9. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
  10. package/docs/SPEC.md +253 -10
  11. package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
  12. package/ios/RNCNativeScrollerComponentView.h +2 -0
  13. package/ios/RNCNativeScrollerComponentView.mm +42 -0
  14. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +393 -0
  15. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
  16. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
  17. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
  18. package/lib/module/CollapsiblePagerView.js +32 -7
  19. package/lib/module/CollapsiblePagerView.web.js +439 -39
  20. package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
  21. package/lib/module/NativeScroller.web.js +16 -3
  22. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  23. package/lib/module/index.js +2 -0
  24. package/lib/module/index.native.js +9 -0
  25. package/lib/module/index.web.js +2 -0
  26. package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
  27. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
  28. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
  29. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  30. package/lib/typescript/src/index.d.ts +4 -0
  31. package/lib/typescript/src/index.native.d.ts +13 -0
  32. package/lib/typescript/src/index.web.d.ts +4 -0
  33. package/package.json +6 -4
  34. package/react-native-pager-view.podspec +1 -0
  35. package/src/CollapsiblePagerView.tsx +99 -8
  36. package/src/CollapsiblePagerView.web.tsx +657 -51
  37. package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
  38. package/src/NativeScroller.web.tsx +24 -2
  39. package/src/WebCollapsiblePagerPage.tsx +258 -0
  40. package/src/index.native.tsx +30 -0
  41. package/src/index.tsx +4 -0
  42. package/src/index.web.tsx +4 -0
@@ -18,6 +18,7 @@
18
18
 
19
19
  // OneKey patch: deduplicate structural mutation parents before scanning the page.
20
20
  #include <unordered_set>
21
+ #include <cmath>
21
22
 
22
23
  using namespace facebook::react;
23
24
 
@@ -25,6 +26,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
25
26
  static void *RNCRefreshControlLayerContext = &RNCRefreshControlLayerContext;
26
27
  static void *RNCScrollRefreshControlContext = &RNCScrollRefreshControlContext;
27
28
  static char RNCRefreshControlOffsetKey;
29
+ static void RNCCollapsiblePagerLog(NSString *message);
28
30
 
29
31
  @interface RNCRefreshControlOffsetState : NSObject
30
32
  + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner;
@@ -39,6 +41,10 @@ static char RNCRefreshControlOffsetKey;
39
41
  BOOL _originalMasksToBounds;
40
42
  BOOL _applying;
41
43
  BOOL _observing;
44
+ CGFloat _lastLoggedOffset;
45
+ NSUInteger _lastLoggedOwnerCount;
46
+ CGFloat _lastLoggedTransformY;
47
+ BOOL _hasLoggedState;
42
48
  }
43
49
 
44
50
  - (instancetype)initWithControl:(UIRefreshControl *)control
@@ -79,6 +85,21 @@ static char RNCRefreshControlOffsetKey;
79
85
  }
80
86
  [CATransaction commit];
81
87
  _applying = NO;
88
+ #if DEBUG
89
+ if (!_hasLoggedState || fabs(_lastLoggedOffset - offset) > 0.01 ||
90
+ _lastLoggedOwnerCount != _offsets.count ||
91
+ fabs(_lastLoggedTransformY - transform.m42) > 0.01) {
92
+ _hasLoggedState = YES;
93
+ _lastLoggedOffset = offset;
94
+ _lastLoggedOwnerCount = _offsets.count;
95
+ _lastLoggedTransformY = transform.m42;
96
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
97
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=refresh-transform ownerCount=%lu offsetSum=%.1f baselineY=%.1f appliedY=%.1f masksToBounds=%d",
98
+ [NSDate.date timeIntervalSince1970] * 1000, CACurrentMediaTime() * 1000,
99
+ (unsigned long)_offsets.count, offset, _baselineTransform.m42,
100
+ transform.m42, control.layer.masksToBounds]);
101
+ }
102
+ #endif
82
103
  }
83
104
 
84
105
  + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner
@@ -137,6 +158,20 @@ static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
137
158
 
138
159
  static const CFTimeInterval RNCCollapsiblePagerFallbackDebounceInterval = 0.5;
139
160
 
161
+ typedef NS_ENUM(NSInteger, RNCScrollCandidateTier) {
162
+ RNCScrollCandidateTierNone = 0,
163
+ RNCScrollCandidateTierGenericFallback = 1,
164
+ RNCScrollCandidateTierVerifiedNativeList = 2,
165
+ RNCScrollCandidateTierFabricScrollView = 3,
166
+ RNCScrollCandidateTierExplicitScroller = 4,
167
+ };
168
+
169
+ struct RNCScrollCandidateResolution {
170
+ UIScrollView *scrollView = nil;
171
+ RNCScrollCandidateTier tier = RNCScrollCandidateTierNone;
172
+ BOOL hasUnreadyScrollHost = NO;
173
+ };
174
+
140
175
  // OneKey patch: native scroll views can survive a move or be reused for a new
141
176
  // Fabric component. Keep weak identity until their page is attached again.
142
177
  @interface RNCCollapsiblePagerReleasedScrollState : NSObject
@@ -348,7 +383,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
348
383
  @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
349
384
  - (void)updateItemsJSON:(NSString *)itemsJSON;
350
385
  - (void)updateStyleWithHeight:(CGFloat)height
386
+ variant:(NSString *)variant
351
387
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
388
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
352
389
  itemSpacing:(CGFloat)itemSpacing
353
390
  fontSize:(CGFloat)fontSize
354
391
  fontFamily:(NSString *)fontFamily
@@ -365,12 +402,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
365
402
 
366
403
  @implementation RNCCollapsiblePagerNativeTabBarView {
367
404
  RNCCollapsiblePagerHorizontalScrollView *_scrollView;
405
+ UIView *_trackView;
368
406
  UIView *_indicatorView;
369
407
  NSArray<NSDictionary *> *_items;
370
408
  NSMutableArray<UIButton *> *_buttons;
371
409
  NSMutableArray<NSValue *> *_indicatorFrames;
372
410
  CGFloat _barHeight;
373
411
  CGFloat _contentPaddingHorizontal;
412
+ CGFloat _contentPaddingVertical;
413
+ BOOL _segmented;
374
414
  CGFloat _itemSpacing;
375
415
  CGFloat _fontSize;
376
416
  NSString *_fontFamily;
@@ -393,6 +433,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
393
433
  _indicatorFrames = [NSMutableArray new];
394
434
  _barHeight = 44;
395
435
  _contentPaddingHorizontal = 20;
436
+ _contentPaddingVertical = 0;
437
+ _segmented = NO;
396
438
  _itemSpacing = 8;
397
439
  _fontSize = 16;
398
440
  _activeTextColor = UIColor.labelColor;
@@ -414,6 +456,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
414
456
  _scrollView.delegate = self;
415
457
  [self addSubview:_scrollView];
416
458
 
459
+ _trackView = [UIView new];
460
+ _trackView.userInteractionEnabled = NO;
461
+ [_scrollView addSubview:_trackView];
462
+
417
463
  _indicatorView = [UIView new];
418
464
  _indicatorView.userInteractionEnabled = NO;
419
465
  [_scrollView addSubview:_indicatorView];
@@ -468,7 +514,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
468
514
  }
469
515
 
470
516
  - (void)updateStyleWithHeight:(CGFloat)height
517
+ variant:(NSString *)variant
471
518
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
519
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
472
520
  itemSpacing:(CGFloat)itemSpacing
473
521
  fontSize:(CGFloat)fontSize
474
522
  fontFamily:(NSString *)fontFamily
@@ -480,19 +528,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
480
528
  indicatorBottom:(CGFloat)indicatorBottom
481
529
  {
482
530
  _barHeight = MAX(1, height);
531
+ BOOL wasSegmented = _segmented;
532
+ _segmented = [variant isEqualToString:@"segmented"];
483
533
  _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
534
+ _contentPaddingVertical = MAX(0, contentPaddingVertical);
484
535
  _itemSpacing = MAX(0, itemSpacing);
485
536
  _fontSize = MAX(1, fontSize);
486
537
  _fontFamily = [fontFamily copy];
487
- self.backgroundColor = backgroundColor ?: UIColor.clearColor;
538
+ self.backgroundColor = _segmented ? UIColor.clearColor : (backgroundColor ?: UIColor.clearColor);
488
539
  _scrollView.backgroundColor = self.backgroundColor;
540
+ _scrollView.scrollEnabled = !_segmented;
541
+ if (_segmented && !wasSegmented) [_scrollView setContentOffset:CGPointZero animated:NO];
542
+ _trackView.hidden = !_segmented;
543
+ _trackView.backgroundColor = backgroundColor ?: UIColor.clearColor;
489
544
  _activeTextColor = activeTextColor ?: UIColor.labelColor;
490
545
  _inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
491
546
  _indicatorColor = indicatorColor ?: _activeTextColor;
492
547
  _indicatorHeight = MAX(0, indicatorHeight);
493
548
  _indicatorBottom = MAX(0, indicatorBottom);
494
549
  _indicatorView.backgroundColor = _indicatorColor;
495
- _indicatorView.layer.cornerRadius = _indicatorHeight / 2;
550
+ _indicatorView.layer.cornerRadius = _segmented ? 0 : _indicatorHeight / 2;
496
551
  for (UIButton *button in _buttons) button.titleLabel.font = self.tabFont;
497
552
  [self setNeedsLayout];
498
553
  }
@@ -551,6 +606,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
551
606
  {
552
607
  [super layoutSubviews];
553
608
  _scrollView.frame = self.bounds;
609
+ if (_segmented) {
610
+ CGFloat width = CGRectGetWidth(self.bounds);
611
+ CGFloat height = CGRectGetHeight(self.bounds);
612
+ CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
613
+ CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
614
+ CGRect trackFrame = CGRectMake(insetX, insetY,
615
+ MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
616
+ _trackView.frame = trackFrame;
617
+ _trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
618
+ _scrollView.contentSize = self.bounds.size;
619
+ [_indicatorFrames removeAllObjects];
620
+ NSUInteger count = _buttons.count;
621
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
622
+ CGFloat first = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * index / count;
623
+ CGFloat last = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * (index + 1) / count;
624
+ CGFloat x = self->_leftToRight ? first : CGRectGetMaxX(trackFrame) - (last - CGRectGetMinX(trackFrame));
625
+ CGFloat end = self->_leftToRight ? last : CGRectGetMaxX(trackFrame) - (first - CGRectGetMinX(trackFrame));
626
+ button.frame = CGRectMake(x, CGRectGetMinY(trackFrame), MAX(0, end - x), CGRectGetHeight(trackFrame));
627
+ CGRect thumbFrame = CGRectMake(x + 2, CGRectGetMinY(trackFrame) + 2,
628
+ MAX(0, CGRectGetWidth(button.frame) - 4), MAX(0, CGRectGetHeight(trackFrame) - 4));
629
+ [self->_indicatorFrames addObject:[NSValue valueWithCGRect:thumbFrame]];
630
+ }];
631
+ _indicatorView.layer.cornerRadius = MAX(0, CGRectGetHeight(trackFrame) - 4) / 2;
632
+ [self updatePresentation];
633
+ return;
634
+ }
635
+ _trackView.frame = CGRectZero;
554
636
  UIFont *font = self.tabFont;
555
637
  NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
556
638
  CGFloat itemsWidth = 0;
@@ -614,10 +696,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
614
696
  CGRectGetMinX(fromFrame) + (CGRectGetMinX(toFrame) - CGRectGetMinX(fromFrame)) * fraction,
615
697
  CGRectGetMinY(fromFrame),
616
698
  CGRectGetWidth(fromFrame) + (CGRectGetWidth(toFrame) - CGRectGetWidth(fromFrame)) * fraction,
617
- _indicatorHeight
699
+ _segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
618
700
  );
619
701
  _indicatorView.frame = indicatorFrame;
620
- _indicatorView.hidden = _indicatorHeight <= 0;
702
+ _indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
621
703
 
622
704
  NSInteger selectedIndex = (NSInteger)round(progress);
623
705
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
@@ -634,7 +716,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
634
716
  : UIAccessibilityTraitButton;
635
717
  }];
636
718
 
637
- if (_shouldCenterSelectedItem && !_scrollView.tracking && !_scrollView.dragging &&
719
+ if (_shouldCenterSelectedItem && !_segmented && !_scrollView.tracking && !_scrollView.dragging &&
638
720
  !_scrollView.decelerating && CGRectGetWidth(_scrollView.bounds) > 0) {
639
721
  CGFloat maximumOffset = MAX(0, _scrollView.contentSize.width - CGRectGetWidth(_scrollView.bounds));
640
722
  CGFloat desiredOffset = RNCClamp(
@@ -1451,23 +1533,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1451
1533
  - (void)liftSharedHeadersForPagerTransition:(NSString *)reason;
1452
1534
  - (void)restoreSharedHeadersToContainer;
1453
1535
  - (void)layoutSharedHeaders;
1536
+ - (void)bringReservedStickyControlToFront;
1454
1537
  - (void)restorePinnedStickyHeader;
1455
1538
  - (void)updatePinnedStickyHeader;
1456
1539
  - (UIScrollView *)currentVerticalScrollViewForParentPager;
1457
1540
  - (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex;
1541
+ - (RNCScrollCandidateResolution)resolveScrollCandidateForPageAtIndex:(NSInteger)pageIndex;
1542
+ - (RNCScrollCandidateResolution)resolveScrollCandidateInView:(UIView *)view;
1458
1543
  - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
1459
1544
  - (UIScrollView *)findFabricScrollViewInView:(UIView *)view;
1460
1545
  - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
1546
+ - (void)scheduleCoalescedScrollReconcile;
1461
1547
  - (void)scheduleScrollObserverRetryForCurrentPage;
1462
1548
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
1463
1549
  - (void)updateSharedHeaderPressCancellationGesture;
1464
1550
  - (void)detachSharedHeaderPressCancellationGesture;
1465
1551
  - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
1466
1552
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
1553
+ - (void)logHomeVisualGeometry:(NSString *)phase scrollView:(UIScrollView *)scrollView;
1467
1554
  - (void)cancelTopInsetReboundCheck;
1468
1555
  - (void)scheduleTopInsetReboundCheck;
1469
1556
  - (void)syncScrollEdgeAppearance;
1470
1557
  - (void)updateHardEdgeSeparatorCovers;
1558
+ - (void)updateTopOverlayThemeBackground;
1471
1559
  - (void)updateScrollEdgeContentMasks;
1472
1560
  - (void)restoreScrollEdgeContentMasks;
1473
1561
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
@@ -1506,6 +1594,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1506
1594
  UILabel *_stickyEdgeMarker;
1507
1595
  CALayer *_topEdgeSeparatorCover;
1508
1596
  CALayer *_stickyEdgeSeparatorCover;
1597
+ CALayer *_topOverlayThemeBackgroundLayer;
1509
1598
  __weak UIView *_topEdgeInteractionHost;
1510
1599
  __weak UIView *_stickyEdgeInteractionHost;
1511
1600
  __weak UIScrollView *_topEdgeScrollView;
@@ -1535,6 +1624,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1535
1624
  NSUInteger _pendingTopInsetReboundGeneration;
1536
1625
  NSTimer *_topInsetReboundTimer;
1537
1626
  CGFloat _nativeTabBarHeight;
1627
+ CGFloat _nativeTabBarTrailingReservedWidth;
1538
1628
  CGFloat _nativeSubHeaderHeight;
1539
1629
  CGFloat _headerOffset;
1540
1630
  BOOL _scrollEnabled;
@@ -1576,6 +1666,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1576
1666
  NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
1577
1667
  NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
1578
1668
  NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
1669
+ CFTimeInterval _homeVisualLastScrollLog;
1670
+ CFTimeInterval _homeVisualLastLayoutLog;
1671
+ CFTimeInterval _homeVisualStartMono;
1672
+ NSString *_homeVisualLastGeometrySignature;
1673
+ CGFloat _homeVisualLastThemeOpacity;
1674
+ CFTimeInterval _homeVisualLastThemeLog;
1675
+ BOOL _homeVisualThemeHolding;
1579
1676
  NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
1580
1677
  NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
1581
1678
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
@@ -1614,6 +1711,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1614
1711
  RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
1615
1712
  RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer *_verticalPagerGesture;
1616
1713
  BOOL _observingContentOffset;
1714
+ BOOL _awaitingScrollOwnerForCurrentPage;
1715
+ BOOL _scrollReconcileScheduled;
1617
1716
  BOOL _scrollResolveRetryScheduled;
1618
1717
  CFTimeInterval _pendingFallbackScrollViewDetectedAt;
1619
1718
  NSInteger _scrollResolveRetryPageIndex;
@@ -1645,6 +1744,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1645
1744
  _displayBodyOverrides = [NSMutableDictionary new];
1646
1745
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nativeListScrollCommandAccepted:)
1647
1746
  name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
1747
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nativeScrollViewDidBecomeReadyNotification:)
1748
+ name:@"OneKeyNativeScrollViewDidBecomeReady" object:nil];
1648
1749
  _pendingPageRestoreKeys = [NSMutableSet new];
1649
1750
  _releasedPageScrollStates = [NSMutableDictionary new];
1650
1751
  _mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
@@ -1803,6 +1904,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1803
1904
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
1804
1905
  [self updateSharedHeaderPressCancellationGesture];
1805
1906
  [self updateHeaderSettledStateForceBusy:NO];
1907
+ [self logHomeVisualGeometry:@"window" scrollView:_observedScrollView];
1806
1908
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1807
1909
  [self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight + self.effectiveTopOverlayHeight];
1808
1910
  }
@@ -1830,7 +1932,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1830
1932
 
1831
1933
  - (CGFloat)effectiveTopOverlayHeight
1832
1934
  {
1833
- return _topOverlayView != nil ? _topOverlayHeight : _externalTopOverlayHeight;
1935
+ BOOL localOverlayPending = _topScrollEdgeEnabled && _topOverlayHeight > 0 &&
1936
+ _externalTopOverlayTag <= 0;
1937
+ return _topOverlayView != nil || localOverlayPending
1938
+ ? _topOverlayHeight : _externalTopOverlayHeight;
1834
1939
  }
1835
1940
 
1836
1941
  - (BOOL)hasFixedTopOverlay
@@ -1913,6 +2018,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1913
2018
  [super traitCollectionDidChange:previousTraitCollection];
1914
2019
  if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
1915
2020
  [self syncScrollEdgeAppearance];
2021
+ [self updateTopOverlayThemeBackground];
2022
+ [self logHomeVisualGeometry:@"trait" scrollView:_observedScrollView];
1916
2023
  }
1917
2024
  }
1918
2025
 
@@ -1962,6 +2069,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1962
2069
  self.window != nil && !_isBeingRecycled;
1963
2070
  if (!enabled) {
1964
2071
  [self restoreScrollEdgeContentMasks];
2072
+ [self updateTopOverlayThemeBackground];
1965
2073
  return;
1966
2074
  }
1967
2075
  if (self.hasFixedTopOverlay && _sharedHeaderHostView.window == self.window &&
@@ -2031,6 +2139,102 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2031
2139
  [_ownedPageMasks removeObjectForKey:page];
2032
2140
  [_originalPageMasks removeObjectForKey:page];
2033
2141
  }
2142
+ [self updateTopOverlayThemeBackground];
2143
+ }
2144
+
2145
+ - (void)updateTopOverlayThemeBackground
2146
+ {
2147
+ if (@available(iOS 27.0, *)) {
2148
+ BOOL localHardOverlay = !_isBeingRecycled && _topScrollEdgeEnabled &&
2149
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2150
+ _nativeSmoothHeaderScrollEnabled && _topOverlayView != nil &&
2151
+ _externalTopOverlayTag <= 0 && _topOverlayHeight > 0 && _headerHeight > 0 &&
2152
+ _topOverlayHostView.window == self.window && self.window != nil &&
2153
+ !CGRectIsEmpty(_topOverlayHostView.bounds);
2154
+ if (!localHardOverlay) {
2155
+ [_topOverlayThemeBackgroundLayer removeFromSuperlayer];
2156
+ _topOverlayThemeBackgroundLayer = nil;
2157
+ _homeVisualThemeHolding = NO;
2158
+ return;
2159
+ }
2160
+
2161
+ UIColor *color = nil;
2162
+ for (UIView *view = self; view != nil; view = view.superview) {
2163
+ UIColor *candidate = [view.backgroundColor resolvedColorWithTraitCollection:
2164
+ _topOverlayHostView.traitCollection];
2165
+ if (candidate != nil && CGColorGetAlpha(candidate.CGColor) >= 1) {
2166
+ color = candidate;
2167
+ break;
2168
+ }
2169
+ }
2170
+ if (color == nil) {
2171
+ [_topOverlayThemeBackgroundLayer removeFromSuperlayer];
2172
+ _topOverlayThemeBackgroundLayer = nil;
2173
+ return;
2174
+ }
2175
+
2176
+ UIScrollView *scrollView = _observedScrollView;
2177
+ BOOL geometryReady = scrollView != nil &&
2178
+ _sharedHeaderScrollView == scrollView && _topEdgeScrollView == scrollView &&
2179
+ _topEdgeInteraction.scrollView == scrollView &&
2180
+ scrollView.topEdgeEffect.style == UIScrollEdgeEffectStyle.hardStyle &&
2181
+ _sharedHeaderMaskView != nil &&
2182
+ _sharedHeaderHostView.maskView == _sharedHeaderMaskView &&
2183
+ _headerView.window == self.window;
2184
+ if (_topOverlayThemeBackgroundLayer == nil && !geometryReady) return;
2185
+ BOOL created = _topOverlayThemeBackgroundLayer == nil;
2186
+ if (created) {
2187
+ _topOverlayThemeBackgroundLayer = [CALayer layer];
2188
+ _topOverlayThemeBackgroundLayer.name = @"onekey-top-overlay-theme-background";
2189
+ _topOverlayThemeBackgroundLayer.actions = @{
2190
+ @"position": NSNull.null, @"bounds": NSNull.null,
2191
+ @"opacity": NSNull.null, @"backgroundColor": NSNull.null
2192
+ };
2193
+ [_topOverlayHostView.layer insertSublayer:_topOverlayThemeBackgroundLayer atIndex:0];
2194
+ }
2195
+ _topOverlayThemeBackgroundLayer.frame = _topOverlayHostView.bounds;
2196
+ _topOverlayThemeBackgroundLayer.backgroundColor = color.CGColor;
2197
+ if (!geometryReady) {
2198
+ if (!_homeVisualThemeHolding) {
2199
+ _homeVisualThemeHolding = YES;
2200
+ #if DEBUG
2201
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2202
+ @"[IOS-HOME-VISUAL] phase=theme-background-hold pagerTag=%ld generation=%lu pageIndex=%ld opacity=%.3f",
2203
+ (long)self.tag, (unsigned long)_generation, (long)_currentIndex,
2204
+ _topOverlayThemeBackgroundLayer.opacity]);
2205
+ #endif
2206
+ }
2207
+ return;
2208
+ }
2209
+ _homeVisualThemeHolding = NO;
2210
+ CGRect overlayWindow = [_topOverlayHostView convertRect:_topOverlayHostView.bounds
2211
+ toView:self.window];
2212
+ CGRect headerWindow = [_headerView convertRect:_headerView.bounds
2213
+ toView:self.window];
2214
+ CGFloat overlayBottom = CGRectGetMaxY(overlayWindow);
2215
+ CGFloat collapse = MIN(MAX(overlayBottom - CGRectGetMinY(headerWindow), 0),
2216
+ _headerHeight);
2217
+ CGFloat opacity = 1 - collapse / _headerHeight;
2218
+ _topOverlayThemeBackgroundLayer.opacity = opacity;
2219
+ #if DEBUG
2220
+ CFTimeInterval now = CACurrentMediaTime();
2221
+ if (created || fabs(opacity - _homeVisualLastThemeOpacity) >= 0.1 ||
2222
+ now - _homeVisualLastThemeLog >= 2.0) {
2223
+ _homeVisualLastThemeOpacity = opacity;
2224
+ _homeVisualLastThemeLog = now;
2225
+ CGFloat red = 0, green = 0, blue = 0, alpha = 0;
2226
+ [color getRed:&red green:&green blue:&blue alpha:&alpha];
2227
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2228
+ @"[IOS-HOME-VISUAL] phase=theme-background epochMs=%.0f monoMs=%.0f pagerTag=%ld generation=%lu pageIndex=%ld opacity=%.3f collapse=%.1f header=%.1f overlayBottom=%.1f headerY=%.1f bodyYEstimate=%.1f hardOwner=%d rgba=%.3f,%.3f,%.3f,%.3f",
2229
+ NSDate.date.timeIntervalSince1970 * 1000, now * 1000,
2230
+ (long)self.tag, (unsigned long)_generation, (long)_currentIndex,
2231
+ opacity, collapse, (CGFloat)_headerHeight, overlayBottom,
2232
+ CGRectGetMinY(headerWindow), overlayBottom + _headerHeight +
2233
+ _stickyHeaderHeight - collapse, _topEdgeInteraction.scrollView == scrollView,
2234
+ red, green, blue, alpha]);
2235
+ }
2236
+ #endif
2237
+ }
2034
2238
  }
2035
2239
 
2036
2240
  - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
@@ -2187,6 +2391,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2187
2391
  CGRectGetWidth(_containerView.bounds), pixel);
2188
2392
  cover.contentsScale = scale;
2189
2393
  cover.backgroundColor = color.CGColor;
2394
+ cover.hidden = index == 0 && _stickyHeaderIsPinned;
2190
2395
  }
2191
2396
  if (index == 0) _topEdgeSeparatorCover = cover;
2192
2397
  else _stickyEdgeSeparatorCover = cover;
@@ -2198,6 +2403,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2198
2403
  [self updateTopScrollEdgeForScrollView:nil];
2199
2404
  [self cancelHeaderSettledCheck];
2200
2405
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
2406
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeScrollViewDidBecomeReady" object:nil];
2201
2407
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
2202
2408
  [self updateNativeScrollerViewport:scrollView stickyHeight:0];
2203
2409
  }
@@ -2345,6 +2551,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2345
2551
  }
2346
2552
  [_nativeTabBarView updateAncestorGesturePrecedence];
2347
2553
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2554
+ [self bringReservedStickyControlToFront];
2348
2555
  }
2349
2556
 
2350
2557
  - (void)restoreSharedHeadersToContainer
@@ -2450,10 +2657,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2450
2657
  CGFloat nativeTabBarVisibleHeight = _nativeTabBarView.hidden
2451
2658
  ? 0
2452
2659
  : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2660
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2453
2661
  _nativeTabBarView.frame = CGRectMake(
2454
- 0,
2662
+ self.isLtrLayout ? 0 : tabReservedWidth,
2455
2663
  stickyChildY,
2456
- width,
2664
+ width - tabReservedWidth,
2457
2665
  nativeTabBarVisibleHeight
2458
2666
  );
2459
2667
  _nativeSubHeaderView.frame = CGRectMake(
@@ -2467,9 +2675,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2467
2675
  );
2468
2676
  _topOverlayHostView.frame = CGRectMake(0, 0, width, self.effectiveTopOverlayHeight);
2469
2677
  _topOverlayView.frame = _topOverlayHostView.bounds;
2678
+ [self bringReservedStickyControlToFront];
2470
2679
  [self applyHeaderOffset];
2471
2680
  }
2472
2681
 
2682
+ - (void)bringReservedStickyControlToFront
2683
+ {
2684
+ if (_stickyHeaderView != nil &&
2685
+ _stickyHeaderView.superview == _nativeTabBarView.superview) {
2686
+ if (_nativeTabBarTrailingReservedWidth > 0) {
2687
+ [_stickyHeaderView.superview bringSubviewToFront:_stickyHeaderView];
2688
+ } else {
2689
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2690
+ if (!_nativeSubHeaderView.hidden &&
2691
+ _nativeSubHeaderView.superview == _nativeTabBarView.superview) {
2692
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2693
+ }
2694
+ }
2695
+ }
2696
+ }
2697
+
2473
2698
  - (void)updatePinnedStickyHeader
2474
2699
  {
2475
2700
  BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
@@ -2516,8 +2741,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2516
2741
  CGFloat childY = shouldPin ? 0 : _headerHeight;
2517
2742
  CGFloat width = CGRectGetWidth(self.bounds);
2518
2743
  CGFloat tabHeight = _nativeTabBarView.hidden ? 0 : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2744
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2519
2745
  _stickyHeaderView.frame = CGRectMake(0, childY, width, _stickyHeaderHeight);
2520
- _nativeTabBarView.frame = CGRectMake(0, childY, width, tabHeight);
2746
+ _nativeTabBarView.frame = CGRectMake(self.isLtrLayout ? 0 : tabReservedWidth,
2747
+ childY, width - tabReservedWidth, tabHeight);
2521
2748
  _nativeSubHeaderView.frame = CGRectMake(0, childY + tabHeight, width,
2522
2749
  MIN(MAX(0, _stickyHeaderHeight - tabHeight), _nativeSubHeaderHeight));
2523
2750
  _pinnedStickyHostView.hidden = !shouldPin;
@@ -2537,7 +2764,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2537
2764
  }
2538
2765
  [_nativeTabBarView updateAncestorGesturePrecedence];
2539
2766
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2767
+ [self bringReservedStickyControlToFront];
2540
2768
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2769
+ [self logHomeVisualGeometry:shouldPin ? @"pin" : @"unpin"
2770
+ scrollView:_observedScrollView];
2541
2771
  }
2542
2772
 
2543
2773
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
@@ -2689,6 +2919,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2689
2919
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
2690
2920
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
2691
2921
  {
2922
+ if (_needsSlotRebuild && _deferTopEdgeSlotRebuild &&
2923
+ _topScrollEdgeEnabled && _nativeSmoothHeaderScrollEnabled &&
2924
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2925
+ _topOverlayHeight > 0 && _pageKeys.count > 0 &&
2926
+ _externalTopOverlayTag <= 0 &&
2927
+ _logicalChildren.count == _pageKeys.count + 3 &&
2928
+ !_transitioning && !_isPagerDragging && !_isBeingRecycled) {
2929
+ _deferTopEdgeSlotRebuild = NO;
2930
+ }
2692
2931
  if (_needsSlotRebuild && !_deferTopEdgeSlotRebuild &&
2693
2932
  !_transitioning && !_isPagerDragging) {
2694
2933
  [self rebuildSlots];
@@ -2711,6 +2950,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2711
2950
  }
2712
2951
  [self updateScrollEdgeContentMasks];
2713
2952
  [_mountingRemovedScrollViews removeAllObjects];
2953
+ [self logHomeVisualGeometry:@"did-mount" scrollView:_observedScrollView];
2714
2954
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
2715
2955
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
2716
2956
  // Prepare newly mounted owned scrollers before their first frame, even while
@@ -2728,6 +2968,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2728
2968
  [self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
2729
2969
  }
2730
2970
  }
2971
+ UIView *page = _pageControllers[_currentIndex].view;
2972
+ if (_scrollResolveRetryAttempt >= 12 &&
2973
+ _releasedPageScrollStates[self.currentPageKey] != nil) {
2974
+ for (const auto &mutation : transaction.getMutations()) {
2975
+ if (mutation.type == ShadowViewMutation::Insert && mutation.parentTag > 0 &&
2976
+ [page viewWithTag:mutation.parentTag] != nil) {
2977
+ // A new current-page insertion starts a bounded recovery round after
2978
+ // the previous replacement search has scheduled its final retry.
2979
+ _scrollResolveRetryToken += 1;
2980
+ _scrollResolveRetryScheduled = NO;
2981
+ _scrollResolveRetryAttempt = 0;
2982
+ break;
2983
+ }
2984
+ }
2985
+ }
2986
+
2987
+ // Record recovery during transitions; ownership still waits for settlement.
2731
2988
  if (_transitioning || _isPagerDragging) return;
2732
2989
 
2733
2990
  // OneKey patch: a release must recover even when the changed parent is above
@@ -2736,7 +2993,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2736
2993
  [self attachScrollObserverForCurrentPage];
2737
2994
  return;
2738
2995
  }
2739
- UIView *page = _pageControllers[_currentIndex].view;
2740
2996
  BOOL headerIsAttached = [_sharedHeaderHostView isDescendantOfView:self];
2741
2997
  BOOL currentListIsAttached = _observedScrollView != nil &&
2742
2998
  [_observedScrollView isDescendantOfView:page] && headerIsAttached;
@@ -2904,18 +3160,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2904
3160
  if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
2905
3161
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
2906
3162
  if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3163
+ [self bringReservedStickyControlToFront];
2907
3164
  [_containerView bringSubviewToFront:_sharedHeaderHostView];
2908
3165
  if (_stickyHeaderIsPinned) {
2909
3166
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
2910
3167
  }
2911
3168
  if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2912
3169
  [self setNeedsLayout];
2913
- dispatch_async(dispatch_get_main_queue(), ^{
2914
- if (!self->_isBeingRecycled) {
2915
- [self prepareAdjacentPageInsets];
2916
- [self attachScrollObserverForCurrentPage];
2917
- }
2918
- });
3170
+ [self scheduleCoalescedScrollReconcile];
2919
3171
  }
2920
3172
 
2921
3173
  - (void)layoutSubviews
@@ -2991,12 +3243,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2991
3243
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2992
3244
  [self updateScrollEdgeContentMasks];
2993
3245
  [self updateHeaderSettledStateForceBusy:NO];
2994
- dispatch_async(dispatch_get_main_queue(), ^{
2995
- if (!self->_isBeingRecycled) {
2996
- [self prepareAdjacentPageInsets];
2997
- [self attachScrollObserverForCurrentPage];
2998
- }
2999
- });
3246
+ [self logHomeVisualGeometry:@"layout" scrollView:_observedScrollView];
3247
+ [self scheduleCoalescedScrollReconcile];
3000
3248
  }
3001
3249
 
3002
3250
  - (void)prepareForRecycle
@@ -3014,6 +3262,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3014
3262
  _generation++;
3015
3263
  [_mountingRemovedScrollViews removeAllObjects];
3016
3264
  _needsScrollObserverReattachAfterMount = NO;
3265
+ _awaitingScrollOwnerForCurrentPage = NO;
3266
+ _scrollReconcileScheduled = NO;
3017
3267
  [_releasedPageScrollStates removeAllObjects];
3018
3268
  [self detachSharedHeaderPressCancellationGesture];
3019
3269
  [self detachScrollObserver];
@@ -3110,7 +3360,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3110
3360
  _externalOuterTransitioning = NO;
3111
3361
  _topScrollEdgeEnabled = NO;
3112
3362
  _topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
3363
+ _homeVisualStartMono = 0;
3364
+ _homeVisualLastScrollLog = 0;
3365
+ _homeVisualLastLayoutLog = 0;
3366
+ _homeVisualLastGeometrySignature = nil;
3113
3367
  _nativeTabBarHeight = 44;
3368
+ _nativeTabBarTrailingReservedWidth = 0;
3114
3369
  _nativeSubHeaderHeight = 74;
3115
3370
  _headerOffset = 0;
3116
3371
  _currentLogicalOffset = 0;
@@ -3192,6 +3447,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3192
3447
  _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
3193
3448
  [_nativeTabBarView setLeftToRight:self.isLtrLayout];
3194
3449
  [_nativeSubHeaderView setLeftToRight:self.isLtrLayout];
3450
+ [self setNeedsLayout];
3451
+ }
3452
+ if (_needsPropsReapply ||
3453
+ oldViewProps.nativeTabBarTrailingReservedWidth != newViewProps.nativeTabBarTrailingReservedWidth) {
3454
+ double reserved = newViewProps.nativeTabBarTrailingReservedWidth;
3455
+ _nativeTabBarTrailingReservedWidth = std::isfinite(reserved) && reserved >= 0 ? reserved : 0;
3456
+ [self setNeedsLayout];
3195
3457
  }
3196
3458
  BOOL insetsChanged = NO;
3197
3459
  if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
@@ -3272,7 +3534,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3272
3534
  }
3273
3535
  if (_needsPropsReapply ||
3274
3536
  oldViewProps.nativeTabBarHeight != newViewProps.nativeTabBarHeight ||
3537
+ oldViewProps.nativeTabBarVariant != newViewProps.nativeTabBarVariant ||
3275
3538
  oldViewProps.nativeTabBarContentPaddingHorizontal != newViewProps.nativeTabBarContentPaddingHorizontal ||
3539
+ oldViewProps.nativeTabBarContentPaddingVertical != newViewProps.nativeTabBarContentPaddingVertical ||
3276
3540
  oldViewProps.nativeTabBarItemSpacing != newViewProps.nativeTabBarItemSpacing ||
3277
3541
  oldViewProps.nativeTabBarFontSize != newViewProps.nativeTabBarFontSize ||
3278
3542
  oldViewProps.nativeTabBarFontFamily != newViewProps.nativeTabBarFontFamily ||
@@ -3289,7 +3553,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3289
3553
  NSString *fontFamily = RCTNSStringFromString(newViewProps.nativeTabBarFontFamily);
3290
3554
  [_nativeTabBarView
3291
3555
  updateStyleWithHeight:_nativeTabBarHeight
3556
+ variant:RCTNSStringFromString(newViewProps.nativeTabBarVariant)
3292
3557
  contentPaddingHorizontal:MAX(0, newViewProps.nativeTabBarContentPaddingHorizontal)
3558
+ contentPaddingVertical:MAX(0, newViewProps.nativeTabBarContentPaddingVertical)
3293
3559
  itemSpacing:MAX(0, newViewProps.nativeTabBarItemSpacing)
3294
3560
  fontSize:newViewProps.nativeTabBarFontSize > 0 ? newViewProps.nativeTabBarFontSize : 16
3295
3561
  fontFamily:fontFamily
@@ -3314,6 +3580,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3314
3580
  if (insetsChanged) {
3315
3581
  [self reapplyInsetsToObservedScrollView];
3316
3582
  [self setNeedsLayout];
3583
+ [self logHomeVisualGeometry:@"props-inset" scrollView:_observedScrollView];
3317
3584
  }
3318
3585
  _applyingTopOverlayInsetChange = NO;
3319
3586
  if (!_nativeTabBarView.hidden) {
@@ -3322,6 +3589,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3322
3589
  if (!_nativeSubHeaderView.hidden) {
3323
3590
  [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
3324
3591
  }
3592
+ [self bringReservedStickyControlToFront];
3325
3593
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3326
3594
  if (_stickyHeaderIsPinned) {
3327
3595
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
@@ -3349,12 +3617,107 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3349
3617
 
3350
3618
  - (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex
3351
3619
  {
3352
- if (pageIndex < 0 || pageIndex >= _pageControllers.count) return nil;
3353
- UIView *pageView = _pageControllers[pageIndex].view;
3354
- UIScrollView *explicitScroller = [self findExplicitNativeScrollerInView:pageView];
3355
- if (explicitScroller != nil) return explicitScroller;
3356
- UIScrollView *fabricScrollView = [self findFabricScrollViewInView:pageView];
3357
- return fabricScrollView ?: [self findVerticalScrollViewInView:pageView];
3620
+ return [self resolveScrollCandidateForPageAtIndex:pageIndex].scrollView;
3621
+ }
3622
+
3623
+ - (RNCScrollCandidateResolution)resolveScrollCandidateForPageAtIndex:(NSInteger)pageIndex
3624
+ {
3625
+ if (pageIndex < 0 || pageIndex >= _pageControllers.count) return {};
3626
+ return [self resolveScrollCandidateInView:_pageControllers[pageIndex].view];
3627
+ }
3628
+
3629
+ // Single-pass tiered resolution replaces three separate subtree walks while
3630
+ // distinguishing genuinely static pages from pages with unready scroll hosts.
3631
+ - (RNCScrollCandidateResolution)resolveScrollCandidateInView:(UIView *)view
3632
+ {
3633
+ RNCScrollCandidateResolution resolution;
3634
+ if (view == nil || view == _sharedHeaderHostView) return resolution;
3635
+ if (view != self &&
3636
+ [view isKindOfClass:RNCCollapsiblePagerViewComponentView.class]) {
3637
+ UIScrollView *nested = [(RNCCollapsiblePagerViewComponentView *)view
3638
+ currentVerticalScrollViewForParentPager];
3639
+ if (nested != nil) {
3640
+ resolution.scrollView = nested;
3641
+ resolution.tier = RNCScrollCandidateTierExplicitScroller;
3642
+ } else {
3643
+ resolution.hasUnreadyScrollHost = YES;
3644
+ }
3645
+ return resolution;
3646
+ }
3647
+
3648
+ Class nativeListClass = NSClassFromString(@"HybridNativeListComponent");
3649
+ if (nativeListClass != Nil && [view isKindOfClass:nativeListClass]) {
3650
+ resolution.hasUnreadyScrollHost = YES;
3651
+ }
3652
+
3653
+ if ([view isKindOfClass:RNCNativeScrollerComponentView.class]) {
3654
+ UIScrollView *scrollView = ((RNCNativeScrollerComponentView *)view).scrollView;
3655
+ if (![_mountingRemovedScrollViews containsObject:scrollView]) {
3656
+ resolution.scrollView = scrollView;
3657
+ resolution.tier = RNCScrollCandidateTierExplicitScroller;
3658
+ return resolution;
3659
+ }
3660
+ } else if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
3661
+ RCTScrollViewComponentView *componentView = (RCTScrollViewComponentView *)view;
3662
+ UIScrollView *scrollView = componentView.scrollView;
3663
+ if (![_mountingRemovedScrollViews containsObject:scrollView]) {
3664
+ if ([componentView.nativeId hasPrefix:RNCCollapsiblePagerNativeScrollerIDPrefix]) {
3665
+ resolution.scrollView = scrollView;
3666
+ resolution.tier = RNCScrollCandidateTierExplicitScroller;
3667
+ return resolution;
3668
+ }
3669
+ if (!scrollView.alwaysBounceHorizontal) {
3670
+ resolution.scrollView = scrollView;
3671
+ resolution.tier = RNCScrollCandidateTierFabricScrollView;
3672
+ }
3673
+ }
3674
+ } else if ([view isKindOfClass:UICollectionView.class]) {
3675
+ UICollectionView *collectionView = (UICollectionView *)view;
3676
+ if (![_mountingRemovedScrollViews containsObject:collectionView]) {
3677
+ UICollectionViewLayout *layout = collectionView.collectionViewLayout;
3678
+ BOOL isVerticalLayout = ![layout isKindOfClass:UICollectionViewFlowLayout.class] ||
3679
+ ((UICollectionViewFlowLayout *)layout).scrollDirection == UICollectionViewScrollDirectionVertical;
3680
+ if (isVerticalLayout) {
3681
+ BOOL hasNativeListHost = RNCNativeListComponentForScrollView(collectionView) != nil;
3682
+ BOOL isMountedNativeList = hasNativeListHost && RNCIsMountedNativeListScrollView(collectionView);
3683
+ BOOL hasUsableBounds = CGRectGetWidth(collectionView.bounds) > 0 &&
3684
+ CGRectGetHeight(collectionView.bounds) > 0;
3685
+ if (hasNativeListHost && !isMountedNativeList) {
3686
+ resolution.hasUnreadyScrollHost = YES;
3687
+ } else if (!hasUsableBounds) {
3688
+ resolution.hasUnreadyScrollHost = YES;
3689
+ } else {
3690
+ resolution.scrollView = collectionView;
3691
+ resolution.tier = isMountedNativeList
3692
+ ? RNCScrollCandidateTierVerifiedNativeList
3693
+ : RNCScrollCandidateTierGenericFallback;
3694
+ // React slots inside a verified list may own independent scroll views.
3695
+ if (isMountedNativeList) return resolution;
3696
+ }
3697
+ }
3698
+ }
3699
+ } else if ([view isKindOfClass:UIScrollView.class]) {
3700
+ UIScrollView *scrollView = (UIScrollView *)view;
3701
+ if (![_mountingRemovedScrollViews containsObject:scrollView] &&
3702
+ !scrollView.alwaysBounceHorizontal &&
3703
+ scrollView.contentSize.width <= scrollView.bounds.size.width + 1) {
3704
+ resolution.scrollView = scrollView;
3705
+ resolution.tier = RNCScrollCandidateTierGenericFallback;
3706
+ }
3707
+ }
3708
+
3709
+ for (UIView *subview in view.subviews) {
3710
+ RNCScrollCandidateResolution child = [self resolveScrollCandidateInView:subview];
3711
+ resolution.hasUnreadyScrollHost |= child.hasUnreadyScrollHost;
3712
+ if (child.tier > resolution.tier) {
3713
+ resolution.scrollView = child.scrollView;
3714
+ resolution.tier = child.tier;
3715
+ if (resolution.tier == RNCScrollCandidateTierExplicitScroller) {
3716
+ return resolution;
3717
+ }
3718
+ }
3719
+ }
3720
+ return resolution;
3358
3721
  }
3359
3722
 
3360
3723
  - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view
@@ -3450,6 +3813,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3450
3813
  return nil;
3451
3814
  }
3452
3815
 
3816
+ - (void)scheduleCoalescedScrollReconcile
3817
+ {
3818
+ if (_isBeingRecycled || _scrollReconcileScheduled) return;
3819
+ _scrollReconcileScheduled = YES;
3820
+ NSUInteger generation = _generation;
3821
+ dispatch_async(dispatch_get_main_queue(), ^{
3822
+ self->_scrollReconcileScheduled = NO;
3823
+ if (self->_isBeingRecycled || self->_generation != generation) return;
3824
+ [self prepareAdjacentPageInsets];
3825
+ [self attachScrollObserverForCurrentPage];
3826
+ });
3827
+ }
3828
+
3453
3829
  - (void)scheduleScrollObserverRetryForCurrentPage
3454
3830
  {
3455
3831
  if (_scrollResolveRetryScheduled || _scrollResolveRetryAttempt >= 12 ||
@@ -3502,29 +3878,35 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3502
3878
  _pendingFallbackScrollView = nil;
3503
3879
  _pendingFallbackScrollViewDetectedAt = 0;
3504
3880
  }
3505
- UIScrollView *candidate = [self verticalScrollViewForPageAtIndex:_currentIndex];
3881
+ RNCScrollCandidateResolution resolution = [self resolveScrollCandidateForPageAtIndex:_currentIndex];
3882
+ UIScrollView *candidate = resolution.scrollView;
3506
3883
  if (_nativeSmoothHeaderScrollEnabled && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
3507
3884
  RNCGuardNestedHorizontalPans(_pageControllers[_currentIndex].view, _verticalPagerGesture);
3508
3885
  }
3509
3886
  if (candidate == nil) {
3510
3887
  [self detachScrollObserver];
3511
3888
  [self restoreSharedHeadersToContainer];
3512
- // Stop retrying released lists on unrelated commits without discarding a
3513
- // surviving identity or offset. A relevant insertion resumes recovery.
3514
- if (_needsScrollObserverReattachAfterMount) {
3515
- _needsScrollObserverReattachAfterMount = NO;
3889
+ _needsScrollObserverReattachAfterMount = NO;
3890
+ _pendingFallbackScrollView = nil;
3891
+ _pendingFallbackScrollViewDetectedAt = 0;
3892
+ _awaitingScrollOwnerForCurrentPage = YES;
3893
+ // Only schedule bounded retry when the page has an unready scroll host in
3894
+ // its subtree or a released scroll owner awaiting replacement. Static pages
3895
+ // without a scroll host do not spin retry timers, and late replacements on
3896
+ // any page also attach deterministically via readiness notifications.
3897
+ BOOL hasReleasedOwnerAwaitingReplacement = _releasedPageScrollStates[self.currentPageKey] != nil;
3898
+ if (resolution.hasUnreadyScrollHost || hasReleasedOwnerAwaitingReplacement) {
3899
+ [self scheduleScrollObserverRetryForCurrentPage];
3900
+ } else {
3516
3901
  _scrollResolveRetryToken += 1;
3517
3902
  _scrollResolveRetryScheduled = NO;
3518
3903
  _scrollResolveRetryAttempt = 0;
3519
- _pendingFallbackScrollView = nil;
3520
- _pendingFallbackScrollViewDetectedAt = 0;
3521
- } else {
3522
- [self scheduleScrollObserverRetryForCurrentPage];
3523
3904
  }
3524
3905
  return;
3525
3906
  }
3526
3907
  if ([candidate isKindOfClass:UICollectionView.class] &&
3527
3908
  candidate != _observedScrollView && !RNCIsMountedNativeListScrollView(candidate)) {
3909
+ _awaitingScrollOwnerForCurrentPage = YES;
3528
3910
  if (_pendingFallbackScrollView != candidate) {
3529
3911
  _pendingFallbackScrollView = candidate;
3530
3912
  _pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
@@ -3536,6 +3918,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3536
3918
  return;
3537
3919
  }
3538
3920
  }
3921
+ _awaitingScrollOwnerForCurrentPage = NO;
3539
3922
  _scrollResolveRetryAttempt = 0;
3540
3923
  _scrollResolveRetryPageIndex = _currentIndex;
3541
3924
  _scrollResolveRetryToken += 1;
@@ -3894,8 +4277,105 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3894
4277
  [_managedRefreshControls removeObjectForKey:scrollView];
3895
4278
  if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
3896
4279
  }
3897
- [RNCRefreshControlOffsetState setOffset:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
4280
+ [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
3898
4281
  forControl:current owner:self];
4282
+ if (previous != current) [self logHomeVisualGeometry:@"refresh-bind" scrollView:scrollView];
4283
+ }
4284
+
4285
+ - (void)logHomeVisualGeometry:(NSString *)phase scrollView:(UIScrollView *)scrollView
4286
+ {
4287
+ #if DEBUG
4288
+ if (self.window == nil || _isBeingRecycled) return;
4289
+ CFTimeInterval now = CACurrentMediaTime();
4290
+ if (_homeVisualStartMono == 0) _homeVisualStartMono = now;
4291
+ if ([phase isEqualToString:@"pull"] && now - _homeVisualLastScrollLog < 0.2) return;
4292
+ if ([phase isEqualToString:@"pull"]) _homeVisualLastScrollLog = now;
4293
+ UIRefreshControl *control = scrollView.refreshControl;
4294
+ CGRect controlWindow = control != nil
4295
+ ? [control convertRect:control.bounds toView:self.window] : CGRectZero;
4296
+ CGRect overlayWindow = [_topOverlayHostView convertRect:_topOverlayHostView.bounds
4297
+ toView:self.window];
4298
+ CGRect stickyWindow = [_stickyHeaderView convertRect:_stickyHeaderView.bounds
4299
+ toView:self.window];
4300
+ CGRect maskWindow = _sharedHeaderMaskView != nil
4301
+ ? [_sharedHeaderHostView convertRect:_sharedHeaderMaskView.frame toView:self.window]
4302
+ : CGRectZero;
4303
+ CGFloat pageMaskWindowY = [_containerView convertPoint:
4304
+ CGPointMake(0, _pageMaskBoundary) toView:self.window].y;
4305
+ BOOL layoutPhase = [phase isEqualToString:@"layout"] ||
4306
+ [phase isEqualToString:@"did-mount"];
4307
+ NSString *signature = [NSString stringWithFormat:
4308
+ @"%.1f:%ld:%ld:%.1f:%d:%.1f:%.1f:%.1f:%d:%d",
4309
+ self.effectiveTopOverlayHeight, (long)_headerHeight,
4310
+ (long)_stickyHeaderHeight, scrollView.contentInset.top, control != nil,
4311
+ CGRectGetMinY(maskWindow), CGRectGetHeight(maskWindow), _pageMaskBoundary,
4312
+ _stickyHeaderIsPinned, _topScrollEdgeEnabled];
4313
+ if (layoutPhase && now - _homeVisualStartMono >= 2.0 &&
4314
+ [_homeVisualLastGeometrySignature isEqualToString:signature] &&
4315
+ now - _homeVisualLastLayoutLog < 2.0) return;
4316
+ if (layoutPhase) {
4317
+ _homeVisualLastLayoutLog = now;
4318
+ _homeVisualLastGeometrySignature = signature;
4319
+ }
4320
+ CGFloat visualYEstimate = CGRectGetMinY(controlWindow) +
4321
+ (control != nil ? control.layer.sublayerTransform.m42 : 0);
4322
+ CALayer *firstChild = control.layer.sublayers.firstObject;
4323
+ BOOL childAttached = firstChild != nil && control.window == self.window;
4324
+ CGRect firstChildWindow = childAttached
4325
+ ? [firstChild convertRect:firstChild.bounds toLayer:self.window.layer]
4326
+ : CGRectZero;
4327
+ CGFloat overscroll = scrollView != nil
4328
+ ? -(scrollView.contentOffset.y + scrollView.contentInset.top) : 0;
4329
+ double epochMs = [NSDate.date timeIntervalSince1970] * 1000;
4330
+ double monoMs = now * 1000;
4331
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4332
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=%@ pagerTag=%ld generation=%lu pageIndex=%ld window=%d edge=%d overlay=%.1f header=%ld sticky=%ld collapse=%.1f scrollY=%.1f insetTop=%.1f overscroll=%.1f refresh=%d refreshing=%d controlY=%.1f controlH=%.1f sublayerY=%.1f visualYEstimate=%.1f overlayY=%.1f overlayBottom=%.1f stickyY=%.1f stickyBottom=%.1f maskY=%.1f maskH=%.1f pageMaskWindowY=%.1f",
4333
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4334
+ (long)_currentIndex, self.window != nil, _topScrollEdgeEnabled,
4335
+ self.effectiveTopOverlayHeight, (long)_headerHeight, (long)_stickyHeaderHeight,
4336
+ _headerOffset, scrollView.contentOffset.y, scrollView.contentInset.top,
4337
+ overscroll, control != nil, control.isRefreshing,
4338
+ CGRectGetMinY(controlWindow), CGRectGetHeight(controlWindow),
4339
+ control != nil ? control.layer.sublayerTransform.m42 : 0, visualYEstimate,
4340
+ CGRectGetMinY(overlayWindow), CGRectGetMaxY(overlayWindow),
4341
+ CGRectGetMinY(stickyWindow), CGRectGetMaxY(stickyWindow),
4342
+ CGRectGetMinY(maskWindow), CGRectGetHeight(maskWindow), pageMaskWindowY]);
4343
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4344
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=%@-refresh-layer pagerTag=%ld generation=%lu pageIndex=%ld boundsY=%.1f boundsH=%.1f centerY=%.1f transformTY=%.1f layerPositionY=%.1f layerTransformY=%.1f directChildCount=%lu firstChildAttached=%d firstChildWindowY=%.1f firstChildWindowH=%.1f",
4345
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4346
+ (long)_currentIndex, control.bounds.origin.y, control.bounds.size.height,
4347
+ control.center.y, control.transform.ty, control.layer.position.y,
4348
+ control.layer.transform.m42, (unsigned long)control.layer.sublayers.count,
4349
+ childAttached, CGRectGetMinY(firstChildWindow), CGRectGetHeight(firstChildWindow)]);
4350
+ if (![phase isEqualToString:@"pull"]) {
4351
+ CALayer *topCover = _topEdgeSeparatorCover;
4352
+ CALayer *stickyCover = _stickyEdgeSeparatorCover;
4353
+ UIColor *color = topCover.backgroundColor != nil
4354
+ ? [UIColor colorWithCGColor:topCover.backgroundColor] : nil;
4355
+ CGFloat red = 0, green = 0, blue = 0, alpha = 0;
4356
+ [color getRed:&red green:&green blue:&blue alpha:&alpha];
4357
+ CGRect topCoverWindow = topCover.superlayer != nil
4358
+ ? [topCover convertRect:topCover.bounds toLayer:self.window.layer] : CGRectZero;
4359
+ CGRect stickyCoverWindow = stickyCover.superlayer != nil
4360
+ ? [stickyCover convertRect:stickyCover.bounds toLayer:self.window.layer] : CGRectZero;
4361
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4362
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=%@-edge pagerTag=%ld generation=%lu pageIndex=%ld trait=%ld override=%ld style=%ld pinned=%d topHostY=%.1f topHostBottom=%.1f stickyHostY=%.1f stickyHostBottom=%.1f topInteractionSameScroll=%d stickyInteractionSameScroll=%d topCover=%d topCoverHidden=%d topCoverY=%.1f topCoverH=%.3f stickyCover=%d stickyCoverHidden=%d stickyCoverY=%.1f stickyCoverH=%.3f coverRGBA=%.3f,%.3f,%.3f,%.3f scale=%.1f",
4363
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4364
+ (long)_currentIndex, (long)self.traitCollection.userInterfaceStyle,
4365
+ (long)_topOverlayHostView.overrideUserInterfaceStyle,
4366
+ (long)_topScrollEdgeEffect, _stickyHeaderIsPinned,
4367
+ CGRectGetMinY(overlayWindow), CGRectGetMaxY(overlayWindow),
4368
+ CGRectGetMinY(stickyWindow), CGRectGetMaxY(stickyWindow),
4369
+ _topEdgeInteraction.scrollView == scrollView,
4370
+ _stickyEdgeInteraction.scrollView == scrollView,
4371
+ topCover != nil, topCover.hidden, CGRectGetMinY(topCoverWindow),
4372
+ CGRectGetHeight(topCoverWindow), stickyCover != nil,
4373
+ stickyCover.hidden,
4374
+ CGRectGetMinY(stickyCoverWindow),
4375
+ CGRectGetHeight(stickyCoverWindow), red, green, blue, alpha,
4376
+ self.window.screen.scale]);
4377
+ }
4378
+ #endif
3899
4379
  }
3900
4380
 
3901
4381
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
@@ -4189,8 +4669,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4189
4669
 
4190
4670
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
4191
4671
  {
4192
- if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
4193
- [(RNCNativeScrollerComponentView *)scrollView.superview setPagerStickyHeight:height forOwner:self];
4672
+ id host = scrollView.superview;
4673
+ if ([host respondsToSelector:@selector(setPagerStickyHeight:forOwner:)]) {
4674
+ [host setPagerStickyHeight:height forOwner:self];
4194
4675
  }
4195
4676
  }
4196
4677
 
@@ -4262,6 +4743,66 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4262
4743
  }
4263
4744
 
4264
4745
 
4746
+ - (void)nativeScrollViewDidBecomeReadyNotification:(NSNotification *)notification
4747
+ {
4748
+ if ([notification.object isKindOfClass:UIScrollView.class]) {
4749
+ [self nativeScrollViewDidBecomeReady:(UIScrollView *)notification.object];
4750
+ }
4751
+ }
4752
+
4753
+ - (void)nativeScrollViewDidBecomeReady:(UIScrollView *)scrollView
4754
+ {
4755
+ if (_isBeingRecycled || self.window == nil || scrollView == nil ||
4756
+ [_mountingRemovedScrollViews containsObject:scrollView]) return;
4757
+ for (UIView *ancestor = scrollView.superview;
4758
+ ancestor != nil && ancestor != self;
4759
+ ancestor = ancestor.superview) {
4760
+ if ([ancestor isKindOfClass:RNCCollapsiblePagerViewComponentView.class]) {
4761
+ RNCCollapsiblePagerViewComponentView *nestedPager =
4762
+ (RNCCollapsiblePagerViewComponentView *)ancestor;
4763
+ if ([nestedPager currentVerticalScrollViewForParentPager] != scrollView) return;
4764
+ break;
4765
+ }
4766
+ }
4767
+ NSInteger pageIndex = NSNotFound;
4768
+ for (NSInteger index = 0; index < _pageControllers.count; index++) {
4769
+ if ([scrollView isDescendantOfView:_pageControllers[index].view]) {
4770
+ pageIndex = index;
4771
+ break;
4772
+ }
4773
+ }
4774
+ if (pageIndex == NSNotFound) return;
4775
+
4776
+ NSString *pageKey = [self pageKeyForIndex:pageIndex];
4777
+ BOOL hasReleasedStateForPage = _releasedPageScrollStates[pageKey] != nil;
4778
+ BOOL currentPageNeedsOwner = pageIndex == _currentIndex &&
4779
+ (_awaitingScrollOwnerForCurrentPage || _needsScrollObserverReattachAfterMount ||
4780
+ hasReleasedStateForPage ||
4781
+ (_observedScrollView != nil &&
4782
+ [_observedScrollView isKindOfClass:UICollectionView.class] &&
4783
+ !RNCIsMountedNativeListScrollView(_observedScrollView) &&
4784
+ _observedScrollView != scrollView));
4785
+
4786
+ if (!hasReleasedStateForPage && !currentPageNeedsOwner &&
4787
+ !_needsScrollObserverReattachAfterMount) return;
4788
+
4789
+ if (_nativeSmoothHeaderScrollEnabled) {
4790
+ NSInteger first = MAX(0, _currentIndex - 1);
4791
+ NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
4792
+ BOOL isOwnedScroller = [scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class] &&
4793
+ ((RNCNativeScrollerComponentView *)scrollView.superview).scrollView == scrollView;
4794
+ if ((pageIndex >= first && pageIndex <= last || pageIndex == _destinationIndex) &&
4795
+ (RNCIsMountedNativeListScrollView(scrollView) || isOwnedScroller) &&
4796
+ [_originalInsets objectForKey:scrollView] == nil) {
4797
+ [self applyInsetsToScrollView:scrollView pageIndex:pageIndex restore:YES];
4798
+ }
4799
+ }
4800
+
4801
+ if (currentPageNeedsOwner && !_transitioning && !_isPagerDragging) {
4802
+ [self attachScrollObserverForCurrentPage];
4803
+ }
4804
+ }
4805
+
4265
4806
  - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
4266
4807
  callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next
4267
4808
  {
@@ -4313,6 +4854,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4313
4854
  if (context == RNCScrollRefreshControlContext) {
4314
4855
  UIScrollView *scrollView = (UIScrollView *)object;
4315
4856
  [self updateRefreshControlForScrollView:scrollView];
4857
+ [self logHomeVisualGeometry:@"refresh-control-change" scrollView:scrollView];
4316
4858
  if (scrollView == _observedScrollView && !_syntheticPageRestore &&
4317
4859
  (!_nativeSmoothHeaderScrollEnabled || (!_transitioning && _transitionPageOffsets == nil))) {
4318
4860
  [self updateHeaderForScrollView:scrollView];
@@ -4320,6 +4862,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4320
4862
  return;
4321
4863
  }
4322
4864
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
4865
+ if ([keyPath isEqualToString:@"contentOffset"] &&
4866
+ -(((UIScrollView *)object).contentOffset.y +
4867
+ ((UIScrollView *)object).contentInset.top) > 5) {
4868
+ [self logHomeVisualGeometry:@"pull" scrollView:(UIScrollView *)object];
4869
+ }
4323
4870
  if (_pendingTopInsetReboundScrollView == object &&
4324
4871
  ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
4325
4872
  if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
@@ -4978,6 +5525,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4978
5525
  {
4979
5526
  UIScrollView *scrollView = _pagerTopScrollView;
4980
5527
  if (_dispatchingPagerTop || ![self matchesPagerTop:scrollView]) return;
5528
+ CGFloat clampedTarget = MIN(MAX(_pagerTopLogicalTarget, 0), _headerHeight);
5529
+ if (clampedTarget != _pagerTopLogicalTarget) {
5530
+ _pagerTopLogicalTarget = clampedTarget;
5531
+ CGFloat physicalTarget = RNCLimitRestoredScrollOffset(clampedTarget - scrollView.contentInset.top,
5532
+ scrollView.contentSize.height, scrollView.bounds.size.height, scrollView.contentInset.top, scrollView.contentInset.bottom);
5533
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:NO];
5534
+ }
4981
5535
  CGFloat logical = scrollView.contentOffset.y + scrollView.contentInset.top;
4982
5536
  if (fabs(logical - _pagerTopLogicalTarget) > 0.5) return;
4983
5537
  _pageOffsets[_pagerTopPageKey] = @(_nativeSmoothHeaderScrollEnabled ? 0 : _pagerTopLogicalTarget);
@@ -4989,11 +5543,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4989
5543
  }
4990
5544
 
4991
5545
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
5546
+ {
5547
+ if (target != 0 && target != 1) return;
5548
+ [self scrollHeaderToOffset:(target == 1 ? _headerHeight : 0) animated:animated];
5549
+ }
5550
+
5551
+ - (void)scrollHeaderBy:(double)delta
5552
+ {
5553
+ if (!std::isfinite(delta)) return;
5554
+ [self scrollHeaderToOffset:MIN(MAX(_headerOffset + delta, 0), _headerHeight) animated:NO];
5555
+ }
5556
+
5557
+ - (void)scrollHeaderToOffset:(double)offset
5558
+ {
5559
+ if (!std::isfinite(offset)) return;
5560
+ [self scrollHeaderToOffset:MIN(MAX(offset, 0), _headerHeight) animated:NO];
5561
+ }
5562
+
5563
+ - (void)scrollHeaderToOffset:(CGFloat)logicalTarget animated:(BOOL)animated
4992
5564
  {
4993
5565
  [self cancelTopInsetReboundCheck];
4994
5566
  UIScrollView *scrollView = _observedScrollView;
4995
5567
  NSString *key = _observedPageKey;
4996
- if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
5568
+ if (_isBeingRecycled || _transitioning || _isPagerDragging ||
4997
5569
  _transitionPageOffsets != nil || _syntheticPageRestore || scrollView == nil || scrollView.window == nil ||
4998
5570
  key == nil || ![key isEqualToString:self.currentPageKey] || _observedPageIndex != _currentIndex ||
4999
5571
  _observedPageGeneration != _generation || self.window == nil) return;
@@ -5009,7 +5581,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5009
5581
  [_alignedBodyKeys removeObject:key];
5010
5582
  [_suspendedBodyKeys removeObject:key];
5011
5583
  [_displayBodyOverrides removeObjectForKey:key];
5012
- CGFloat logicalTarget = target == 1 ? _headerHeight : 0;
5584
+ logicalTarget = MIN(MAX(logicalTarget, 0), _headerHeight);
5013
5585
  _pageOffsets[key] = @(_nativeSmoothHeaderScrollEnabled ? 0 : logicalTarget);
5014
5586
  _pagerTopScrollView = scrollView;
5015
5587
  _pagerTopPageKey = [key copy];