@onekeyfe/react-native-pager-view 6.7.0-alpha.327 → 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.
package/README.md CHANGED
@@ -67,6 +67,15 @@ Web scroll child can opt in by exposing a DOM element with
67
67
  `data-collapsible-pager-scroll`. CSS scroll timelines drive header/sticky
68
68
  movement, so React is not updated on each vertical scroll frame.
69
69
 
70
+ On Web, `WebCollapsiblePagerView` is an explicit root export of the existing Web
71
+ pager. It measures Header and sticky content when their heights are omitted.
72
+ `isActive` describes route visibility; the pager preserves a pending scroll
73
+ target across a short or temporarily hidden page and retries when real content
74
+ geometry becomes ready. `useWebCollapsiblePagerPage()` exposes the selected
75
+ page's scroll owner, visible metrics, content registration and pending target
76
+ to descendants. These Web-only APIs require a DOM when mounted. Native callers
77
+ continue to use `CollapsiblePagerView`.
78
+
70
79
  On iOS and Android, `nativeTabBar` renders the tab bar natively and
71
80
  `onNativeTabPress` reports presses. By default, adjacent tab presses animate and
72
81
  non-adjacent presses jump directly to the target without passing intermediate
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. It binds UIKit's
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 or opaque material. It clips scrolling assets above
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
- Experimental trial: `CollapsiblePagerView` paints a cover at each owned
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; rendered
82
- light/dark, scrolling, and transition acceptance is pending. Standalone
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
@@ -841,3 +856,62 @@ The native Home Spotlight adapter consumes this snapshot in the main UI runtime
841
856
  Removing a page or clearing the page collection MUST cancel the active horizontal transition and its temporary display snapshots before notifying the adapter. Cancellation MUST NOT rewrite the shared Header collapse or surviving pages' saved body reading memory. The previous observed owner MUST be detached without committing an offset under a shifted page key.
842
857
 
843
858
  An empty adapter MUST reject transition starts and transition completion without coercing an index into an empty range. The selected index uses zero as an empty-state sentinel; nonempty mutations clamp it to the remaining valid range. Readding pages MUST allow a fresh selection and MUST NOT resume a removed transition source. Conformance requires actual Android active-transition → zero-pages → readd/selection regression and remaining-page mutation checks; structural checks and compilation alone do not establish runtime behavior.
