@onekeyfe/react-native-pager-view 6.7.0-alpha.324 → 6.7.0-alpha.335

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 (43) hide show
  1. package/README.md +9 -0
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +2 -2
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +2 -2
  4. package/docs/SPEC.md +107 -6
  5. package/ios/RNCCollapsiblePagerViewComponentView.mm +341 -23
  6. package/ios/RNCNativeScrollerComponentView.mm +30 -5
  7. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +124 -0
  8. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +186 -1
  9. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
  10. package/ios/tests/RNCNativeScrollerTests.mm +39 -0
  11. package/lib/module/CollapsiblePagerView.js +16 -7
  12. package/lib/module/CollapsiblePagerView.web.js +428 -37
  13. package/lib/module/CollapsiblePagerViewNativeComponent.ts +1 -1
  14. package/lib/module/NativeScroller.js +2 -2
  15. package/lib/module/NativeScroller.web.js +16 -3
  16. package/lib/module/NativeScrollerNativeComponent.ts +1 -1
  17. package/lib/module/ScrollEdgeEffect.js +8 -0
  18. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  19. package/lib/module/index.js +3 -0
  20. package/lib/module/index.native.js +9 -0
  21. package/lib/module/index.web.js +3 -0
  22. package/lib/typescript/src/CollapsiblePagerView.d.ts +8 -5
  23. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -4
  24. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -1
  25. package/lib/typescript/src/NativeScroller.d.ts +3 -2
  26. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +1 -1
  27. package/lib/typescript/src/ScrollEdgeEffect.d.ts +5 -0
  28. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  29. package/lib/typescript/src/index.d.ts +5 -0
  30. package/lib/typescript/src/index.native.d.ts +13 -0
  31. package/lib/typescript/src/index.web.d.ts +5 -0
  32. package/package.json +6 -4
  33. package/src/CollapsiblePagerView.tsx +43 -11
  34. package/src/CollapsiblePagerView.web.tsx +640 -50
  35. package/src/CollapsiblePagerViewNativeComponent.ts +1 -1
  36. package/src/NativeScroller.tsx +6 -4
  37. package/src/NativeScroller.web.tsx +24 -2
  38. package/src/NativeScrollerNativeComponent.ts +1 -1
  39. package/src/ScrollEdgeEffect.ts +4 -0
  40. package/src/WebCollapsiblePagerPage.tsx +258 -0
  41. package/src/index.native.tsx +30 -0
  42. package/src/index.tsx +6 -0
  43. package/src/index.web.tsx +6 -0
@@ -25,6 +25,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
25
25
  static void *RNCRefreshControlLayerContext = &RNCRefreshControlLayerContext;
26
26
  static void *RNCScrollRefreshControlContext = &RNCScrollRefreshControlContext;
27
27
  static char RNCRefreshControlOffsetKey;
28
+ static void RNCCollapsiblePagerLog(NSString *message);
28
29
 
29
30
  @interface RNCRefreshControlOffsetState : NSObject
30
31
  + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner;
@@ -39,6 +40,10 @@ static char RNCRefreshControlOffsetKey;
39
40
  BOOL _originalMasksToBounds;
40
41
  BOOL _applying;
41
42
  BOOL _observing;
43
+ CGFloat _lastLoggedOffset;
44
+ NSUInteger _lastLoggedOwnerCount;
45
+ CGFloat _lastLoggedTransformY;
46
+ BOOL _hasLoggedState;
42
47
  }
43
48
 
44
49
  - (instancetype)initWithControl:(UIRefreshControl *)control
@@ -79,6 +84,21 @@ static char RNCRefreshControlOffsetKey;
79
84
  }
80
85
  [CATransaction commit];
81
86
  _applying = NO;
87
+ #if DEBUG
88
+ if (!_hasLoggedState || fabs(_lastLoggedOffset - offset) > 0.01 ||
89
+ _lastLoggedOwnerCount != _offsets.count ||
90
+ fabs(_lastLoggedTransformY - transform.m42) > 0.01) {
91
+ _hasLoggedState = YES;
92
+ _lastLoggedOffset = offset;
93
+ _lastLoggedOwnerCount = _offsets.count;
94
+ _lastLoggedTransformY = transform.m42;
95
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
96
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=refresh-transform ownerCount=%lu offsetSum=%.1f baselineY=%.1f appliedY=%.1f masksToBounds=%d",
97
+ [NSDate.date timeIntervalSince1970] * 1000, CACurrentMediaTime() * 1000,
98
+ (unsigned long)_offsets.count, offset, _baselineTransform.m42,
99
+ transform.m42, control.layer.masksToBounds]);
100
+ }
101
+ #endif
82
102
  }
83
103
 
