@onekeyfe/react-native-pager-view 6.7.0-5 → 6.7.0-7
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.
- package/docs/SPEC.md +23 -8
- package/ios/RNCCollapsiblePagerViewComponentView.mm +257 -2
- package/ios/RNCNativeScrollerComponentView.mm +24 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +124 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +160 -0
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
- package/lib/module/CollapsiblePagerView.js +12 -3
- package/lib/typescript/src/CollapsiblePagerView.d.ts +4 -2
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +31 -4
package/docs/SPEC.md
CHANGED
|
@@ -14,17 +14,32 @@ valid prop values. Both the style and overlay are
|
|
|
14
14
|
disabled by default. On iOS 26 or newer, a local overlay remains fixed at the
|
|
15
15
|
top of the viewport while the owned vertical `UIScrollView` can draw behind it.
|
|
16
16
|
The component measures the overlay's Fabric layout height and owns the
|
|
17
|
-
corresponding top content inset.
|
|
17
|
+
corresponding top content inset.
|
|
18
|
+
For callers with a known initial height, `topOverlayHeightHint` reserves that
|
|
19
|
+
height on the first frame after the overlay is enabled, before Fabric delivers
|
|
20
|
+
`onLayout`. The measured height replaces the hint, and disabling the overlay
|
|
21
|
+
clears the measurement so a later enablement can use a new hint. The hint does
|
|
22
|
+
not enable the effect or reserve an inset without an active local overlay.
|
|
23
|
+
This uses the existing native `topOverlayHeight` prop without changing the
|
|
24
|
+
native interface. For native smooth local Hard mode, a complete Fabric mount
|
|
25
|
+
attaches the overlay slot synchronously before `didMount` returns; incomplete
|
|
26
|
+
slots retain the deferred fallback. It binds UIKit's
|
|
18
27
|
`UIScrollEdgeElementContainerInteraction` to that exact scroll view and selects
|
|
19
28
|
the requested top edge style. Hard has UIKit's dividing line; Soft has its
|
|
20
29
|
gradual blurred transition. A pager owns the effect and inset for its current page;
|
|
21
30
|
its nested NativeList or NativeScroller must not opt into another top overlay.
|
|
22
31
|
Invisible, noninteractive native labels let UIKit recognize the React-drawn
|
|
23
32
|
overlay and sticky header as scroll-edge elements. The pager does not add an
|
|
24
|
-
auxiliary full-area glass
|
|
33
|
+
auxiliary full-area glass. It clips scrolling assets above
|
|
25
34
|
the fixed overlay boundary; during horizontal transitions it clips its owned
|
|
26
35
|
visible page hosts below the current sticky boundary so text cannot pass in
|
|
27
36
|
front of the fixed controls. Masked assets must not intercept their hit targets.
|
|
37
|
+
On iOS 27 or newer, a local smooth Hard pager with a valid shared-header mask
|
|
38
|
+
fades a bottommost overlay layer using the resolved opaque pager or ancestor
|
|
39
|
+
theme color as the real header collapses, without hard-coded colors or changing
|
|
40
|
+
UIKit's Hard style, owner or interactions. During temporary horizontal owner
|
|
41
|
+
transitions the layer holds its last opacity until geometry is valid again;
|
|
42
|
+
disabling the local overlay, changing style or recycling removes it.
|
|
28
43
|
|
|
29
44
|
For an iOS 26 pager whose fixed header is rendered by a sibling in the same
|
|
30
45
|
React surface, `externalTopOverlayTag` may identify that header's native view.
|
|
@@ -61,6 +76,8 @@ The measured overlay height is excluded from the collapsible header distance:
|
|
|
61
76
|
overlay. Selection, cancellation, reuse and disposal rebind or release the
|
|
62
77
|
interaction and restore any prior scroll-edge style. Native scroll/refresh
|
|
63
78
|
coordination continues on the main thread without per-frame JavaScript work.
|
|
79
|
+
Refresh retains the pager-owned `overlay + header + sticky` top inset but offsets
|
|
80
|
+
the indicator by `header + sticky`, so its spinner begins below the fixed overlay.
|
|
64
81
|
|
|
65
82
|
On iOS before 26, the external tag is not forwarded to native. On iOS before
|
|
66
83
|
26, Android and Web the new props have no scroll-edge effect.
|
|
@@ -73,17 +90,15 @@ Acceptance requires real iOS 26 scrolling, partial collapse, short/empty page,
|
|
|
73
90
|
refresh, page transition/cancellation, dynamic tabs and light/dark checks;
|
|
74
91
|
source and build checks alone do not establish visual correctness.
|
|
75
92
|
|
|
76
|
-
|
|
93
|
+
`CollapsiblePagerView` paints a cover at each owned
|
|
77
94
|
fixed-overlay or pinned-sticky boundary with one physical pixel of the nearest
|
|
78
95
|
opaque ancestor background color (system background if none is available).
|
|
79
96
|
These native layers do not receive touches, change insets, or clip glass shadows.
|
|
80
97
|
They are removed for Soft, disabled/detached effects, and recycling. This is a
|
|
81
|
-
visual workaround using public layer APIs, not a UIKit separator setting
|
|
82
|
-
|
|
98
|
+
visual workaround using public layer APIs, not a UIKit separator setting. While
|
|
99
|
+
the sticky header is pinned, only the top cover is hidden; it reappears on unpin,
|
|
100
|
+
and the sticky cover and both Hard interactions remain active. Standalone
|
|
83
101
|
NativeScroller and NativeList retain UIKit's unmodified Hard appearance.
|
|
84
|
-
On the iOS 26.5 simulator, a runtime probe confirmed Hard and the cover layers,
|
|
85
|
-
including pinned-header ownership. The original hairline was not reliably
|
|
86
|
-
reproduced with the covers hidden, so separator suppression is not yet verified.
|
|
87
102
|
|
|
88
103
|
Style selection crosses Fabric as the enum `none | hard | soft`, defaulting to
|
|
89
104
|
`none`, rather than a hard-effect boolean. Switching Hard ↔ Soft on an attached
|
|
@@ -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,10 +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;
|
|
1470
1491
|
- (void)updateHardEdgeSeparatorCovers;
|
|
1492
|
+
- (void)updateTopOverlayThemeBackground;
|
|
1471
1493
|
- (void)updateScrollEdgeContentMasks;
|
|
1472
1494
|
- (void)restoreScrollEdgeContentMasks;
|
|
1473
1495
|
- (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
@@ -1506,6 +1528,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1506
1528
|
UILabel *_stickyEdgeMarker;
|
|
1507
1529
|
CALayer *_topEdgeSeparatorCover;
|
|
1508
1530
|
CALayer *_stickyEdgeSeparatorCover;
|
|
1531
|
+
CALayer *_topOverlayThemeBackgroundLayer;
|
|
1509
1532
|
__weak UIView *_topEdgeInteractionHost;
|
|
1510
1533
|
__weak UIView *_stickyEdgeInteractionHost;
|
|
1511
1534
|
__weak UIScrollView *_topEdgeScrollView;
|
|
@@ -1576,6 +1599,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1576
1599
|
NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
|
|
1577
1600
|
NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
|
|
1578
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;
|
|
1579
1609
|
NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
|
|
1580
1610
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
1581
1611
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
@@ -1803,6 +1833,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1803
1833
|
[self updateTopScrollEdgeForScrollView:_observedScrollView];
|
|
1804
1834
|
[self updateSharedHeaderPressCancellationGesture];
|
|
1805
1835
|
[self updateHeaderSettledStateForceBusy:NO];
|
|
1836
|
+
[self logHomeVisualGeometry:@"window" scrollView:_observedScrollView];
|
|
1806
1837
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
1807
1838
|
[self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight + self.effectiveTopOverlayHeight];
|
|
1808
1839
|
}
|
|
@@ -1830,7 +1861,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1830
1861
|
|
|
1831
1862
|
- (CGFloat)effectiveTopOverlayHeight
|
|
1832
1863
|
{
|
|
1833
|
-
|
|
1864
|
+
BOOL localOverlayPending = _topScrollEdgeEnabled && _topOverlayHeight > 0 &&
|
|
1865
|
+
_externalTopOverlayTag <= 0;
|
|
1866
|
+
return _topOverlayView != nil || localOverlayPending
|
|
1867
|
+
? _topOverlayHeight : _externalTopOverlayHeight;
|
|
1834
1868
|
}
|
|
1835
1869
|
|
|
1836
1870
|
- (BOOL)hasFixedTopOverlay
|
|
@@ -1913,6 +1947,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1913
1947
|
[super traitCollectionDidChange:previousTraitCollection];
|
|
1914
1948
|
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
|
|
1915
1949
|
[self syncScrollEdgeAppearance];
|
|
1950
|
+
[self updateTopOverlayThemeBackground];
|
|
1951
|
+
[self logHomeVisualGeometry:@"trait" scrollView:_observedScrollView];
|
|
1916
1952
|
}
|
|
1917
1953
|
}
|
|
1918
1954
|
|
|
@@ -1962,6 +1998,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1962
1998
|
self.window != nil && !_isBeingRecycled;
|
|
1963
1999
|
if (!enabled) {
|
|
1964
2000
|
[self restoreScrollEdgeContentMasks];
|
|
2001
|
+
[self updateTopOverlayThemeBackground];
|
|
1965
2002
|
return;
|
|
1966
2003
|
}
|
|
1967
2004
|
if (self.hasFixedTopOverlay && _sharedHeaderHostView.window == self.window &&
|
|
@@ -2031,6 +2068,102 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2031
2068
|
[_ownedPageMasks removeObjectForKey:page];
|
|
2032
2069
|
[_originalPageMasks removeObjectForKey:page];
|
|
2033
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
|
+
}
|
|
2034
2167
|
}
|
|
2035
2168
|
|
|
2036
2169
|
- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
|
|
@@ -2187,6 +2320,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2187
2320
|
CGRectGetWidth(_containerView.bounds), pixel);
|
|
2188
2321
|
cover.contentsScale = scale;
|
|
2189
2322
|
cover.backgroundColor = color.CGColor;
|
|
2323
|
+
cover.hidden = index == 0 && _stickyHeaderIsPinned;
|
|
2190
2324
|
}
|
|
2191
2325
|
if (index == 0) _topEdgeSeparatorCover = cover;
|
|
2192
2326
|
else _stickyEdgeSeparatorCover = cover;
|
|
@@ -2538,6 +2672,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2538
2672
|
[_nativeTabBarView updateAncestorGesturePrecedence];
|
|
2539
2673
|
[_nativeSubHeaderView updateAncestorGesturePrecedence];
|
|
2540
2674
|
[self updateTopScrollEdgeForScrollView:_observedScrollView];
|
|
2675
|
+
[self logHomeVisualGeometry:shouldPin ? @"pin" : @"unpin"
|
|
2676
|
+
scrollView:_observedScrollView];
|
|
2541
2677
|
}
|
|
2542
2678
|
|
|
2543
2679
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
|
|
@@ -2689,6 +2825,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2689
2825
|
- (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
|
|
2690
2826
|
withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
|
|
2691
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
|
+
}
|
|
2692
2837
|
if (_needsSlotRebuild && !_deferTopEdgeSlotRebuild &&
|
|
2693
2838
|
!_transitioning && !_isPagerDragging) {
|
|
2694
2839
|
[self rebuildSlots];
|
|
@@ -2711,6 +2856,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2711
2856
|
}
|
|
2712
2857
|
[self updateScrollEdgeContentMasks];
|
|
2713
2858
|
[_mountingRemovedScrollViews removeAllObjects];
|
|
2859
|
+
[self logHomeVisualGeometry:@"did-mount" scrollView:_observedScrollView];
|
|
2714
2860
|
if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
|
|
2715
2861
|
_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
|
|
2716
2862
|
// Prepare newly mounted owned scrollers before their first frame, even while
|
|
@@ -2991,6 +3137,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2991
3137
|
[self updateTopScrollEdgeForScrollView:_observedScrollView];
|
|
2992
3138
|
[self updateScrollEdgeContentMasks];
|
|
2993
3139
|
[self updateHeaderSettledStateForceBusy:NO];
|
|
3140
|
+
[self logHomeVisualGeometry:@"layout" scrollView:_observedScrollView];
|
|
2994
3141
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2995
3142
|
if (!self->_isBeingRecycled) {
|
|
2996
3143
|
[self prepareAdjacentPageInsets];
|
|
@@ -3110,6 +3257,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3110
3257
|
_externalOuterTransitioning = NO;
|
|
3111
3258
|
_topScrollEdgeEnabled = NO;
|
|
3112
3259
|
_topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
|
|
3260
|
+
_homeVisualStartMono = 0;
|
|
3261
|
+
_homeVisualLastScrollLog = 0;
|
|
3262
|
+
_homeVisualLastLayoutLog = 0;
|
|
3263
|
+
_homeVisualLastGeometrySignature = nil;
|
|
3113
3264
|
_nativeTabBarHeight = 44;
|
|
3114
3265
|
_nativeSubHeaderHeight = 74;
|
|
3115
3266
|
_headerOffset = 0;
|
|
@@ -3314,6 +3465,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3314
3465
|
if (insetsChanged) {
|
|
3315
3466
|
[self reapplyInsetsToObservedScrollView];
|
|
3316
3467
|
[self setNeedsLayout];
|
|
3468
|
+
[self logHomeVisualGeometry:@"props-inset" scrollView:_observedScrollView];
|
|
3317
3469
|
}
|
|
3318
3470
|
_applyingTopOverlayInsetChange = NO;
|
|
3319
3471
|
if (!_nativeTabBarView.hidden) {
|
|
@@ -3894,8 +4046,105 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3894
4046
|
[_managedRefreshControls removeObjectForKey:scrollView];
|
|
3895
4047
|
if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
|
|
3896
4048
|
}
|
|
3897
|
-
[RNCRefreshControlOffsetState setOffset:
|
|
4049
|
+
[RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
|
|
3898
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
|
|
3899
4148
|
}
|
|
3900
4149
|
|
|
3901
4150
|
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
|
|
@@ -4313,6 +4562,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
4313
4562
|
if (context == RNCScrollRefreshControlContext) {
|
|
4314
4563
|
UIScrollView *scrollView = (UIScrollView *)object;
|
|
4315
4564
|
[self updateRefreshControlForScrollView:scrollView];
|
|
4565
|
+
[self logHomeVisualGeometry:@"refresh-control-change" scrollView:scrollView];
|
|
4316
4566
|
if (scrollView == _observedScrollView && !_syntheticPageRestore &&
|
|
4317
4567
|
(!_nativeSmoothHeaderScrollEnabled || (!_transitioning && _transitionPageOffsets == nil))) {
|
|
4318
4568
|
[self updateHeaderForScrollView:scrollView];
|
|
@@ -4320,6 +4570,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
4320
4570
|
return;
|
|
4321
4571
|
}
|
|
4322
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
|
+
}
|
|
4323
4578
|
if (_pendingTopInsetReboundScrollView == object &&
|
|
4324
4579
|
([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
|
|
4325
4580
|
if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
|
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
using namespace facebook::react;
|
|
10
10
|
|
|
11
|
+
static void RNCNativeScrollerHomeVisualLog(NSString *phase, UIScrollView *scrollView)
|
|
12
|
+
{
|
|
13
|
+
#if DEBUG
|
|
14
|
+
Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
|
|
15
|
+
if (logClass == nil) logClass = NSClassFromString(@"OneKeyLog");
|
|
16
|
+
SEL selector = NSSelectorFromString(@"debug::");
|
|
17
|
+
if (logClass == nil || ![logClass respondsToSelector:selector]) return;
|
|
18
|
+
UIRefreshControl *control = scrollView.refreshControl;
|
|
19
|
+
NSString *message = [NSString stringWithFormat:
|
|
20
|
+
@"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=scroller-%@ scrollTag=%ld refresh=%d refreshing=%d scrollY=%.1f insetTop=%.1f",
|
|
21
|
+
[NSDate.date timeIntervalSince1970] * 1000, CACurrentMediaTime() * 1000,
|
|
22
|
+
phase, (long)scrollView.tag, control != nil, control.isRefreshing,
|
|
23
|
+
scrollView.contentOffset.y, scrollView.contentInset.top];
|
|
24
|
+
typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
|
|
25
|
+
LogFunction logFunction = (LogFunction)[logClass methodForSelector:selector];
|
|
26
|
+
logFunction(logClass, selector, @"NativeScroller", message);
|
|
27
|
+
#endif
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
@interface RNCNativeVerticalScrollView : UIScrollView
|
|
12
31
|
@end
|
|
13
32
|
@implementation RNCNativeVerticalScrollView
|
|
@@ -156,6 +175,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
156
175
|
[_refreshControl addTarget:self action:@selector(refreshTriggered:)
|
|
157
176
|
forControlEvents:UIControlEventValueChanged];
|
|
158
177
|
_scrollView.refreshControl = _refreshControl;
|
|
178
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-created", _scrollView);
|
|
159
179
|
} else if (!next.refreshEnabled && _refreshControl != nil) {
|
|
160
180
|
[_refreshControl endRefreshing];
|
|
161
181
|
_scrollView.refreshControl = nil;
|
|
@@ -174,8 +194,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
174
194
|
}
|
|
175
195
|
if (next.refreshing && !_refreshControl.refreshing) {
|
|
176
196
|
[_refreshControl beginRefreshing];
|
|
197
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-begin-prop", _scrollView);
|
|
177
198
|
} else if (!next.refreshing && (previous.refreshing || createdRefreshControl)) {
|
|
178
199
|
[_refreshControl endRefreshing];
|
|
200
|
+
if (previous.refreshing) RNCNativeScrollerHomeVisualLog(@"refresh-end-prop", _scrollView);
|
|
179
201
|
}
|
|
180
202
|
}
|
|
181
203
|
|
|
@@ -379,6 +401,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
379
401
|
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
380
402
|
if (!props.refreshEnabled || props.refreshing) return;
|
|
381
403
|
_refreshEventSentForDrag = YES;
|
|
404
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-native-trigger", _scrollView);
|
|
382
405
|
if (_eventEmitter) {
|
|
383
406
|
std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter)->onRefresh({});
|
|
384
407
|
}
|
|
@@ -523,6 +546,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
523
546
|
- (void)setRefreshing:(BOOL)refreshing
|
|
524
547
|
{
|
|
525
548
|
if (_recycled || _refreshControl == nil) return;
|
|
549
|
+
RNCNativeScrollerHomeVisualLog(refreshing ? @"refresh-command-start" : @"refresh-command-stop", _scrollView);
|
|
526
550
|
if (refreshing) {
|
|
527
551
|
if (!_refreshControl.refreshing) [_refreshControl beginRefreshing];
|
|
528
552
|
} else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#import <XCTest/XCTest.h>
|
|
2
2
|
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
3
3
|
#import "RNCNativeScrollerComponentView.h"
|
|
4
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
4
5
|
#import <react/renderer/mounting/MountingTransaction.h>
|
|
5
6
|
|
|
6
7
|
using namespace facebook::react;
|
|
@@ -22,6 +23,10 @@ using namespace facebook::react;
|
|
|
22
23
|
- (void)updateHeaderSettledStateForceBusy:(BOOL)forceBusy;
|
|
23
24
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)index;
|
|
24
25
|
- (void)applyInsetsToScrollView:(UIScrollView *)scrollView pageIndex:(NSInteger)index restore:(BOOL)restore;
|
|
26
|
+
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps;
|
|
27
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;
|
|
28
|
+
- (CGFloat)effectiveTopOverlayHeight;
|
|
29
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
25
30
|
@end
|
|
26
31
|
|
|
27
32
|
@interface RNCNativeScrollerComponentView (ScrollCommandTesting)
|
|
@@ -34,6 +39,125 @@ using namespace facebook::react;
|
|
|
34
39
|
|
|
35
40
|
@implementation RNCCollapsiblePagerFirstFrameTests
|
|
36
41
|
|
|
42
|
+
- (void)testPendingLocalOverlayReservesManagedScrollerInsetBeforeSlotRebuild
|
|
43
|
+
{
|
|
44
|
+
RNCCollapsiblePagerViewComponentView *pager =
|
|
45
|
+
[[RNCCollapsiblePagerViewComponentView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
|
|
46
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:pager.frame];
|
|
47
|
+
UIViewController *root = [UIViewController new];
|
|
48
|
+
window.rootViewController = root;
|
|
49
|
+
[root.view addSubview:pager];
|
|
50
|
+
window.hidden = NO;
|
|
51
|
+
[self addTeardownBlock:^{
|
|
52
|
+
[pager prepareForRecycle];
|
|
53
|
+
window.hidden = YES;
|
|
54
|
+
}];
|
|
55
|
+
|
|
56
|
+
auto baseline = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
57
|
+
baseline->nativeSmoothHeaderScrollEnabled = true;
|
|
58
|
+
baseline->headerHeight = 120;
|
|
59
|
+
baseline->stickyHeaderHeight = 44;
|
|
60
|
+
baseline->pageKeys = "[\"page-0\"]";
|
|
61
|
+
[pager updateProps:baseline oldProps:nullptr];
|
|
62
|
+
|
|
63
|
+
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 120)];
|
|
64
|
+
UIView *sticky = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
|
|
65
|
+
UIView *page = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
|
|
66
|
+
RNCNativeScrollerComponentView *scroller = [RNCNativeScrollerComponentView new];
|
|
67
|
+
scroller.frame = page.bounds;
|
|
68
|
+
scroller.scrollView.frame = scroller.bounds;
|
|
69
|
+
scroller.scrollView.contentSize = CGSizeMake(320, 1500);
|
|
70
|
+
[page addSubview:scroller];
|
|
71
|
+
[pager mountChildComponentView:(id)header index:0];
|
|
72
|
+
[pager mountChildComponentView:(id)sticky index:1];
|
|
73
|
+
[pager mountChildComponentView:(id)page index:2];
|
|
74
|
+
[root.view layoutIfNeeded];
|
|
75
|
+
[pager layoutIfNeeded];
|
|
76
|
+
XCTestExpectation *settled = [self expectationWithDescription:@"Baseline slots and observer settled"];
|
|
77
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [settled fulfill]; });
|
|
78
|
+
[self waitForExpectations:@[settled] timeout:1];
|
|
79
|
+
[pager attachScrollObserverForCurrentPage];
|
|
80
|
+
UIScrollView *ownedScroll = scroller.scrollView;
|
|
81
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
82
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 164, 0.01);
|
|
83
|
+
[scroller scrollTo:0 y:40 - ownedScroll.contentInset.top animated:NO];
|
|
84
|
+
[pager setValue:@40 forKey:@"headerOffset"];
|
|
85
|
+
CGFloat initialLogicalOffset = ownedScroll.contentOffset.y + ownedScroll.contentInset.top;
|
|
86
|
+
XCTAssertEqualWithAccuracy(initialLogicalOffset, 40, 0.01);
|
|
87
|
+
|
|
88
|
+
auto hard = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
89
|
+
hard->nativeSmoothHeaderScrollEnabled = true;
|
|
90
|
+
hard->headerHeight = 120;
|
|
91
|
+
hard->stickyHeaderHeight = 44;
|
|
92
|
+
hard->pageKeys = "[\"page-0\"]";
|
|
93
|
+
hard->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
|
|
94
|
+
hard->topOverlayHeight = 162;
|
|
95
|
+
[pager updateProps:hard oldProps:baseline];
|
|
96
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
97
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
98
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 162, 0.01);
|
|
99
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
100
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
101
|
+
initialLogicalOffset, 0.01);
|
|
102
|
+
[self finishMount:pager];
|
|
103
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
104
|
+
XCTAssertTrue([[pager valueForKey:@"deferTopEdgeSlotRebuild"] boolValue]);
|
|
105
|
+
|
|
106
|
+
UIView *overlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 162)];
|
|
107
|
+
[pager mountChildComponentView:(id)overlay index:2];
|
|
108
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
109
|
+
[self finishMount:pager];
|
|
110
|
+
UIView *overlayHost = [pager valueForKey:@"topOverlayHostView"];
|
|
111
|
+
XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
|
|
112
|
+
XCTAssertEqual(overlay.superview, overlayHost);
|
|
113
|
+
XCTAssertEqual(overlay.window, window);
|
|
114
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
115
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
116
|
+
initialLogicalOffset, 0.01);
|
|
117
|
+
XCTestExpectation *rebuilt = [self expectationWithDescription:@"Post-mount observer restored"];
|
|
118
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [rebuilt fulfill]; });
|
|
119
|
+
[self waitForExpectations:@[rebuilt] timeout:1];
|
|
120
|
+
[pager attachScrollObserverForCurrentPage];
|
|
121
|
+
XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
|
|
122
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
123
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
124
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
125
|
+
initialLogicalOffset, 0.01);
|
|
126
|
+
|
|
127
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
128
|
+
ownedScroll.refreshControl = control;
|
|
129
|
+
[pager updateRefreshControlForScrollView:ownedScroll];
|
|
130
|
+
UIEdgeInsets refreshingInset = ownedScroll.contentInset;
|
|
131
|
+
refreshingInset.top += 60;
|
|
132
|
+
ownedScroll.contentInset = refreshingInset;
|
|
133
|
+
auto disabled = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
134
|
+
disabled->nativeSmoothHeaderScrollEnabled = true;
|
|
135
|
+
disabled->headerHeight = 120;
|
|
136
|
+
disabled->stickyHeaderHeight = 44;
|
|
137
|
+
disabled->pageKeys = "[\"page-0\"]";
|
|
138
|
+
disabled->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
|
|
139
|
+
disabled->topOverlayHeight = 0;
|
|
140
|
+
[pager updateProps:disabled oldProps:hard];
|
|
141
|
+
XCTAssertEqual(ownedScroll.refreshControl, control);
|
|
142
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
|
|
143
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 224, 0.01);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (void)testPendingLocalOverlayHeightDoesNotOverrideExternalGeometry
|
|
147
|
+
{
|
|
148
|
+
RNCCollapsiblePagerViewComponentView *pager = [RNCCollapsiblePagerViewComponentView new];
|
|
149
|
+
auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
150
|
+
props->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
|
|
151
|
+
props->topOverlayHeight = 162;
|
|
152
|
+
props->externalTopOverlayTag = 999;
|
|
153
|
+
[pager updateProps:props oldProps:nullptr];
|
|
154
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
155
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
|
|
156
|
+
[pager setValue:@64 forKey:@"externalTopOverlayHeight"];
|
|
157
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 64, 0.01);
|
|
158
|
+
[pager prepareForRecycle];
|
|
159
|
+
}
|
|
160
|
+
|
|
37
161
|
- (void)testHeaderSettledAtTopRequiresCurrentQuietOwnerAndCancelsDetachedTimer
|
|
38
162
|
{
|
|
39
163
|
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
@@ -8,6 +8,7 @@ using namespace facebook::react;
|
|
|
8
8
|
|
|
9
9
|
@interface RNCCollapsiblePagerViewComponentView (ScrollEdgeTesting)
|
|
10
10
|
- (void)updateTopScrollEdgeForScrollView:(UIScrollView *)scrollView;
|
|
11
|
+
- (void)updateScrollEdgeContentMasks;
|
|
11
12
|
@end
|
|
12
13
|
|
|
13
14
|
@interface RNCCollapsiblePagerHitTestTests : XCTestCase
|
|
@@ -151,4 +152,163 @@ using namespace facebook::react;
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
- (void)testPinnedHardEdgeHidesOnlyTopSeparatorCoverUntilUnpinned
|
|
156
|
+
{
|
|
157
|
+
if (@available(iOS 26.0, *)) {
|
|
158
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:self.root.bounds];
|
|
159
|
+
[window addSubview:self.root];
|
|
160
|
+
[self addTeardownBlock:^{ window.hidden = YES; }];
|
|
161
|
+
window.hidden = NO;
|
|
162
|
+
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.pager.bounds];
|
|
163
|
+
[self.pager addSubview:scrollView];
|
|
164
|
+
[self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
|
|
165
|
+
[self.pager setValue:@(static_cast<NSInteger>(RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard))
|
|
166
|
+
forKey:@"topScrollEdgeEffect"];
|
|
167
|
+
[self.pager setValue:@123 forKey:@"externalTopOverlayTag"];
|
|
168
|
+
[self.pager setValue:@64 forKey:@"externalTopOverlayHeight"];
|
|
169
|
+
[self.pager setValue:self.sticky forKey:@"stickyHeaderView"];
|
|
170
|
+
UIView *container = [self.pager valueForKey:@"containerView"];
|
|
171
|
+
container.frame = self.pager.bounds;
|
|
172
|
+
UIView *overlayHost = [self.pager valueForKey:@"topOverlayHostView"];
|
|
173
|
+
overlayHost.frame = CGRectMake(0, 0, 320, 64);
|
|
174
|
+
overlayHost.hidden = NO;
|
|
175
|
+
UIView *pinnedHost = [self.pager valueForKey:@"pinnedStickyHostView"];
|
|
176
|
+
pinnedHost.frame = CGRectMake(0, 64, 320, 44);
|
|
177
|
+
pinnedHost.hidden = NO;
|
|
178
|
+
[scrollView addSubview:pinnedHost];
|
|
179
|
+
|
|
180
|
+
[self.pager updateTopScrollEdgeForScrollView:scrollView];
|
|
181
|
+
UIScrollEdgeElementContainerInteraction *topInteraction =
|
|
182
|
+
[self.pager valueForKey:@"topEdgeInteraction"];
|
|
183
|
+
CALayer *topCover = [self.pager valueForKey:@"topEdgeSeparatorCover"];
|
|
184
|
+
XCTAssertNotNil(topInteraction);
|
|
185
|
+
XCTAssertNotNil(topCover);
|
|
186
|
+
XCTAssertFalse(topCover.hidden);
|
|
187
|
+
XCTAssertEqual(scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
|
|
188
|
+
|
|
189
|
+
[self.pager setValue:@YES forKey:@"stickyHeaderIsPinned"];
|
|
190
|
+
[self.pager updateTopScrollEdgeForScrollView:scrollView];
|
|
191
|
+
UIScrollEdgeElementContainerInteraction *stickyInteraction =
|
|
192
|
+
[self.pager valueForKey:@"stickyEdgeInteraction"];
|
|
193
|
+
CALayer *stickyCover = [self.pager valueForKey:@"stickyEdgeSeparatorCover"];
|
|
194
|
+
XCTAssertEqual([self.pager valueForKey:@"topEdgeInteraction"], topInteraction);
|
|
195
|
+
XCTAssertEqual(topInteraction.scrollView, scrollView);
|
|
196
|
+
XCTAssertEqual(stickyInteraction.scrollView, scrollView);
|
|
197
|
+
XCTAssertEqual([self.pager valueForKey:@"topEdgeSeparatorCover"], topCover);
|
|
198
|
+
XCTAssertTrue(topCover.hidden);
|
|
199
|
+
XCTAssertNotNil(stickyCover);
|
|
200
|
+
XCTAssertFalse(stickyCover.hidden);
|
|
201
|
+
XCTAssertEqual(scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
|
|
202
|
+
|
|
203
|
+
[self.pager setValue:@NO forKey:@"stickyHeaderIsPinned"];
|
|
204
|
+
[self.pager updateTopScrollEdgeForScrollView:scrollView];
|
|
205
|
+
XCTAssertEqual([self.pager valueForKey:@"topEdgeInteraction"], topInteraction);
|
|
206
|
+
XCTAssertEqual([self.pager valueForKey:@"topEdgeSeparatorCover"], topCover);
|
|
207
|
+
XCTAssertFalse(topCover.hidden);
|
|
208
|
+
XCTAssertEqual(scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
|
|
209
|
+
[self.root removeFromSuperview];
|
|
210
|
+
} else {
|
|
211
|
+
XCTSkip(@"Scroll-edge interactions require iOS 26 or newer");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
- (void)testIOS27LocalHardThemeBackgroundTracksMaskedHeaderGeometry
|
|
216
|
+
{
|
|
217
|
+
if (@available(iOS 27.0, *)) {
|
|
218
|
+
UIWindowScene *scene = nil;
|
|
219
|
+
for (UIScene *connectedScene in UIApplication.sharedApplication.connectedScenes) {
|
|
220
|
+
if ([connectedScene isKindOfClass:UIWindowScene.class]) {
|
|
221
|
+
scene = (UIWindowScene *)connectedScene;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
XCTAssertNotNil(scene);
|
|
226
|
+
UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
|
|
227
|
+
window.frame = self.root.bounds;
|
|
228
|
+
[window addSubview:self.root];
|
|
229
|
+
window.hidden = NO;
|
|
230
|
+
[self addTeardownBlock:^{ window.hidden = YES; }];
|
|
231
|
+
[self.pager layoutIfNeeded];
|
|
232
|
+
self.pager.backgroundColor = [UIColor colorWithWhite:0.2 alpha:1];
|
|
233
|
+
UIView *container = [self.pager valueForKey:@"containerView"];
|
|
234
|
+
container.frame = self.pager.bounds;
|
|
235
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:container.bounds];
|
|
236
|
+
scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
237
|
+
scroll.alwaysBounceVertical = YES;
|
|
238
|
+
scroll.contentSize = CGSizeMake(320, 1200);
|
|
239
|
+
scroll.contentInset = UIEdgeInsetsMake(50 + 100 + 20, 0, 0, 0);
|
|
240
|
+
[container insertSubview:scroll atIndex:0];
|
|
241
|
+
UIView *sharedHost = [self.pager valueForKey:@"sharedHeaderHostView"];
|
|
242
|
+
for (UIView *child in sharedHost.subviews.copy) [child removeFromSuperview];
|
|
243
|
+
[scroll addSubview:sharedHost];
|
|
244
|
+
sharedHost.frame = CGRectMake(0, -120, 320, 120);
|
|
245
|
+
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 100)];
|
|
246
|
+
[sharedHost addSubview:header];
|
|
247
|
+
[self.pager setValue:header forKey:@"headerView"];
|
|
248
|
+
UIView *overlayHost = [self.pager valueForKey:@"topOverlayHostView"];
|
|
249
|
+
overlayHost.frame = CGRectMake(0, 0, 320, 50);
|
|
250
|
+
overlayHost.hidden = NO;
|
|
251
|
+
UIView *overlay = [[UIView alloc] initWithFrame:overlayHost.bounds];
|
|
252
|
+
[overlayHost addSubview:overlay];
|
|
253
|
+
[self.pager setValue:overlay forKey:@"topOverlayView"];
|
|
254
|
+
[self.pager setValue:@50 forKey:@"topOverlayHeight"];
|
|
255
|
+
[self.pager setValue:@100 forKey:@"headerHeight"];
|
|
256
|
+
[self.pager setValue:@20 forKey:@"stickyHeaderHeight"];
|
|
257
|
+
[self.pager setValue:@YES forKey:@"nativeSmoothHeaderScrollEnabled"];
|
|
258
|
+
[self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
|
|
259
|
+
[self.pager setValue:@(static_cast<NSInteger>(RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard))
|
|
260
|
+
forKey:@"topScrollEdgeEffect"];
|
|
261
|
+
[self.pager setValue:scroll forKey:@"observedScrollView"];
|
|
262
|
+
[self.pager setValue:scroll forKey:@"sharedHeaderScrollView"];
|
|
263
|
+
scroll.contentOffset = CGPointMake(0, -170);
|
|
264
|
+
[self.pager updateTopScrollEdgeForScrollView:scroll];
|
|
265
|
+
CALayer *layer = [self.pager valueForKey:@"topOverlayThemeBackgroundLayer"];
|
|
266
|
+
XCTAssertNotNil(layer);
|
|
267
|
+
XCTAssertEqual(layer.superlayer, overlayHost.layer);
|
|
268
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 1, 0.01);
|
|
269
|
+
XCTAssertEqual(scroll.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
|
|
270
|
+
XCTAssertEqual([self.pager valueForKey:@"topEdgeScrollView"], scroll);
|
|
271
|
+
|
|
272
|
+
scroll.contentOffset = CGPointMake(0, -120);
|
|
273
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
274
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 0.5, 0.01);
|
|
275
|
+
scroll.contentOffset = CGPointMake(0, -70);
|
|
276
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
277
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 0, 0.01);
|
|
278
|
+
scroll.contentOffset = CGPointMake(0, -210);
|
|
279
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
280
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 1, 0.01);
|
|
281
|
+
|
|
282
|
+
scroll.contentInset = UIEdgeInsetsMake(230, 0, 0, 0);
|
|
283
|
+
scroll.contentOffset = CGPointMake(0, -230);
|
|
284
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
285
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 1, 0.01);
|
|
286
|
+
self.pager.backgroundColor = UIColor.whiteColor;
|
|
287
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
288
|
+
XCTAssertTrue(CGColorEqualToColor(layer.backgroundColor, UIColor.whiteColor.CGColor));
|
|
289
|
+
|
|
290
|
+
[self.pager updateTopScrollEdgeForScrollView:nil];
|
|
291
|
+
XCTAssertEqual([self.pager valueForKey:@"topOverlayThemeBackgroundLayer"], layer);
|
|
292
|
+
XCTAssertEqualWithAccuracy(layer.opacity, 1, 0.01);
|
|
293
|
+
[self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
|
|
294
|
+
[self.pager updateTopScrollEdgeForScrollView:scroll];
|
|
295
|
+
[self.pager setValue:@123 forKey:@"externalTopOverlayTag"];
|
|
296
|
+
[self.pager updateScrollEdgeContentMasks];
|
|
297
|
+
XCTAssertNil([self.pager valueForKey:@"topOverlayThemeBackgroundLayer"]);
|
|
298
|
+
[self.pager setValue:@0 forKey:@"externalTopOverlayTag"];
|
|
299
|
+
[self.pager updateTopScrollEdgeForScrollView:scroll];
|
|
300
|
+
XCTAssertEqual(scroll.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
|
|
301
|
+
[self.pager setValue:@NO forKey:@"topScrollEdgeEnabled"];
|
|
302
|
+
[self.pager updateTopScrollEdgeForScrollView:scroll];
|
|
303
|
+
XCTAssertNil([self.pager valueForKey:@"topOverlayThemeBackgroundLayer"]);
|
|
304
|
+
[self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
|
|
305
|
+
[self.pager updateTopScrollEdgeForScrollView:scroll];
|
|
306
|
+
XCTAssertNotNil([self.pager valueForKey:@"topOverlayThemeBackgroundLayer"]);
|
|
307
|
+
[self.pager prepareForRecycle];
|
|
308
|
+
XCTAssertNil([self.pager valueForKey:@"topOverlayThemeBackgroundLayer"]);
|
|
309
|
+
} else {
|
|
310
|
+
XCTSkip(@"The theme background applies only on iOS 27 or newer");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
154
314
|
@end
|
|
@@ -60,6 +60,56 @@
|
|
|
60
60
|
XCTAssertEqualWithAccuracy(control.bounds.origin.x, 3, 0.001);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
- (void)testRefreshLayerWindowGeometryStartsBelowFixedOverlay
|
|
64
|
+
{
|
|
65
|
+
for (NSNumber *overlayHeight in @[@0, @162]) {
|
|
66
|
+
CGFloat fixedHeight = overlayHeight.doubleValue;
|
|
67
|
+
CGFloat baseInset = fixedHeight + 180 + 44;
|
|
68
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 390, 844)];
|
|
69
|
+
UIViewController *root = [UIViewController new];
|
|
70
|
+
window.rootViewController = root;
|
|
71
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:window.bounds];
|
|
72
|
+
scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
73
|
+
scroll.contentSize = CGSizeMake(390, 1500);
|
|
74
|
+
scroll.contentInset = UIEdgeInsetsMake(baseInset + 60, 0, 0, 0);
|
|
75
|
+
scroll.contentOffset = CGPointMake(0, -baseInset - 102);
|
|
76
|
+
[root.view addSubview:scroll];
|
|
77
|
+
UIView *overlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 390, fixedHeight)];
|
|
78
|
+
[root.view addSubview:overlay];
|
|
79
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
80
|
+
scroll.refreshControl = control;
|
|
81
|
+
window.hidden = NO;
|
|
82
|
+
[root.view layoutIfNeeded];
|
|
83
|
+
[control layoutIfNeeded];
|
|
84
|
+
// Match the control's window position recorded during a real refresh drag.
|
|
85
|
+
control.frame = [scroll convertRect:CGRectMake(0, baseInset, 390, 60)
|
|
86
|
+
fromView:window];
|
|
87
|
+
|
|
88
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:180];
|
|
89
|
+
[pager setValue:@(fixedHeight) forKey:@"topOverlayHeight"];
|
|
90
|
+
if (fixedHeight > 0) [pager setValue:overlay forKey:@"topOverlayView"];
|
|
91
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
92
|
+
[CATransaction flush];
|
|
93
|
+
CALayer *firstChild = control.layer.sublayers.firstObject;
|
|
94
|
+
XCTAssertNotNil(firstChild);
|
|
95
|
+
XCTAssertEqual(control.window, window);
|
|
96
|
+
CGRect overlayWindow = [overlay convertRect:overlay.bounds toView:window];
|
|
97
|
+
CGRect controlWindow = [control convertRect:control.bounds toView:window];
|
|
98
|
+
CGRect childWindow = [firstChild convertRect:firstChild.bounds toLayer:window.layer];
|
|
99
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinY(controlWindow), baseInset, 0.01);
|
|
100
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinY(childWindow),
|
|
101
|
+
CGRectGetMaxY(overlayWindow), 0.01);
|
|
102
|
+
XCTAssertEqualWithAccuracy(scroll.contentInset.top, baseInset + 60, 0.01);
|
|
103
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
104
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinY(
|
|
105
|
+
[firstChild convertRect:firstChild.bounds toLayer:window.layer]),
|
|
106
|
+
CGRectGetMaxY(overlayWindow), 0.01);
|
|
107
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
108
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
109
|
+
window.hidden = YES;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
63
113
|
- (void)testUIKitSizeChangesAndRefreshCycleDoNotAccumulateOffset
|
|
64
114
|
{
|
|
65
115
|
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
@@ -23,7 +23,7 @@ const pageKey = (child, index) => {
|
|
|
23
23
|
*/
|
|
24
24
|
export class CollapsiblePagerView extends React.PureComponent {
|
|
25
25
|
state = {
|
|
26
|
-
topOverlayHeight:
|
|
26
|
+
topOverlayHeight: null,
|
|
27
27
|
selectedPage: clampedPageIndex(this.props.initialPage ?? 0, React.Children.toArray(this.props.children).length),
|
|
28
28
|
transientRetainedPages: []
|
|
29
29
|
};
|
|
@@ -41,7 +41,14 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
41
41
|
componentDidMount() {
|
|
42
42
|
this.notifyMountedPagesChanged();
|
|
43
43
|
}
|
|
44
|
-
componentDidUpdate() {
|
|
44
|
+
componentDidUpdate(previousProps) {
|
|
45
|
+
const previousOverlayEnabled = previousProps.topOverlay != null && (previousProps.topScrollEdgeEffect === "hard" || previousProps.topScrollEdgeEffect === "soft");
|
|
46
|
+
const overlayEnabled = this.props.topOverlay != null && (this.props.topScrollEdgeEffect === "hard" || this.props.topScrollEdgeEffect === "soft");
|
|
47
|
+
if (previousOverlayEnabled && !overlayEnabled && this.state.topOverlayHeight !== null) {
|
|
48
|
+
this.setState({
|
|
49
|
+
topOverlayHeight: null
|
|
50
|
+
});
|
|
51
|
+
}
|
|
45
52
|
const pageCount = this.pages().length;
|
|
46
53
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
47
54
|
if (selectedPage !== this.state.selectedPage) {
|
|
@@ -197,6 +204,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
197
204
|
headerHeight,
|
|
198
205
|
stickyHeaderHeight,
|
|
199
206
|
topOverlay,
|
|
207
|
+
topOverlayHeightHint,
|
|
200
208
|
externalTopOverlayTag,
|
|
201
209
|
topScrollEdgeEffect,
|
|
202
210
|
topOverlayColorScheme,
|
|
@@ -219,6 +227,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
219
227
|
const supportsTopScrollEdge = Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26;
|
|
220
228
|
const scrollEdgeEnabled = supportsTopScrollEdge && (topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft");
|
|
221
229
|
const hasTopOverlay = scrollEdgeEnabled && topOverlay != null;
|
|
230
|
+
const initialTopOverlayHeight = typeof topOverlayHeightHint === "number" && Number.isFinite(topOverlayHeightHint) ? Math.max(0, Math.ceil(topOverlayHeightHint)) : 0;
|
|
222
231
|
const pageKeys = pages.map(pageKey);
|
|
223
232
|
const retained = new Set(this.mountedPages(pages.length));
|
|
224
233
|
const deducedLayoutDirection = !layoutDirection || layoutDirection === "locale" ? I18nManager.isRTL ? "rtl" : "ltr" : layoutDirection;
|
|
@@ -245,7 +254,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
245
254
|
stickyHeaderHeight: Math.max(0, Math.round(stickyHeaderHeight)),
|
|
246
255
|
topScrollEdgeEffect: scrollEdgeEnabled ? topScrollEdgeEffect : "none",
|
|
247
256
|
topOverlayColorScheme: scrollEdgeEnabled ? topOverlayColorScheme : undefined,
|
|
248
|
-
topOverlayHeight: hasTopOverlay ? this.state.topOverlayHeight : 0,
|
|
257
|
+
topOverlayHeight: hasTopOverlay ? this.state.topOverlayHeight ?? initialTopOverlayHeight : 0,
|
|
249
258
|
externalTopOverlayTag: supportsTopScrollEdge && !hasTopOverlay ? externalTopOverlayTag ?? 0 : 0,
|
|
250
259
|
nativeSmoothHeaderScrollEnabled: nativeSmoothHeaderScrollEnabled,
|
|
251
260
|
pageKeys: JSON.stringify(pageKeys),
|
|
@@ -67,6 +67,8 @@ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children"
|
|
|
67
67
|
stickyHeaderHeight: number;
|
|
68
68
|
/** Fixed content above the collapsible header on iOS 26 and newer. */
|
|
69
69
|
topOverlay?: React.ReactNode;
|
|
70
|
+
/** Known overlay height to reserve until its first layout measurement. */
|
|
71
|
+
topOverlayHeightHint?: number;
|
|
70
72
|
/** Existing fixed header shared with a parent pager on iOS 26 and newer. */
|
|
71
73
|
externalTopOverlayTag?: number;
|
|
72
74
|
/** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
|
|
@@ -102,7 +104,7 @@ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children"
|
|
|
102
104
|
type State = {
|
|
103
105
|
selectedPage: number;
|
|
104
106
|
transientRetainedPages: readonly number[];
|
|
105
|
-
topOverlayHeight: number;
|
|
107
|
+
topOverlayHeight: number | null;
|
|
106
108
|
};
|
|
107
109
|
/**
|
|
108
110
|
* Pager variant whose vertical header coordination is performed by the native
|
|
@@ -120,7 +122,7 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
120
122
|
private nativePagerScrollState;
|
|
121
123
|
private nativeTabCommandQueuedDuringScroll;
|
|
122
124
|
componentDidMount(): void;
|
|
123
|
-
componentDidUpdate(): void;
|
|
125
|
+
componentDidUpdate(previousProps: CollapsiblePagerViewProps): void;
|
|
124
126
|
setPage: (selectedPage: number) => void;
|
|
125
127
|
setPageWithoutAnimation: (selectedPage: number) => void;
|
|
126
128
|
scrollToPageTop: (options?: Readonly<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "6.7.0-
|
|
3
|
+
"version": "6.7.0-7",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"typescript": "^5.9.2"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-
|
|
76
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-7",
|
|
77
77
|
"react": "*",
|
|
78
78
|
"react-native": "*"
|
|
79
79
|
},
|
|
@@ -120,6 +120,8 @@ export interface CollapsiblePagerViewProps
|
|
|
120
120
|
stickyHeaderHeight: number;
|
|
121
121
|
/** Fixed content above the collapsible header on iOS 26 and newer. */
|
|
122
122
|
topOverlay?: React.ReactNode;
|
|
123
|
+
/** Known overlay height to reserve until its first layout measurement. */
|
|
124
|
+
topOverlayHeightHint?: number;
|
|
123
125
|
/** Existing fixed header shared with a parent pager on iOS 26 and newer. */
|
|
124
126
|
externalTopOverlayTag?: number;
|
|
125
127
|
/** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
|
|
@@ -158,7 +160,7 @@ export interface CollapsiblePagerViewProps
|
|
|
158
160
|
type State = {
|
|
159
161
|
selectedPage: number;
|
|
160
162
|
transientRetainedPages: readonly number[];
|
|
161
|
-
topOverlayHeight: number;
|
|
163
|
+
topOverlayHeight: number | null;
|
|
162
164
|
};
|
|
163
165
|
|
|
164
166
|
const pageIndex = (value: number) => {
|
|
@@ -186,7 +188,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
186
188
|
State
|
|
187
189
|
> {
|
|
188
190
|
state: State = {
|
|
189
|
-
topOverlayHeight:
|
|
191
|
+
topOverlayHeight: null,
|
|
190
192
|
selectedPage: clampedPageIndex(
|
|
191
193
|
this.props.initialPage ?? 0,
|
|
192
194
|
React.Children.toArray(this.props.children).length
|
|
@@ -219,7 +221,22 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
219
221
|
this.notifyMountedPagesChanged();
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
componentDidUpdate() {
|
|
224
|
+
componentDidUpdate(previousProps: CollapsiblePagerViewProps) {
|
|
225
|
+
const previousOverlayEnabled =
|
|
226
|
+
previousProps.topOverlay != null &&
|
|
227
|
+
(previousProps.topScrollEdgeEffect === "hard" ||
|
|
228
|
+
previousProps.topScrollEdgeEffect === "soft");
|
|
229
|
+
const overlayEnabled =
|
|
230
|
+
this.props.topOverlay != null &&
|
|
231
|
+
(this.props.topScrollEdgeEffect === "hard" ||
|
|
232
|
+
this.props.topScrollEdgeEffect === "soft");
|
|
233
|
+
if (
|
|
234
|
+
previousOverlayEnabled &&
|
|
235
|
+
!overlayEnabled &&
|
|
236
|
+
this.state.topOverlayHeight !== null
|
|
237
|
+
) {
|
|
238
|
+
this.setState({ topOverlayHeight: null });
|
|
239
|
+
}
|
|
223
240
|
const pageCount = this.pages().length;
|
|
224
241
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
225
242
|
if (selectedPage !== this.state.selectedPage) {
|
|
@@ -473,6 +490,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
473
490
|
headerHeight,
|
|
474
491
|
stickyHeaderHeight,
|
|
475
492
|
topOverlay,
|
|
493
|
+
topOverlayHeightHint,
|
|
476
494
|
externalTopOverlayTag,
|
|
477
495
|
topScrollEdgeEffect,
|
|
478
496
|
topOverlayColorScheme,
|
|
@@ -498,6 +516,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
498
516
|
supportsTopScrollEdge &&
|
|
499
517
|
(topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft");
|
|
500
518
|
const hasTopOverlay = scrollEdgeEnabled && topOverlay != null;
|
|
519
|
+
const initialTopOverlayHeight =
|
|
520
|
+
typeof topOverlayHeightHint === "number" &&
|
|
521
|
+
Number.isFinite(topOverlayHeightHint)
|
|
522
|
+
? Math.max(0, Math.ceil(topOverlayHeightHint))
|
|
523
|
+
: 0;
|
|
501
524
|
const pageKeys = pages.map(pageKey);
|
|
502
525
|
const retained = new Set(this.mountedPages(pages.length));
|
|
503
526
|
const deducedLayoutDirection =
|
|
@@ -542,7 +565,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
542
565
|
topOverlayColorScheme={
|
|
543
566
|
scrollEdgeEnabled ? topOverlayColorScheme : undefined
|
|
544
567
|
}
|
|
545
|
-
topOverlayHeight={
|
|
568
|
+
topOverlayHeight={
|
|
569
|
+
hasTopOverlay
|
|
570
|
+
? this.state.topOverlayHeight ?? initialTopOverlayHeight
|
|
571
|
+
: 0
|
|
572
|
+
}
|
|
546
573
|
externalTopOverlayTag={
|
|
547
574
|
supportsTopScrollEdge && !hasTopOverlay
|
|
548
575
|
? externalTopOverlayTag ?? 0
|