859
+
860
+
861
+ ## Web parent-scroll owner identity
862
+
863
+ Web Header and sticky overlays use the named scroll timeline only when the
864
+ browser supports every CSS property needed for that linkage. Otherwise, the
865
+ active scroll owner's offset drives the same clamped Header collapse in JS:
866
+ `C = min(max(scrollTop, 0), max(headerHeight, 0))`. The fallback keeps the
867
+ existing page owner, insets, body offset memory, and sticky anchor. It updates
868
+ when scrolling, selecting or restoring a page, replacing an owner, or changing
869
+ Header height, and releases its listener when that owner detaches. An inactive
870
+ page cannot move the visible Header. This is a Web-only compatibility path; no
871
+ native behavior or public prop changes.
872
+ When an overlay forwards a vertical wheel or touch scroll to the active owner,
873
+ the owner receives the same internal user-input notification used by route
874
+ restoration to cancel an obsolete pending offset.
875
+
876
+ On Web, an explicitly marked NativeScroller is selected before an implicit NativeList viewport. There is one vertical owner per page; callers using parent-owned NativeList pass that same scroll element as webScrollHost.
877
+
878
+ The existing NativeScroller pagerScrollKey identifies a Web page scroll owner. Changing the key invalidates that page body offset without remounting its React business content. The active owner also resets the shared header collapse and cancels queued old restore/top commands. Inactive owners invalidate their saved body offset without imperative scrolling; selection checks the current identity before restoring. A→B→A identity changes never revive a prior owner offset. Stable keys preserve ordinary page C+B restoration. Current-page top commands continue to affect only the selected page.
879
+
880
+ Identity notifications and DOM owner listeners are internal implementation details, removed when the pager root or owner is replaced/unmounted. No native behavior or public configuration is added.
881
+
882
+ ### Web overlay input and keyed-page updates
883
+
884
+ - Header and sticky slots remain overlays: overflow outside the sticky slot may remain visible without adding its portal height to page insets twice.
885
+ - Vertical wheel or touch input over those overlays transfers only to the active page scroll owner when no nested scrollable consumes it. Horizontal controls, editable inputs, and ordinary button/tap behavior retain their own events. Input originating inside the page owner follows native scrolling once.
886
+ - Reordering or removing keyed pages must save the previously configured key and owner, rather than resolving the old selected index against new children. Caller index synchronization preserves keyed body offsets and shared header collapse.
887
+ - For an explicit NativeScroller, Pager chrome insets apply to the existing ScrollView content element, not its viewport. Insets taller than the available window cannot enlarge the scroll viewport. Existing owner/content padding is preserved and restored separately from Pager insets; direct NativeList/page-host fallback retains its prior layout path.
888
+ - The Pager root clips without becoming a programmatically scrollable overflow box. Browser focus and descendant scrollIntoView cannot add a second horizontal page offset outside the selected-page transform.
889
+
890
+ ## Web page geometry and route restoration
891
+
892
+ Status: Implemented in Web source and focused tests; browser and native runtime acceptance remain separate.
893
+
894
+ `WebCollapsiblePagerView` is the existing Web `CollapsiblePagerView` class under an explicit root export. Its `isActive` prop defaults to true and represents route visibility, independently of the selected page. The Web-only `useWebCollapsiblePagerPage()` hook returns null outside a Web pager and otherwise exposes `isFocused`, `isRouteActive`, `owner`, `content`, optional `contentHeight`, `width`, `height`, content-relative `scrollTop`, `registerContent`, and read-only `pendingRouteScrollTop`. Native `CollapsiblePagerView`, its bridge, and its props are unchanged. The Web component rejects mounting without a DOM; importing the root or Web module alone must not access DOM globals.
895
+
896
+ Web callers may supply finite nonnegative `headerHeight` and `stickyHeaderHeight`. When omitted or invalid (negative, NaN, or infinite), the SDK measures the natural inner Header and sticky content with `ResizeObserver`; explicit valid values take precedence, including zero. An initial unmeasured zero is not ready for route restoration; a measured empty chrome element may legitimately have zero height. Measurement must not use the constrained slot height as its own input. Height changes update the owned insets before route restoration. Existing explicit-height callers retain their layout path.
897
+
898
+ Each page has one SDK-discovered vertical owner: explicit `NativeScroller`, then implicit NativeList viewport, then page host. Registration accepts a connected content element and optional finite nonnegative explicit height; invalid height metadata is ignored. It does not change DOM layout or make a pending target reachable. A later registration of the same element supersedes an earlier one, and stale cleanup cannot remove it. An explicit-height change schedules geometry measurement even if the element border box is unchanged. Metrics use owner client width; `pinnedBottom = ownerRect.top + max(0, headerHeight - owner.scrollTop) + stickyHeaderHeight`; `visibleTop = max(ownerRect.top, pinnedBottom, contentRect.top if present)`; visible height is `max(0, ownerRect.bottom - visibleTop)` and content-relative scroll top is `max(0, pinnedBottom - contentRect.top)`. Unregistered content falls back to the first owner child. Detached content and callbacks from old owners are ignored. One animation-frame measurement per page coalesces scroll, resize, registration, owner configuration, and active-page changes. Disposal releases observers, listeners, frames, and subscriptions.
899
+
900
+ On active-to-inactive transition, the Web pager freezes the selected logical React page key, `pagerScrollKey` identity, actual owner generation, and last valid raw DOM top. Inactive scroll/geometry changes cannot replace the snapshot. A second blur of the same pending identity preserves the original target. On focus, owner, content, chrome, or inset changes, the pager retries without a timer only when the route is active, that exact key/identity/owner remains selected and connected, the viewport has positive height, the pager has configured the expected insets, and real `scrollHeight - clientHeight` reaches the target. Short content keeps the target pending. Restoration clears pending before one instant scroll and synchronizes the existing shared Header C and per-page body B memory. An idempotent Web `restoreScrollPosition()` returns false while inactive or not ready and true when no target remains or it applies; normal callers need no polling.
901
+
902
+ Valid vertical wheel, scroll keys, scrollbar drag, overlay-forwarded input, accepted NativeList commands, and explicit page-top/sticky-top commands cancel pending route intent through the existing SDK takeover path. As in the prior Web app adapter, any owner `pointerdown` or `touchstart`, including a child target, cancels only route pending; this does not newly commit or clear SDK C+B memory. Geometry-only scroll/resize/scrollend cannot cancel intent. Valid same-offset commands cancel it; invalid or stale commands do not. A matching route target takes priority over automatic tab C+B restoration. Selection of a different key, owner identity reset (including A→B→A), genuine DOM owner replacement, or removal cancels pending; a same-key index reorder preserves it. A same-commit blur and Activity/Offscreen effect disconnect captures the current route target before releasing the owner; repeated disconnects preserve an existing pending target. A temporary React ref null or Activity/Offscreen effect disconnect followed by the identical connected owner preserves the logical route target and owner generation while releasing all observers, listeners, frames, and DOM configuration during disconnect. Restoration is unavailable while disconnected; reconnect remeasures automatic chrome before applying pending. A truly removed pager instance cannot transfer its pending target to a new instance. Older queued tab/top callbacks must validate generation before writing after route restoration or cancellation. The app owns navigation focus and business tab names; the SDK never infers them from escaped React child keys.
903
+
904
+ Platform matrix: Web owns this DOM geometry, registration, route pending, and explicit Web export. iOS and Android keep native scrolling, restoration, and page coordination documented above, with no new prop, native bridge call, JS heap sharing, or process resource. The UI main runtime owns Web state; background runtimes do not deserialize it. Source/type tests establish only Web contract implementation, not browser rendering or native device behavior. Acceptance requires real Web/Desktop route blur, short content then growth, user takeover, dynamic chrome, key reset, and Home/Market scroll regressions.
905
+
906
+ ## Native package entry isolation
907
+
908
+ The Native package entry exports the existing Native PagerView,
909
+ CollapsiblePagerView, NativeScroller, usePagerView and ScrollEdgeEffect APIs.
910
+ Native package resolution MUST NOT evaluate the Web pager or Web page-context
911
+ modules. The react-native field and conditional export resolve to a real Native
912
+ entry; the browser entry retains the Web runtime APIs. The generic declaration
913
+ entry preserves the existing shared Web value/type API for TypeScript callers.
914
+ This separates package initialization in the Native UI main runtime; it does not
915
+ add a bridge, share JS heaps with background, or change Native scroll commands.
916
+ Acceptance includes the actual Native dependency graph and unchanged startup
917
+ budgets, Web runtime/type exports, and package source/module/declaration entries.
@@ -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
- return _topOverlayView != nil ? _topOverlayHeight : _externalTopOverlayHeight;
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:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
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 {