84
104
  + (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner
@@ -1464,9 +1484,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1464
1484
  - (void)detachSharedHeaderPressCancellationGesture;
1465
1485
  - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
1466
1486
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
1487
+ - (void)logHomeVisualGeometry:(NSString *)phase scrollView:(UIScrollView *)scrollView;
1467
1488
  - (void)cancelTopInsetReboundCheck;
1468
1489
  - (void)scheduleTopInsetReboundCheck;
1469
1490
  - (void)syncScrollEdgeAppearance;
1491
+ - (void)updateHardEdgeSeparatorCovers;
1492
+ - (void)updateTopOverlayThemeBackground;
1470
1493
  - (void)updateScrollEdgeContentMasks;
1471
1494
  - (void)restoreScrollEdgeContentMasks;
1472
1495
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
@@ -1503,6 +1526,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1503
1526
  UIScrollEdgeElementContainerInteraction *_stickyEdgeInteraction;
1504
1527
  UILabel *_topEdgeMarker;
1505
1528
  UILabel *_stickyEdgeMarker;
1529
+ CALayer *_topEdgeSeparatorCover;
1530
+ CALayer *_stickyEdgeSeparatorCover;
1531
+ CALayer *_topOverlayThemeBackgroundLayer;
1506
1532
  __weak UIView *_topEdgeInteractionHost;
1507
1533
  __weak UIView *_stickyEdgeInteractionHost;
1508
1534
  __weak UIScrollView *_topEdgeScrollView;
@@ -1521,7 +1547,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1521
1547
  __weak RNCPagerViewComponentView *_externalOuterPager;
1522
1548
  BOOL _externalOuterPageActive;
1523
1549
  BOOL _externalOuterTransitioning;
1524
- BOOL _topScrollEdgeHard;
1550
+ BOOL _topScrollEdgeEnabled;
1551
+ RNCCollapsiblePagerViewTopScrollEdgeEffect _topScrollEdgeEffect;
1525
1552
  NSString *_topOverlayColorScheme;
1526
1553
  BOOL _pendingTopOverlayAppearanceCommit;
1527
1554
  BOOL _applyingTopOverlayInsetChange;
@@ -1572,6 +1599,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1572
1599
  NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
1573
1600
  NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
1574
1601
  NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
1602
+ CFTimeInterval _homeVisualLastScrollLog;
1603
+ CFTimeInterval _homeVisualLastLayoutLog;
1604
+ CFTimeInterval _homeVisualStartMono;
1605
+ NSString *_homeVisualLastGeometrySignature;
1606
+ CGFloat _homeVisualLastThemeOpacity;
1607
+ CFTimeInterval _homeVisualLastThemeLog;
1608
+ BOOL _homeVisualThemeHolding;
1575
1609
  NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
1576
1610
  NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
1577
1611
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
@@ -1799,6 +1833,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1799
1833
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
1800
1834
  [self updateSharedHeaderPressCancellationGesture];
1801
1835
  [self updateHeaderSettledStateForceBusy:NO];
1836
+ [self logHomeVisualGeometry:@"window" scrollView:_observedScrollView];
1802
1837
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1803
1838
  [self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight + self.effectiveTopOverlayHeight];
1804
1839
  }
@@ -1826,7 +1861,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1826
1861
 
1827
1862
  - (CGFloat)effectiveTopOverlayHeight
1828
1863
  {
1829
- return _topOverlayView != nil ? _topOverlayHeight : _externalTopOverlayHeight;
1864
+ BOOL localOverlayPending = _topScrollEdgeEnabled && _topOverlayHeight > 0 &&
1865
+ _externalTopOverlayTag <= 0;
1866
+ return _topOverlayView != nil || localOverlayPending
1867
+ ? _topOverlayHeight : _externalTopOverlayHeight;
1830
1868
  }
1831
1869
 
1832
1870
  - (BOOL)hasFixedTopOverlay
@@ -1909,12 +1947,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1909
1947
  [super traitCollectionDidChange:previousTraitCollection];
1910
1948
  if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
1911
1949
  [self syncScrollEdgeAppearance];
1950
+ [self updateTopOverlayThemeBackground];
1951
+ [self logHomeVisualGeometry:@"trait" scrollView:_observedScrollView];
1912
1952
  }
1913
1953
  }
1914
1954
 
1915
1955
  - (void)syncScrollEdgeAppearance
1916
1956
  {
1917
- BOOL active = _topScrollEdgeHard &&
1957
+ BOOL active = _topScrollEdgeEnabled &&
1918
1958
  (self.hasFixedTopOverlay || _stickyHeaderView != nil) && self.window != nil &&
1919
1959
  !_isBeingRecycled;
1920
1960
  UIUserInterfaceStyle style = UIUserInterfaceStyleUnspecified;
@@ -1952,11 +1992,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1952
1992
 
1953
1993
  - (void)updateScrollEdgeContentMasks
1954
1994
  {
1955
- BOOL enabled = _topScrollEdgeHard &&
1995
+ [self updateHardEdgeSeparatorCovers];
1996
+ BOOL enabled = _topScrollEdgeEnabled &&
1956
1997
  (self.hasFixedTopOverlay || _stickyHeaderView != nil) &&
1957
1998
  self.window != nil && !_isBeingRecycled;
1958
1999
  if (!enabled) {
1959
2000
  [self restoreScrollEdgeContentMasks];
2001
+ [self updateTopOverlayThemeBackground];
1960
2002
  return;
1961
2003
  }
1962
2004
  if (self.hasFixedTopOverlay && _sharedHeaderHostView.window == self.window &&
@@ -2026,6 +2068,102 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2026
2068
  [_ownedPageMasks removeObjectForKey:page];
2027
2069
  [_originalPageMasks removeObjectForKey:page];
2028
2070
  }
2071
+ [self updateTopOverlayThemeBackground];
2072
+ }
2073
+
2074
+ - (void)updateTopOverlayThemeBackground
2075
+ {
2076
+ if (@available(iOS 27.0, *)) {
2077
+ BOOL localHardOverlay = !_isBeingRecycled && _topScrollEdgeEnabled &&
2078
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2079
+ _nativeSmoothHeaderScrollEnabled && _topOverlayView != nil &&
2080
+ _externalTopOverlayTag <= 0 && _topOverlayHeight > 0 && _headerHeight > 0 &&
2081
+ _topOverlayHostView.window == self.window && self.window != nil &&
2082
+ !CGRectIsEmpty(_topOverlayHostView.bounds);
2083
+ if (!localHardOverlay) {
2084
+ [_topOverlayThemeBackgroundLayer removeFromSuperlayer];
2085
+ _topOverlayThemeBackgroundLayer = nil;
2086
+ _homeVisualThemeHolding = NO;
2087
+ return;
2088
+ }
2089
+
2090
+ UIColor *color = nil;
2091
+ for (UIView *view = self; view != nil; view = view.superview) {
2092
+ UIColor *candidate = [view.backgroundColor resolvedColorWithTraitCollection:
2093
+ _topOverlayHostView.traitCollection];
2094
+ if (candidate != nil && CGColorGetAlpha(candidate.CGColor) >= 1) {
2095
+ color = candidate;
2096
+ break;
2097
+ }
2098
+ }
2099
+ if (color == nil) {
2100
+ [_topOverlayThemeBackgroundLayer removeFromSuperlayer];
2101
+ _topOverlayThemeBackgroundLayer = nil;
2102
+ return;
2103
+ }
2104
+
2105
+ UIScrollView *scrollView = _observedScrollView;
2106
+ BOOL geometryReady = scrollView != nil &&
2107
+ _sharedHeaderScrollView == scrollView && _topEdgeScrollView == scrollView &&
2108
+ _topEdgeInteraction.scrollView == scrollView &&
2109
+ scrollView.topEdgeEffect.style == UIScrollEdgeEffectStyle.hardStyle &&
2110
+ _sharedHeaderMaskView != nil &&
2111
+ _sharedHeaderHostView.maskView == _sharedHeaderMaskView &&
2112
+ _headerView.window == self.window;
2113
+ if (_topOverlayThemeBackgroundLayer == nil && !geometryReady) return;
2114
+ BOOL created = _topOverlayThemeBackgroundLayer == nil;
2115
+ if (created) {
2116
+ _topOverlayThemeBackgroundLayer = [CALayer layer];
2117
+ _topOverlayThemeBackgroundLayer.name = @"onekey-top-overlay-theme-background";
2118
+ _topOverlayThemeBackgroundLayer.actions = @{
2119
+ @"position": NSNull.null, @"bounds": NSNull.null,
2120
+ @"opacity": NSNull.null, @"backgroundColor": NSNull.null
2121
+ };
2122
+ [_topOverlayHostView.layer insertSublayer:_topOverlayThemeBackgroundLayer atIndex:0];
2123
+ }
2124
+ _topOverlayThemeBackgroundLayer.frame = _topOverlayHostView.bounds;
2125
+ _topOverlayThemeBackgroundLayer.backgroundColor = color.CGColor;
2126
+ if (!geometryReady) {
2127
+ if (!_homeVisualThemeHolding) {
2128
+ _homeVisualThemeHolding = YES;
2129
+ #if DEBUG
2130
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2131
+ @"[IOS-HOME-VISUAL] phase=theme-background-hold pagerTag=%ld generation=%lu pageIndex=%ld opacity=%.3f",
2132
+ (long)self.tag, (unsigned long)_generation, (long)_currentIndex,
2133
+ _topOverlayThemeBackgroundLayer.opacity]);
2134
+ #endif
2135
+ }
2136
+ return;
2137
+ }
2138
+ _homeVisualThemeHolding = NO;
2139
+ CGRect overlayWindow = [_topOverlayHostView convertRect:_topOverlayHostView.bounds
2140
+ toView:self.window];
2141
+ CGRect headerWindow = [_headerView convertRect:_headerView.bounds
2142
+ toView:self.window];
2143
+ CGFloat overlayBottom = CGRectGetMaxY(overlayWindow);
2144
+ CGFloat collapse = MIN(MAX(overlayBottom - CGRectGetMinY(headerWindow), 0),
2145
+ _headerHeight);
2146
+ CGFloat opacity = 1 - collapse / _headerHeight;
2147
+ _topOverlayThemeBackgroundLayer.opacity = opacity;
2148
+ #if DEBUG
2149
+ CFTimeInterval now = CACurrentMediaTime();
2150
+ if (created || fabs(opacity - _homeVisualLastThemeOpacity) >= 0.1 ||
2151
+ now - _homeVisualLastThemeLog >= 2.0) {
2152
+ _homeVisualLastThemeOpacity = opacity;
2153
+ _homeVisualLastThemeLog = now;
2154
+ CGFloat red = 0, green = 0, blue = 0, alpha = 0;
2155
+ [color getRed:&red green:&green blue:&blue alpha:&alpha];
2156
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2157
+ @"[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",
2158
+ NSDate.date.timeIntervalSince1970 * 1000, now * 1000,
2159
+ (long)self.tag, (unsigned long)_generation, (long)_currentIndex,
2160
+ opacity, collapse, (CGFloat)_headerHeight, overlayBottom,
2161
+ CGRectGetMinY(headerWindow), overlayBottom + _headerHeight +
2162
+ _stickyHeaderHeight - collapse, _topEdgeInteraction.scrollView == scrollView,
2163
+ red, green, blue, alpha]);
2164
+ }
2165
+ #endif
2166
+ }
2029
2167
  }
2030
2168
 
2031
2169
  - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
@@ -2052,9 +2190,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2052
2190
  BOOL externalOwnerReady = _externalTopOverlayTag <= 0 ||
2053
2191
  _externalOuterPager == nil ||
2054
2192
  (_externalOuterPageActive && !_externalOuterTransitioning);
2055
- BOOL hasOverlay = _topScrollEdgeHard && self.hasFixedTopOverlay &&
2193
+ BOOL hasOverlay = _topScrollEdgeEnabled && self.hasFixedTopOverlay &&
2056
2194
  externalOwnerReady && self.window != nil;
2057
- BOOL hasSticky = _topScrollEdgeHard && _stickyHeaderView != nil &&
2195
+ BOOL hasSticky = _topScrollEdgeEnabled && _stickyHeaderView != nil &&
2058
2196
  externalOwnerReady && self.window != nil;
2059
2197
  BOOL enabled = (hasOverlay || hasSticky) && scrollView != nil;
2060
2198
  BOOL topHostChanged = (_topEdgeInteraction != nil) != hasOverlay ||
@@ -2080,7 +2218,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2080
2218
  if (enabled && _topEdgeScrollView == nil) {
2081
2219
  _originalTopEdgeStyle = scrollView.topEdgeEffect.style;
2082
2220
  _topEdgeScrollView = scrollView;
2083
- scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
2221
+ }
2222
+ if (enabled) {
2223
+ UIScrollEdgeEffectStyle *style =
2224
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Soft
2225
+ ? UIScrollEdgeEffectStyle.softStyle : UIScrollEdgeEffectStyle.hardStyle;
2226
+ if (scrollView.topEdgeEffect.style != style) scrollView.topEdgeEffect.style = style;
2084
2227
  }
2085
2228
  if (hasOverlay && enabled && _topEdgeInteraction == nil) {
2086
2229
  // UIKit needs a native element to shape the edge effect around React-drawn content.
@@ -2135,6 +2278,55 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2135
2278
  }
2136
2279
  }
2137
2280
 
2281
+ - (void)updateHardEdgeSeparatorCovers
2282
+ {
2283
+ BOOL enabled = _topScrollEdgeEnabled &&
2284
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2285
+ _topEdgeScrollView != nil && self.window != nil && !_isBeingRecycled;
2286
+ if (!enabled && _topEdgeSeparatorCover == nil && _stickyEdgeSeparatorCover == nil) return;
2287
+ UIColor *color = nil;
2288
+ for (UIView *view = self; view != nil; view = view.superview) {
2289
+ UIColor *candidate = [view.backgroundColor resolvedColorWithTraitCollection:
2290
+ _topOverlayHostView.traitCollection];
2291
+ if (candidate != nil && CGColorGetAlpha(candidate.CGColor) >= 1) {
2292
+ color = candidate;
2293
+ break;
2294
+ }
2295
+ }
2296
+ color = color ?: [UIColor.systemBackgroundColor resolvedColorWithTraitCollection:
2297
+ _topOverlayHostView.traitCollection];
2298
+ CGFloat scale = self.window.screen.scale ?: self.traitCollection.displayScale;
2299
+ CGFloat pixel = 1.0 / MAX(1, scale);
2300
+ UIView *hosts[] = {_topEdgeInteractionHost, _stickyEdgeInteractionHost};
2301
+ CALayer *covers[] = {_topEdgeSeparatorCover, _stickyEdgeSeparatorCover};
2302
+ for (NSInteger index = 0; index < 2; index++) {
2303
+ UIView *host = hosts[index];
2304
+ CALayer *cover = covers[index];
2305
+ if (!enabled || host == nil || host.hidden || CGRectIsEmpty(host.bounds)) {
2306
+ [cover removeFromSuperlayer];
2307
+ cover = nil;
2308
+ } else {
2309
+ if (cover == nil) {
2310
+ cover = [CALayer layer];
2311
+ cover.name = @"onekey-hard-edge-separator-cover";
2312
+ cover.zPosition = 1;
2313
+ cover.actions = @{@"position": NSNull.null, @"bounds": NSNull.null,
2314
+ @"backgroundColor": NSNull.null};
2315
+ [_containerView.layer addSublayer:cover];
2316
+ }
2317
+ // Cover only the hairline; keep the native Hard material and glass shadows.
2318
+ CGFloat boundary = CGRectGetMaxY([host convertRect:host.bounds toView:_containerView]);
2319
+ cover.frame = CGRectMake(0, round(boundary * scale) / scale - pixel,
2320
+ CGRectGetWidth(_containerView.bounds), pixel);
2321
+ cover.contentsScale = scale;
2322
+ cover.backgroundColor = color.CGColor;
2323
+ cover.hidden = index == 0 && _stickyHeaderIsPinned;
2324
+ }
2325
+ if (index == 0) _topEdgeSeparatorCover = cover;
2326
+ else _stickyEdgeSeparatorCover = cover;
2327
+ }
2328
+ }
2329
+
2138
2330
  - (void)dealloc
2139
2331
  {
2140
2332
  [self updateTopScrollEdgeForScrollView:nil];
@@ -2321,7 +2513,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2321
2513
  if (_sharedHeaderScrollView == scrollView &&
2322
2514
  _sharedHeaderHostView.superview == scrollView) {
2323
2515
  _sharedHeaderHostView.layer.zPosition =
2324
- _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2516
+ _topScrollEdgeEnabled && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2325
2517
  [self layoutSharedHeaders];
2326
2518
  return;
2327
2519
  }
@@ -2329,7 +2521,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2329
2521
  _sharedHeaderScrollView = scrollView;
2330
2522
  _sharedHeadersLiftedForPagerTransition = NO;
2331
2523
  _sharedHeaderHostView.layer.zPosition =
2332
- _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2524
+ _topScrollEdgeEnabled && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2333
2525
  if (_sharedHeaderHostView.superview != scrollView) {
2334
2526
  [scrollView addSubview:_sharedHeaderHostView];
2335
2527
  }
@@ -2424,7 +2616,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2424
2616
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
2425
2617
  CGFloat stickyTop = [_sharedHeaderHostView convertPoint:
2426
2618
  CGPointMake(0, _headerHeight + pinnedTranslation) toView:self].y;
2427
- BOOL shouldPin = _topScrollEdgeHard &&
2619
+ BOOL shouldPin = _topScrollEdgeEnabled &&
2428
2620
  _stickyHeaderView != nil && attachedToList && !_transitioning &&
2429
2621
  !_externalOuterTransitioning &&
2430
2622
  !_isPagerDragging && _headerOffset >= _headerHeight - 0.5 &&
@@ -2480,6 +2672,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2480
2672
  [_nativeTabBarView updateAncestorGesturePrecedence];
2481
2673
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2482
2674
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2675
+ [self logHomeVisualGeometry:shouldPin ? @"pin" : @"unpin"
2676
+ scrollView:_observedScrollView];
2483
2677
  }
2484
2678
 
2485
2679
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
@@ -2631,6 +2825,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2631
2825
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
2632
2826
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
2633
2827
  {
2828
+ if (_needsSlotRebuild && _deferTopEdgeSlotRebuild &&
2829
+ _topScrollEdgeEnabled && _nativeSmoothHeaderScrollEnabled &&
2830
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2831
+ _topOverlayHeight > 0 && _pageKeys.count > 0 &&
2832
+ _externalTopOverlayTag <= 0 &&
2833
+ _logicalChildren.count == _pageKeys.count + 3 &&
2834
+ !_transitioning && !_isPagerDragging && !_isBeingRecycled) {
2835
+ _deferTopEdgeSlotRebuild = NO;
2836
+ }
2634
2837
  if (_needsSlotRebuild && !_deferTopEdgeSlotRebuild &&
2635
2838
  !_transitioning && !_isPagerDragging) {
2636
2839
  [self rebuildSlots];
@@ -2646,13 +2849,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2646
2849
  _topOverlayView.window == self.window;
2647
2850
  BOOL externalOverlayReady = _externalTopOverlayTag > 0 &&
2648
2851
  _externalTopOverlayView.window == self.window;
2649
- if (!_isBeingRecycled && _topScrollEdgeHard && self.window != nil &&
2852
+ if (!_isBeingRecycled && _topScrollEdgeEnabled && self.window != nil &&
2650
2853
  (localOverlayReady || externalOverlayReady)) {
2651
2854
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2652
2855
  }
2653
2856
  }
2654
2857
  [self updateScrollEdgeContentMasks];
2655
2858
  [_mountingRemovedScrollViews removeAllObjects];
2859
+ [self logHomeVisualGeometry:@"did-mount" scrollView:_observedScrollView];
2656
2860
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
2657
2861
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
2658
2862
  // Prepare newly mounted owned scrollers before their first frame, even while
@@ -2729,7 +2933,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2729
2933
  if (_isBeingRecycled) return;
2730
2934
  NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
2731
2935
  [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
2732
- if (_topScrollEdgeHard || _topOverlayView != nil) {
2936
+ if (_topScrollEdgeEnabled || _topOverlayView != nil) {
2733
2937
  _needsSlotRebuild = YES;
2734
2938
  } else {
2735
2939
  [self rebuildSlots];
@@ -2744,7 +2948,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2744
2948
  if (existingIndex == NSNotFound) return;
2745
2949
  [childComponentView removeFromSuperview];
2746
2950
  [_logicalChildren removeObjectAtIndex:existingIndex];
2747
- if (_topScrollEdgeHard || _topOverlayView != nil) {
2951
+ if (_topScrollEdgeEnabled || _topOverlayView != nil) {
2748
2952
  _needsSlotRebuild = YES;
2749
2953
  } else {
2750
2954
  [self rebuildSlots];
@@ -2761,7 +2965,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2761
2965
  _needsSlotRebuild = YES;
2762
2966
  return;
2763
2967
  }
2764
- if (_topScrollEdgeHard &&
2968
+ if (_topScrollEdgeEnabled &&
2765
2969
  _logicalChildren.count != _pageKeys.count + 2 &&
2766
2970
  _logicalChildren.count != _pageKeys.count + 3) {
2767
2971
  _needsSlotRebuild = YES;
@@ -2800,7 +3004,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2800
3004
  _stickyHeaderView = _logicalChildren[1];
2801
3005
  [_sharedHeaderHostView addSubview:_stickyHeaderView];
2802
3006
  }
2803
- BOOL hasOverlaySlot = _topScrollEdgeHard &&
3007
+ BOOL hasOverlaySlot = _topScrollEdgeEnabled &&
2804
3008
  _logicalChildren.count == _pageKeys.count + 3;
2805
3009
  NSInteger pageSlotStart = hasOverlaySlot ? 3 : 2;
2806
3010
  if (hasOverlaySlot) {
@@ -2933,6 +3137,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2933
3137
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2934
3138
  [self updateScrollEdgeContentMasks];
2935
3139
  [self updateHeaderSettledStateForceBusy:NO];
3140
+ [self logHomeVisualGeometry:@"layout" scrollView:_observedScrollView];
2936
3141
  dispatch_async(dispatch_get_main_queue(), ^{
2937
3142
  if (!self->_isBeingRecycled) {
2938
3143
  [self prepareAdjacentPageInsets];
@@ -3050,7 +3255,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3050
3255
  _externalTopOverlayHeight = 0;
3051
3256
  _externalOuterPageActive = NO;
3052
3257
  _externalOuterTransitioning = NO;
3053
- _topScrollEdgeHard = NO;
3258
+ _topScrollEdgeEnabled = NO;
3259
+ _topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
3260
+ _homeVisualStartMono = 0;
3261
+ _homeVisualLastScrollLog = 0;
3262
+ _homeVisualLastLayoutLog = 0;
3263
+ _homeVisualLastGeometrySignature = nil;
3054
3264
  _nativeTabBarHeight = 44;
3055
3265
  _nativeSubHeaderHeight = 74;
3056
3266
  _headerOffset = 0;
@@ -3092,7 +3302,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3092
3302
  const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
3093
3303
  const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
3094
3304
  _applyingTopOverlayInsetChange =
3095
- oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard ||
3305
+ (oldViewProps.topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::None) !=
3306
+ (newViewProps.topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::None) ||
3096
3307
  oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight;
3097
3308
 
3098
3309
  if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
@@ -3143,9 +3354,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3143
3354
  _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
3144
3355
  insetsChanged = YES;
3145
3356
  }
3146
- if (_needsPropsReapply || oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard) {
3147
- _topScrollEdgeHard = newViewProps.topScrollEdgeHard;
3148
- if (oldViewProps.topScrollEdgeHard || newViewProps.topScrollEdgeHard) {
3357
+ if (_needsPropsReapply || oldViewProps.topScrollEdgeEffect != newViewProps.topScrollEdgeEffect) {
3358
+ BOOL enabled = newViewProps.topScrollEdgeEffect != RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
3359
+ BOOL enabledChanged = _topScrollEdgeEnabled != enabled;
3360
+ _topScrollEdgeEnabled = enabled;
3361
+ _topScrollEdgeEffect = newViewProps.topScrollEdgeEffect;
3362
+ if (enabledChanged) {
3149
3363
  _deferTopEdgeSlotRebuild = YES;
3150
3364
  [self rebuildSlots];
3151
3365
  NSUInteger generation = _generation;
@@ -3156,12 +3370,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3156
3370
  [self rebuildSlots];
3157
3371
  });
3158
3372
  }
3159
- insetsChanged = YES;
3373
+ insetsChanged |= enabledChanged;
3160
3374
  }
3161
3375
  if (_needsPropsReapply ||
3162
3376
  oldViewProps.topOverlayColorScheme != newViewProps.topOverlayColorScheme) {
3163
3377
  NSString *nextScheme = RCTNSStringFromString(newViewProps.topOverlayColorScheme);
3164
- if (self.hasFixedTopOverlay && _topScrollEdgeHard &&
3378
+ if (self.hasFixedTopOverlay && _topScrollEdgeEnabled &&
3165
3379
  _topOverlayColorScheme != nil &&
3166
3380
  ![_topOverlayColorScheme isEqualToString:nextScheme]) {
3167
3381
  _pendingTopOverlayAppearanceCommit = YES;
@@ -3251,6 +3465,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3251
3465
  if (insetsChanged) {
3252
3466
  [self reapplyInsetsToObservedScrollView];
3253
3467
  [self setNeedsLayout];
3468
+ [self logHomeVisualGeometry:@"props-inset" scrollView:_observedScrollView];
3254
3469
  }
3255
3470
  _applyingTopOverlayInsetChange = NO;
3256
3471
  if (!_nativeTabBarView.hidden) {
@@ -3831,8 +4046,105 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3831
4046
  [_managedRefreshControls removeObjectForKey:scrollView];
3832
4047
  if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
3833
4048
  }
3834
- [RNCRefreshControlOffsetState setOffset:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
4049
+ [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
3835
4050
  forControl:current owner:self];
4051
+ if (previous != current) [self logHomeVisualGeometry:@"refresh-bind" scrollView:scrollView];
4052
+ }
4053
+
4054
+ - (void)logHomeVisualGeometry:(NSString *)phase scrollView:(UIScrollView *)scrollView
4055
+ {
4056
+ #if DEBUG
4057
+ if (self.window == nil || _isBeingRecycled) return;
4058
+ CFTimeInterval now = CACurrentMediaTime();
4059
+ if (_homeVisualStartMono == 0) _homeVisualStartMono = now;
4060
+ if ([phase isEqualToString:@"pull"] && now - _homeVisualLastScrollLog < 0.2) return;
4061
+ if ([phase isEqualToString:@"pull"]) _homeVisualLastScrollLog = now;
4062
+ UIRefreshControl *control = scrollView.refreshControl;
4063
+ CGRect controlWindow = control != nil
4064
+ ? [control convertRect:control.bounds toView:self.window] : CGRectZero;
4065
+ CGRect overlayWindow = [_topOverlayHostView convertRect:_topOverlayHostView.bounds
4066
+ toView:self.window];
4067
+ CGRect stickyWindow = [_stickyHeaderView convertRect:_stickyHeaderView.bounds
4068
+ toView:self.window];
4069
+ CGRect maskWindow = _sharedHeaderMaskView != nil
4070
+ ? [_sharedHeaderHostView convertRect:_sharedHeaderMaskView.frame toView:self.window]
4071
+ : CGRectZero;
4072
+ CGFloat pageMaskWindowY = [_containerView convertPoint:
4073
+ CGPointMake(0, _pageMaskBoundary) toView:self.window].y;
4074
+ BOOL layoutPhase = [phase isEqualToString:@"layout"] ||
4075
+ [phase isEqualToString:@"did-mount"];
4076
+ NSString *signature = [NSString stringWithFormat:
4077
+ @"%.1f:%ld:%ld:%.1f:%d:%.1f:%.1f:%.1f:%d:%d",
4078
+ self.effectiveTopOverlayHeight, (long)_headerHeight,
4079
+ (long)_stickyHeaderHeight, scrollView.contentInset.top, control != nil,
4080
+ CGRectGetMinY(maskWindow), CGRectGetHeight(maskWindow), _pageMaskBoundary,
4081
+ _stickyHeaderIsPinned, _topScrollEdgeEnabled];
4082
+ if (layoutPhase && now - _homeVisualStartMono >= 2.0 &&
4083
+ [_homeVisualLastGeometrySignature isEqualToString:signature] &&
4084
+ now - _homeVisualLastLayoutLog < 2.0) return;
4085
+ if (layoutPhase) {
4086
+ _homeVisualLastLayoutLog = now;
4087
+ _homeVisualLastGeometrySignature = signature;
4088
+ }
4089
+ CGFloat visualYEstimate = CGRectGetMinY(controlWindow) +
4090
+ (control != nil ? control.layer.sublayerTransform.m42 : 0);
4091
+ CALayer *firstChild = control.layer.sublayers.firstObject;
4092
+ BOOL childAttached = firstChild != nil && control.window == self.window;
4093
+ CGRect firstChildWindow = childAttached
4094
+ ? [firstChild convertRect:firstChild.bounds toLayer:self.window.layer]
4095
+ : CGRectZero;
4096
+ CGFloat overscroll = scrollView != nil
4097
+ ? -(scrollView.contentOffset.y + scrollView.contentInset.top) : 0;
4098
+ double epochMs = [NSDate.date timeIntervalSince1970] * 1000;
4099
+ double monoMs = now * 1000;
4100
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4101
+ @"[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",
4102
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4103
+ (long)_currentIndex, self.window != nil, _topScrollEdgeEnabled,
4104
+ self.effectiveTopOverlayHeight, (long)_headerHeight, (long)_stickyHeaderHeight,
4105
+ _headerOffset, scrollView.contentOffset.y, scrollView.contentInset.top,
4106
+ overscroll, control != nil, control.isRefreshing,
4107
+ CGRectGetMinY(controlWindow), CGRectGetHeight(controlWindow),
4108
+ control != nil ? control.layer.sublayerTransform.m42 : 0, visualYEstimate,
4109
+ CGRectGetMinY(overlayWindow), CGRectGetMaxY(overlayWindow),
4110
+ CGRectGetMinY(stickyWindow), CGRectGetMaxY(stickyWindow),
4111
+ CGRectGetMinY(maskWindow), CGRectGetHeight(maskWindow), pageMaskWindowY]);
4112
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4113
+ @"[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",
4114
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4115
+ (long)_currentIndex, control.bounds.origin.y, control.bounds.size.height,
4116
+ control.center.y, control.transform.ty, control.layer.position.y,
4117
+ control.layer.transform.m42, (unsigned long)control.layer.sublayers.count,
4118
+ childAttached, CGRectGetMinY(firstChildWindow), CGRectGetHeight(firstChildWindow)]);
4119
+ if (![phase isEqualToString:@"pull"]) {
4120
+ CALayer *topCover = _topEdgeSeparatorCover;
4121
+ CALayer *stickyCover = _stickyEdgeSeparatorCover;
4122
+ UIColor *color = topCover.backgroundColor != nil
4123
+ ? [UIColor colorWithCGColor:topCover.backgroundColor] : nil;
4124
+ CGFloat red = 0, green = 0, blue = 0, alpha = 0;
4125
+ [color getRed:&red green:&green blue:&blue alpha:&alpha];
4126
+ CGRect topCoverWindow = topCover.superlayer != nil
4127
+ ? [topCover convertRect:topCover.bounds toLayer:self.window.layer] : CGRectZero;
4128
+ CGRect stickyCoverWindow = stickyCover.superlayer != nil
4129
+ ? [stickyCover convertRect:stickyCover.bounds toLayer:self.window.layer] : CGRectZero;
4130
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
4131
+ @"[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",
4132
+ epochMs, monoMs, phase, (long)self.tag, (unsigned long)_generation,
4133
+ (long)_currentIndex, (long)self.traitCollection.userInterfaceStyle,
4134
+ (long)_topOverlayHostView.overrideUserInterfaceStyle,
4135
+ (long)_topScrollEdgeEffect, _stickyHeaderIsPinned,
4136
+ CGRectGetMinY(overlayWindow), CGRectGetMaxY(overlayWindow),
4137
+ CGRectGetMinY(stickyWindow), CGRectGetMaxY(stickyWindow),
4138
+ _topEdgeInteraction.scrollView == scrollView,
4139
+ _stickyEdgeInteraction.scrollView == scrollView,
4140
+ topCover != nil, topCover.hidden, CGRectGetMinY(topCoverWindow),
4141
+ CGRectGetHeight(topCoverWindow), stickyCover != nil,
4142
+ stickyCover.hidden,
4143
+ CGRectGetMinY(stickyCoverWindow),
4144
+ CGRectGetHeight(stickyCoverWindow), red, green, blue, alpha,
4145
+ self.window.screen.scale]);
4146
+ }
4147
+ #endif
3836
4148
  }
3837
4149
 
3838
4150
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
@@ -4250,6 +4562,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4250
4562
  if (context == RNCScrollRefreshControlContext) {
4251
4563
  UIScrollView *scrollView = (UIScrollView *)object;
4252
4564
  [self updateRefreshControlForScrollView:scrollView];
4565
+ [self logHomeVisualGeometry:@"refresh-control-change" scrollView:scrollView];
4253
4566
  if (scrollView == _observedScrollView && !_syntheticPageRestore &&
4254
4567
  (!_nativeSmoothHeaderScrollEnabled || (!_transitioning && _transitionPageOffsets == nil))) {
4255
4568
  [self updateHeaderForScrollView:scrollView];
@@ -4257,6 +4570,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4257
4570
  return;
4258
4571
  }
4259
4572
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
4573
+ if ([keyPath isEqualToString:@"contentOffset"] &&
4574
+ -(((UIScrollView *)object).contentOffset.y +
4575
+ ((UIScrollView *)object).contentInset.top) > 5) {
4576
+ [self logHomeVisualGeometry:@"pull" scrollView:(UIScrollView *)object];
4577
+ }
4260
4578
  if (_pendingTopInsetReboundScrollView == object &&
4261
4579
  ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
4262
4580
  if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {