@onekeyfe/react-native-pager-view 3.0.170 → 3.0.171

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.
@@ -1437,6 +1437,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1437
1437
  @end
1438
1438
 
1439
1439
  @implementation RNCCollapsiblePagerViewComponentView {
1440
+ #if DEBUG
1441
+ NSUInteger _offsetDiagSequence;
1442
+ #endif
1440
1443
  UIView *_containerView;
1441
1444
  RNCCollapsiblePagerSharedHeaderView *_sharedHeaderHostView;
1442
1445
  UIPageViewController *_pageViewController;
@@ -1491,6 +1494,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1491
1494
  BOOL _needsScrollObserverReattachAfterMount;
1492
1495
  NSHashTable<UIScrollView *> *_mountingRemovedScrollViews;
1493
1496
  __weak UIScrollView *_observedScrollView;
1497
+ NSString *_observedPageKey;
1498
+ NSInteger _observedPageIndex;
1499
+ NSUInteger _observedPageGeneration;
1500
+ NSDictionary<NSString *, NSNumber *> *_transitionPageOffsets;
1501
+ NSMutableIndexSet *_transitionCompensatedPages;
1502
+ #if DEBUG
1503
+ NSInteger _transitionDiagBucket;
1504
+ #endif
1505
+ NSInteger _transitionSourceIndex;
1506
+ BOOL _syntheticPageRestore;
1507
+ BOOL _finishingPageStateTransition;
1508
+ NSMutableSet<NSString *> *_pendingPageRestoreKeys;
1494
1509
  __weak UIScrollView *_sharedHeaderScrollView;
1495
1510
  __weak UIScrollView *_pendingFallbackScrollView;
1496
1511
  __weak UIView *_sharedHeaderPressCancellationGestureHost;
@@ -1525,6 +1540,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1525
1540
  _logicalChildren = [NSMutableArray new];
1526
1541
  _pageControllers = [NSMutableArray new];
1527
1542
  _pageOffsets = [NSMutableDictionary new];
1543
+ _pendingPageRestoreKeys = [NSMutableSet new];
1528
1544
  _releasedPageScrollStates = [NSMutableDictionary new];
1529
1545
  _mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
1530
1546
  _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
@@ -1899,18 +1915,123 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1899
1915
  [self applyHeaderOffset];
1900
1916
  }
1901
1917
 
1918
+ - (void)offsetDiag:(NSString *)reason page:(NSInteger)index scroll:(UIScrollView *)scroll
1919
+ {
1920
+ #if DEBUG
1921
+ NSString *key = index >= 0 && index < _pageControllers.count ? [self pageKeyForIndex:index] : @"none";
1922
+ if (![@[@"portfolio", @"perps", @"defi", @"nft", @"history"] containsObject:key]) key = @"other";
1923
+ NSNumber *saved = index >= 0 && index < _pageControllers.count ? _pageOffsets[[self pageKeyForIndex:index]] : nil;
1924
+ UIView *component = scroll == nil ? nil : RNCScrollComponentView(scroll);
1925
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1926
+ @"[HOME-OFFSET-DIAG] pager phase=%@ seq=%lu wallMs=%.0f mono=%.6f host=%p generation=%lu transitionId=%lu page=%ld key=%@ current=%ld destination=%ld transition=%d dragging=%d scroll=%p tag=%ld component=%p componentTag=%ld observed=%p ownerIndex=%ld ownerGeneration=%lu synthetic=%d cacheExists=%d cache=%.4f header=%.4f headerHeight=%ld physicalY=%.4f insetTop=%.4f insetBottom=%.4f logicalY=%.4f contentHeight=%.4f boundsHeight=%.4f",
1927
+ reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
1928
+ (__bridge void *)self, (unsigned long)_generation, (unsigned long)_transitionId, (long)index, key,
1929
+ (long)_currentIndex, (long)_destinationIndex, _transitioning, _isPagerDragging,
1930
+ (__bridge void *)scroll, (long)scroll.tag, (__bridge void *)component, (long)component.tag,
1931
+ (__bridge void *)_observedScrollView, (long)_observedPageIndex, (unsigned long)_observedPageGeneration, _syntheticPageRestore, saved != nil, saved.doubleValue, _headerOffset, (long)_headerHeight,
1932
+ scroll.contentOffset.y, scroll.contentInset.top, scroll.contentInset.bottom,
1933
+ scroll.contentOffset.y + scroll.contentInset.top, scroll.contentSize.height, scroll.bounds.size.height]);
1934
+ #endif
1935
+ }
1936
+
1902
1937
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
1903
1938
  {
1904
- if (!_nativeSmoothHeaderScrollEnabled || _headerOffset >= _headerHeight ||
1939
+ [self offsetDiag:@"prepare-check" page:pageIndex scroll:nil];
1940
+ if (!_nativeSmoothHeaderScrollEnabled ||
1905
1941
  pageIndex < 0 || pageIndex >= _pageControllers.count) return;
1906
-
1907
- _pageOffsets[[self pageKeyForIndex:pageIndex]] = @(_headerOffset);
1908
1942
  UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:pageIndex];
1909
1943
  if (scrollView != nil && scrollView != _observedScrollView) {
1910
1944
  [self applyInsetsToScrollView:scrollView pageIndex:pageIndex restore:YES];
1911
1945
  }
1912
1946
  }
1913
1947
 
1948
+ - (void)beginPageStateTransition
1949
+ {
1950
+ if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0 || _transitionPageOffsets != nil) return;
1951
+ [self detachScrollObserver];
1952
+ _transitionSourceIndex = _currentIndex;
1953
+ _transitionPageOffsets = [_pageOffsets copy];
1954
+ _transitionCompensatedPages = [NSMutableIndexSet new];
1955
+ #if DEBUG
1956
+ _transitionDiagBucket = -1;
1957
+ [self offsetDiag:[NSString stringWithFormat:@"screen-begin-source=%ld-savedQ=%.4f", (long)_transitionSourceIndex,
1958
+ [_transitionPageOffsets[self.currentPageKey] doubleValue]] page:_currentIndex scroll:nil];
1959
+ #endif
1960
+ }
1961
+
1962
+ - (void)updatePageStateTransitionProgress:(CGFloat)progress
1963
+ {
1964
+ if (_transitionPageOffsets == nil || _finishingPageStateTransition) return;
1965
+ NSInteger first = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
1966
+ NSInteger second = MIN(first + 1, (NSInteger)_pageControllers.count - 1);
1967
+ CGFloat fraction = RNCClamp(progress - first, 0, 1);
1968
+ if (!_isPagerDragging && _destinationIndex != _transitionSourceIndex) {
1969
+ first = _transitionSourceIndex;
1970
+ second = _destinationIndex;
1971
+ fraction = RNCClamp((progress - first) / (second - first), 0, 1);
1972
+ }
1973
+ CGFloat firstQ = [_transitionPageOffsets[[self pageKeyForIndex:first]] doubleValue];
1974
+ CGFloat secondQ = [_transitionPageOffsets[[self pageKeyForIndex:second]] doubleValue];
1975
+ CGFloat firstH = RNCClamp(firstQ, 0, _headerHeight);
1976
+ CGFloat secondH = RNCClamp(secondQ, 0, _headerHeight);
1977
+ _headerOffset = firstH + (secondH - firstH) * fraction;
1978
+ #if DEBUG
1979
+ NSInteger bucket = (NSInteger)floor(fraction * 4);
1980
+ if (_transitionDiagBucket != bucket) {
1981
+ _transitionDiagBucket = bucket;
1982
+ [self offsetDiag:[NSString stringWithFormat:
1983
+ @"screen-progress-kind=%@-source=%ld-target=%ld-sourceQ=%.4f-targetQ=%.4f-position=%.4f-fraction=%.4f",
1984
+ _isPagerDragging ? @"interactive" : @"programmatic-endpoint", (long)first, (long)second,
1985
+ firstQ, secondQ, progress, fraction] page:_currentIndex scroll:nil];
1986
+ }
1987
+ #endif
1988
+ _syntheticPageRestore = YES;
1989
+ NSInteger visibleFirst = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
1990
+ NSInteger visibleLast = MIN(visibleFirst + 1, (NSInteger)_pageControllers.count - 1);
1991
+ NSMutableIndexSet *displayPages = [NSMutableIndexSet indexSetWithIndexesInRange:
1992
+ NSMakeRange(visibleFirst, visibleLast - visibleFirst + 1)];
1993
+ if (!_isPagerDragging) {
1994
+ // UIView tap animation reports its endpoint once while the source exits.
1995
+ [displayPages addIndex:_transitionSourceIndex];
1996
+ [displayPages addIndex:_destinationIndex];
1997
+ }
1998
+ for (NSUInteger index = displayPages.firstIndex; index != NSNotFound;
1999
+ index = [displayPages indexGreaterThanIndex:index]) {
2000
+ UIScrollView *scroll = [self verticalScrollViewForPageAtIndex:index];
2001
+ if (scroll != nil) [self applyInsetsToScrollView:scroll pageIndex:index restore:YES];
2002
+ }
2003
+ _syntheticPageRestore = NO;
2004
+ [self layoutSharedHeaders];
2005
+ }
2006
+
2007
+ - (void)finishPageStateTransition
2008
+ {
2009
+ if (_transitionPageOffsets == nil) return;
2010
+ _finishingPageStateTransition = YES;
2011
+ _headerOffset = RNCClamp([_transitionPageOffsets[self.currentPageKey] doubleValue], 0, _headerHeight);
2012
+ _syntheticPageRestore = YES;
2013
+ // Both completion and cancellation undo every temporary viewport offset.
2014
+ for (NSUInteger index = _transitionCompensatedPages.firstIndex; index != NSNotFound;
2015
+ index = [_transitionCompensatedPages indexGreaterThanIndex:index]) {
2016
+ UIScrollView *scroll = [self verticalScrollViewForPageAtIndex:index];
2017
+ if (scroll == nil) continue;
2018
+ [self applyInsetsToScrollView:scroll pageIndex:index restore:YES];
2019
+ NSString *key = [self pageKeyForIndex:index];
2020
+ if (![_pendingPageRestoreKeys containsObject:key]) {
2021
+ _pageOffsets[key] = @(scroll.contentOffset.y + scroll.contentInset.top);
2022
+ }
2023
+ }
2024
+ _syntheticPageRestore = NO;
2025
+ _finishingPageStateTransition = NO;
2026
+ _transitionPageOffsets = nil;
2027
+ _transitionCompensatedPages = nil;
2028
+ [self layoutSharedHeaders];
2029
+ #if DEBUG
2030
+ [self offsetDiag:[NSString stringWithFormat:@"screen-finish-source=%ld-cancelled=%d", (long)_transitionSourceIndex,
2031
+ _currentIndex == _transitionSourceIndex] page:_currentIndex scroll:nil];
2032
+ #endif
2033
+ }
2034
+
1914
2035
  #pragma mark - React mounting
1915
2036
 
1916
2037
  // OneKey patch: Fabric can recycle a removed list as a descendant of our header
@@ -1958,8 +2079,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1958
2079
  _releasedPageScrollStates[pageKey] = state;
1959
2080
  // Retained pages may be visually aligned to the expanded header while
1960
2081
  // their saved offsets still hold the user's independent list position.
1961
- if (scrollView == _observedScrollView || _pageOffsets[pageKey] == nil) {
2082
+ BOOL ownsObservedPage = scrollView == _observedScrollView &&
2083
+ (!_nativeSmoothHeaderScrollEnabled ||
2084
+ (_observedPageGeneration == _generation && _observedPageIndex == index &&
2085
+ [_observedPageKey isEqualToString:pageKey]));
2086
+ if (_transitionPageOffsets == nil && !_syntheticPageRestore &&
2087
+ (!_nativeSmoothHeaderScrollEnabled || ![_pendingPageRestoreKeys containsObject:pageKey]) &&
2088
+ (ownsObservedPage || _pageOffsets[pageKey] == nil)) {
2089
+ [self offsetDiag:@"mount-release-save-before" page:index scroll:scrollView];
1962
2090
  _pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2091
+ [self offsetDiag:@"mount-release-save-after" page:index scroll:scrollView];
1963
2092
  }
1964
2093
  break;
1965
2094
  }
@@ -2243,7 +2372,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2243
2372
  if (appliedTop != nil && scrollView.refreshControl != nil) {
2244
2373
  original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2245
2374
  }
2246
- scrollView.contentInset = original;
2375
+ [self offsetDiag:@"release-original-inset-before" page:_currentIndex scroll:scrollView];
2376
+ scrollView.contentInset = original;
2377
+ [self offsetDiag:@"release-original-inset-after" page:_currentIndex scroll:scrollView];
2247
2378
  }
2248
2379
  NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
2249
2380
  if (indicatorValue != nil) {
@@ -2295,6 +2426,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2295
2426
  _pagerScrollView = nil;
2296
2427
  [super prepareForRecycle];
2297
2428
  [_pageOffsets removeAllObjects];
2429
+ [_pendingPageRestoreKeys removeAllObjects];
2430
+ _transitionPageOffsets = nil;
2431
+ _transitionCompensatedPages = nil;
2432
+ _syntheticPageRestore = NO;
2433
+ _finishingPageStateTransition = NO;
2298
2434
  _pageKeys = @[];
2299
2435
  _retainedPages = @"[]";
2300
2436
  _currentIndex = 0;
@@ -2627,6 +2763,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2627
2763
  - (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled
2628
2764
  {
2629
2765
  if (_isBeingRecycled) return;
2766
+ if (_nativeSmoothHeaderScrollEnabled &&
2767
+ (_transitioning || _isPagerDragging || _transitionPageOffsets != nil) && !selectionSettled) return;
2768
+ if (selectionSettled) [self finishPageStateTransition];
2630
2769
  if (_needsScrollObserverReattachAfterMount) {
2631
2770
  if (self.window == nil || ((_transitioning || _isPagerDragging) && !selectionSettled)) return;
2632
2771
  [self prepareAdjacentPageInsets];
@@ -2685,20 +2824,34 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2685
2824
  if (_pagerScrollView != nil && !_nativeSmoothHeaderScrollEnabled && !_needsPropsReapply) {
2686
2825
  [candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
2687
2826
  }
2688
- if (candidate == _observedScrollView) {
2827
+ if (candidate == _observedScrollView &&
2828
+ (!_nativeSmoothHeaderScrollEnabled ||
2829
+ ([_observedPageKey isEqualToString:self.currentPageKey] &&
2830
+ _observedPageIndex == _currentIndex && _observedPageGeneration == _generation))) {
2689
2831
  _needsScrollObserverReattachAfterMount = NO;
2832
+ if ([_pendingPageRestoreKeys containsObject:self.currentPageKey]) {
2833
+ [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2834
+ }
2690
2835
  [self attachSharedHeadersToScrollView:candidate];
2691
2836
  [self updateSharedHeaderPressCancellationGesture];
2692
2837
  return;
2693
2838
  }
2694
2839
 
2695
- BOOL replacingCurrentScrollView = _observingContentOffset;
2840
+ [self offsetDiag:@"observer-candidate" page:_currentIndex scroll:candidate];
2841
+ BOOL replacingCurrentScrollView = _observingContentOffset &&
2842
+ (!_nativeSmoothHeaderScrollEnabled || [_observedPageKey isEqualToString:self.currentPageKey]);
2696
2843
  [self detachScrollObserver];
2697
2844
  if (replacingCurrentScrollView) {
2698
- // A list replaced by an empty RN view keeps collapse, not the old row offset.
2699
- _pageOffsets[self.currentPageKey] = @(_headerOffset);
2845
+ // Legacy replacement discards body state; smooth replacement waits for layout.
2846
+ [self offsetDiag:@"observer-replacement-cache-before" page:_currentIndex scroll:candidate];
2847
+ if (!_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2848
+ [self offsetDiag:@"observer-replacement-cache-after" page:_currentIndex scroll:candidate];
2700
2849
  }
2701
2850
  _observedScrollView = candidate;
2851
+ _observedPageKey = [self.currentPageKey copy];
2852
+ _observedPageIndex = _currentIndex;
2853
+ _observedPageGeneration = _generation;
2854
+ [self offsetDiag:@"observer-attached-before-restore" page:_currentIndex scroll:candidate];
2702
2855
  [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2703
2856
  [candidate addObserver:self
2704
2857
  forKeyPath:@"contentOffset"
@@ -2725,7 +2878,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2725
2878
  UIScrollView *scrollView = _observedScrollView;
2726
2879
  if (scrollView != nil) {
2727
2880
  [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
2728
- _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2881
+ [self offsetDiag:@"detach-save-before" page:_currentIndex scroll:scrollView];
2882
+ NSString *ownerKey = _nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey;
2883
+ BOOL ownerKeyExists = ownerKey != nil && ([_pageKeys containsObject:ownerKey] ||
2884
+ (_observedPageIndex >= 0 && _observedPageIndex < _pageControllers.count &&
2885
+ [[self pageKeyForIndex:_observedPageIndex] isEqualToString:ownerKey]));
2886
+ if (ownerKey != nil && (!_nativeSmoothHeaderScrollEnabled ||
2887
+ (ownerKeyExists && _observedPageGeneration == _generation && _transitionPageOffsets == nil &&
2888
+ !_syntheticPageRestore && ![_pendingPageRestoreKeys containsObject:ownerKey]))) {
2889
+ _pageOffsets[ownerKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2890
+ }
2891
+ [self offsetDiag:@"detach-save-after" page:_currentIndex scroll:scrollView];
2729
2892
  if (_observingContentOffset) {
2730
2893
  [scrollView removeObserver:self
2731
2894
  forKeyPath:@"contentOffset"
@@ -2740,6 +2903,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2740
2903
  }
2741
2904
  _observingContentOffset = NO;
2742
2905
  _observedScrollView = nil;
2906
+ _observedPageKey = nil;
2907
+ _observedPageIndex = NSNotFound;
2743
2908
  if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
2744
2909
  [(RNCNativeScrollerComponentView *)scrollView.superview stopScrolling];
2745
2910
  }
@@ -3010,7 +3175,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3010
3175
  [self releaseRefreshControlForScrollView:scrollView];
3011
3176
  if (scrollView == _observedScrollView) {
3012
3177
  [self detachScrollObserver];
3013
- if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3178
+ [self offsetDiag:resetSavedOffset ? @"release-reset-before" : @"release-retain-before" page:_currentIndex scroll:scrollView];
3179
+ if (resetSavedOffset && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3180
+ [self offsetDiag:@"release-reset-after" page:_currentIndex scroll:scrollView];
3014
3181
  }
3015
3182
  if (scrollView == _sharedHeaderScrollView) {
3016
3183
  [self restoreSharedHeadersToContainer];
@@ -3030,8 +3197,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3030
3197
  [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3031
3198
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
3032
3199
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
3200
+ [self offsetDiag:@"release-original-offset-before" page:_currentIndex scroll:scrollView];
3033
3201
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
3034
3202
  animated:NO];
3203
+ [self offsetDiag:@"release-original-offset-after" page:_currentIndex scroll:scrollView];
3035
3204
  [_originalInsets removeObjectForKey:scrollView];
3036
3205
  [_appliedTopInsets removeObjectForKey:scrollView];
3037
3206
  [_originalIndicatorInsets removeObjectForKey:scrollView];
@@ -3060,8 +3229,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3060
3229
  pageIndex:(NSInteger)pageIndex
3061
3230
  restore:(BOOL)restore
3062
3231
  {
3063
- // OneKey patch: a surviving list keeps its full offset. A replacement keeps
3064
- // only header collapse, even if Fabric reused the same native scroll object.
3232
+ // Smooth pages retain their complete offset while replacement identity and
3233
+ // layout settle. Legacy pages retain the historical header-only policy.
3234
+ [self offsetDiag:restore ? @"restore-enter" : @"insets-enter" page:pageIndex scroll:scrollView];
3065
3235
  NSString *pageKey = [self pageKeyForIndex:pageIndex];
3066
3236
  RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
3067
3237
  BOOL deferredReleasedIdentity = NO;
@@ -3107,8 +3277,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3107
3277
  deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
3108
3278
  RNCCollapsiblePagerFallbackDebounceInterval;
3109
3279
  }
3280
+ #if DEBUG
3281
+ [self offsetDiag:[NSString stringWithFormat:@"released-identity-match=%d-deferred=%d-oldTag=%ld-oldScroll=%p", sameList, deferredReleasedIdentity, (long)released.componentTag, (__bridge void *)released.scrollView] page:pageIndex scroll:scrollView];
3282
+ #endif
3110
3283
  if (!deferredReleasedIdentity) {
3111
- if (!sameList) _pageOffsets[pageKey] = @(_headerOffset);
3284
+ [self offsetDiag:@"released-identity-cache-before" page:pageIndex scroll:scrollView];
3285
+ if (!sameList && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[pageKey] = @(_headerOffset);
3286
+ [self offsetDiag:@"released-identity-cache-after" page:pageIndex scroll:scrollView];
3112
3287
  [_releasedPageScrollStates removeObjectForKey:pageKey];
3113
3288
  }
3114
3289
  }
@@ -3156,7 +3331,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3156
3331
  if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
3157
3332
  return;
3158
3333
  }
3334
+ [self offsetDiag:@"inset-assignment-before" page:pageIndex scroll:scrollView];
3159
3335
  scrollView.contentInset = next;
3336
+ [self offsetDiag:@"inset-assignment-after" page:pageIndex scroll:scrollView];
3160
3337
  UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
3161
3338
  indicatorInsets.top = pagerTopInset;
3162
3339
  scrollView.verticalScrollIndicatorInsets = indicatorInsets;
@@ -3164,21 +3341,37 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3164
3341
 
3165
3342
  CGFloat targetOffset = previousLogicalOffset - next.top;
3166
3343
  if (restore) {
3167
- NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
3344
+ NSNumber *saved = _transitionPageOffsets != nil
3345
+ ? _transitionPageOffsets[pageKey] : _pageOffsets[pageKey];
3168
3346
  // The sticky slot can change height between pages. Cache offsets relative
3169
3347
  // to content start so a round trip does not add that height difference.
3170
- CGFloat restoredLogicalOffset = deferredReleasedIdentity || saved == nil
3171
- ? _headerOffset
3172
- : saved.doubleValue;
3173
- // While the shared header is visible, retained pages must agree on its
3174
- // collapse position. Deep offsets remain independent after full collapse.
3175
- if (_nativeSmoothHeaderScrollEnabled && _headerOffset < _headerHeight) {
3176
- restoredLogicalOffset = _headerOffset;
3348
+ CGFloat restoredLogicalOffset;
3349
+ if (_nativeSmoothHeaderScrollEnabled && _headerHeight > 0) {
3350
+ restoredLogicalOffset = saved.doubleValue;
3351
+ if (_transitionPageOffsets != nil && !_finishingPageStateTransition) {
3352
+ [_transitionCompensatedPages addIndex:pageIndex];
3353
+ restoredLogicalOffset = MAX(restoredLogicalOffset - _headerHeight, 0) + _headerOffset;
3354
+ }
3355
+ if (deferredReleasedIdentity || scrollView.bounds.size.height <= 0 || scrollView.contentSize.height <= 0) {
3356
+ [_pendingPageRestoreKeys addObject:pageKey];
3357
+ return;
3358
+ }
3359
+ [_pendingPageRestoreKeys removeObject:pageKey];
3360
+ } else {
3361
+ restoredLogicalOffset = deferredReleasedIdentity || saved == nil ? _headerOffset : saved.doubleValue;
3177
3362
  }
3363
+ #if DEBUG
3364
+ [self offsetDiag:[NSString stringWithFormat:@"restore-candidate-saved=%.4f-headerAlignedLogical=%.4f", saved.doubleValue, restoredLogicalOffset] page:pageIndex scroll:scrollView];
3365
+ #endif
3178
3366
  targetOffset = restoredLogicalOffset - next.top;
3179
- targetOffset = MAX(targetOffset, -next.top + _headerOffset);
3367
+ if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0) {
3368
+ targetOffset = MAX(targetOffset, -next.top + _headerOffset);
3369
+ }
3180
3370
  // An inactive list may shrink after its offset observer was detached.
3181
3371
  // Do not overwrite its corrected offset with a now-unreachable cached one.
3372
+ #if DEBUG
3373
+ [self offsetDiag:[NSString stringWithFormat:@"restore-clamp-before-candidate=%.4f-max=%.4f", targetOffset, MAX(-next.top, scrollView.contentSize.height - scrollView.bounds.size.height + next.bottom)] page:pageIndex scroll:scrollView];
3374
+ #endif
3182
3375
  targetOffset = RNCLimitRestoredScrollOffset(
3183
3376
  targetOffset,
3184
3377
  scrollView.contentSize.height,
@@ -3187,11 +3380,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3187
3380
  next.bottom
3188
3381
  );
3189
3382
  }
3383
+ #if DEBUG
3384
+ [self offsetDiag:[NSString stringWithFormat:@"restore-final-target=%.4f-restore=%d", targetOffset, restore] page:pageIndex scroll:scrollView];
3385
+ #endif
3190
3386
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
3191
3387
  if (restore || previousTopInset != next.top) {
3192
3388
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
3193
3389
  }
3194
3390
  if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
3391
+ [self offsetDiag:@"restore-post" page:pageIndex scroll:scrollView];
3195
3392
  }
3196
3393
 
3197
3394
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
@@ -3252,10 +3449,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3252
3449
  if (context == RNCScrollRefreshControlContext) {
3253
3450
  UIScrollView *scrollView = (UIScrollView *)object;
3254
3451
  [self updateRefreshControlForScrollView:scrollView];
3255
- if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
3452
+ if (scrollView == _observedScrollView && !_syntheticPageRestore &&
3453
+ (!_nativeSmoothHeaderScrollEnabled || (!_transitioning && _transitionPageOffsets == nil))) {
3454
+ [self updateHeaderForScrollView:scrollView];
3455
+ }
3256
3456
  return;
3257
3457
  }
3258
3458
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
3459
+ if (_nativeSmoothHeaderScrollEnabled &&
3460
+ (_observedPageGeneration != _generation || _observedPageKey == nil ||
3461
+ _observedPageIndex != _currentIndex || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3462
+ _transitioning || _isPagerDragging ||
3463
+ _transitionPageOffsets != nil || _syntheticPageRestore)) return;
3464
+ if (_nativeSmoothHeaderScrollEnabled && [_pendingPageRestoreKeys containsObject:_observedPageKey]) {
3465
+ if ([keyPath isEqualToString:@"contentSize"]) {
3466
+ [self applyInsetsToScrollView:_observedScrollView pageIndex:_observedPageIndex restore:YES];
3467
+ }
3468
+ return;
3469
+ }
3259
3470
  if ([keyPath isEqualToString:@"contentInset"]) {
3260
3471
  [self updateHeaderForScrollView:(UIScrollView *)object];
3261
3472
  return;
@@ -3270,7 +3481,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3270
3481
  }
3271
3482
  UIScrollView *scrollView = (UIScrollView *)object;
3272
3483
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3273
- _pageOffsets[self.currentPageKey] = @(_currentLogicalOffset);
3484
+ #if DEBUG
3485
+ BOOL offsetDiagChanged = _pageOffsets[self.currentPageKey] == nil || _pageOffsets[self.currentPageKey].doubleValue != _currentLogicalOffset;
3486
+ if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-before" page:_currentIndex scroll:scrollView];
3487
+ #endif
3488
+ _pageOffsets[_nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey] = @(_currentLogicalOffset);
3489
+ #if DEBUG
3490
+ if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-after" page:_currentIndex scroll:scrollView];
3491
+ #endif
3274
3492
  [self updateHeaderForScrollView:scrollView];
3275
3493
  return;
3276
3494
  }
@@ -3279,6 +3497,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3279
3497
 
3280
3498
  - (void)updateHeaderForScrollView:(UIScrollView *)scrollView
3281
3499
  {
3500
+ if (_nativeSmoothHeaderScrollEnabled &&
3501
+ (_syntheticPageRestore || _transitionPageOffsets != nil)) return;
3282
3502
  CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3283
3503
  _currentLogicalOffset = logicalOffset;
3284
3504
  _headerOffset = MIN(MAX(logicalOffset, 0), _headerHeight);
@@ -3322,6 +3542,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3322
3542
 
3323
3543
  - (void)goTo:(NSInteger)index animated:(BOOL)animated
3324
3544
  {
3545
+ [self offsetDiag:@"go-to" page:index scroll:_observedScrollView];
3325
3546
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
3326
3547
  _hasReceivedPageCommand = YES;
3327
3548
  _hasAppliedInitialPage = YES;
@@ -3347,6 +3568,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3347
3568
  return;
3348
3569
  }
3349
3570
 
3571
+ [self beginPageStateTransition];
3350
3572
  [self preparePageForHorizontalTransitionAtIndex:index];
3351
3573
  _destinationIndex = index;
3352
3574
  [self liftSharedHeadersForPagerTransition:@"programmatic"];
@@ -3366,6 +3588,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3366
3588
  void (^animations)(void) = ^{
3367
3589
  [self->_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0)];
3368
3590
  [self->_nativeTabBarView setProgress:index];
3591
+ [self updatePageStateTransitionProgress:index];
3369
3592
  };
3370
3593
  void (^completion)(BOOL) = ^(BOOL finished) {
3371
3594
  __strong __typeof__(weakSelf) self = weakSelf;
@@ -3373,6 +3596,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3373
3596
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
3374
3597
  self->_pagerScrollView.scrollEnabled = self->_scrollEnabled;
3375
3598
  if (finished) {
3599
+ [self detachScrollObserver];
3376
3600
  self->_currentIndex = index;
3377
3601
  self->_destinationIndex = index;
3378
3602
  [self->_nativeTabBarView setProgress:index];
@@ -3410,6 +3634,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3410
3634
  if (self == nil || self->_generation != capturedGeneration ||
3411
3635
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
3412
3636
  if (finished) {
3637
+ [self detachScrollObserver];
3413
3638
  self->_currentIndex = index;
3414
3639
  self->_destinationIndex = index;
3415
3640
  [self->_nativeTabBarView setProgress:index];
@@ -3434,6 +3659,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3434
3659
  dispatch_async(dispatch_get_main_queue(), ^{
3435
3660
  if (self->_generation != generation || self->_transitionId != transition ||
3436
3661
  self->_isBeingRecycled) return;
3662
+ [self finishPageStateTransition];
3437
3663
  self->_transitioning = NO;
3438
3664
  if (self->_hasPendingDirectNativePagerChange) {
3439
3665
  BOOL enabled = self->_pendingDirectNativePagerEnabled;
@@ -3497,9 +3723,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3497
3723
 
3498
3724
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
3499
3725
  {
3726
+ [self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
3500
3727
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3501
3728
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3502
3729
  }
3730
+ [self beginPageStateTransition];
3503
3731
  if (_directNativePagerEnabled) {
3504
3732
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex - 1];
3505
3733
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex + 1];
@@ -3651,6 +3879,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3651
3879
  NSInteger position = (NSInteger)floor(progress);
3652
3880
  CGFloat offset = progress - position;
3653
3881
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
3882
+ [self updatePageStateTransitionProgress:progress];
3654
3883
  [_nativeTabBarView setProgress:progress];
3655
3884
  const auto emitter = [self eventEmitter];
3656
3885
  if (emitter) {
@@ -3668,6 +3897,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3668
3897
  CGFloat transitionProgress = RNCClamp(fabs(rawOffset), 0, 1);
3669
3898
  tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
3670
3899
  }
3900
+ [self updatePageStateTransitionProgress:tabProgress];
3671
3901
  [_nativeTabBarView setProgress:tabProgress];
3672
3902
  const auto emitter = [self eventEmitter];
3673
3903
  if (emitter) {
@@ -3685,6 +3915,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3685
3915
 
3686
3916
  - (void)emitPageSelected:(NSInteger)position
3687
3917
  {
3918
+ [self offsetDiag:@"selection-complete" page:position scroll:_observedScrollView];
3688
3919
  const auto emitter = [self eventEmitter];
3689
3920
  if (emitter) emitter->onPageSelected({.position = (double)position});
3690
3921
  }
@@ -33,6 +33,9 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
33
33
  @end
34
34
 
35
35
  @implementation RNCNativeScrollerComponentView {
36
+ #if DEBUG
37
+ NSUInteger _offsetDiagSequence;
38
+ #endif
36
39
  RNCNativeScrollerShadowNode::ConcreteState::Shared _state;
37
40
  RNCNativeVerticalScrollView *_scrollView;
38
41
  UIView<RCTComponentViewProtocol> *_reactContent;
@@ -51,6 +54,24 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
51
54
  NSNumber *_lastContentViewportHeight;
52
55
  }
53
56
 
57
+ - (void)offsetDiag:(NSString *)reason
58
+ {
59
+ #if DEBUG
60
+ Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog") ?: NSClassFromString(@"OneKeyLog");
61
+ SEL selector = NSSelectorFromString(@"debug::");
62
+ if (logClass == nil || ![logClass respondsToSelector:selector]) return;
63
+ NSString *message = [NSString stringWithFormat:
64
+ @"[HOME-OFFSET-DIAG] scroller phase=%@ seq=%lu wallMs=%.0f mono=%.6f host=%p tag=%ld scroll=%p generation=%lu physicalY=%.4f insetTop=%.4f logicalY=%.4f contentHeight=%.4f boundsHeight=%.4f adjustedBottom=%.4f needsClamp=%d drag=%d decel=%d refresh=%d",
65
+ reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
66
+ (__bridge void *)self, (long)self.tag, (__bridge void *)_scrollView, (unsigned long)_generation,
67
+ _scrollView.contentOffset.y, _scrollView.contentInset.top, _scrollView.contentOffset.y + _scrollView.contentInset.top,
68
+ _scrollView.contentSize.height, _scrollView.bounds.size.height, _scrollView.adjustedContentInset.bottom,
69
+ _needsContentClamp, _scrollView.dragging, _scrollView.decelerating, _refreshControl.refreshing];
70
+ typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
71
+ ((LogFunction)[logClass methodForSelector:selector])(logClass, selector, @"NativeScroller", message);
72
+ #endif
73
+ }
74
+
54
75
  + (ComponentDescriptorProvider)componentDescriptorProvider
55
76
  {
56
77
  return concreteComponentDescriptorProvider<RNCNativeScrollerComponentDescriptor>();
@@ -179,7 +200,9 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
179
200
  _needsContentClamp |= size.height < _scrollView.contentSize.height;
180
201
  CGPoint offset = _scrollView.contentOffset;
181
202
  BOOL wasPulling = offset.y < -_scrollView.adjustedContentInset.top;
203
+ [self offsetDiag:@"content-size-before"];
182
204
  _scrollView.contentSize = size;
205
+ [self offsetDiag:@"content-size-after"];
183
206
  // UIKit can synchronously discard a pull when content becomes empty.
184
207
  // Upper-range shrink correction remains owned by the deferred clamp.
185
208
  if (wasPulling && _scrollView.contentOffset.y != offset.y) {
@@ -197,10 +220,12 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
197
220
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
198
221
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
199
222
  {
223
+ [self offsetDiag:@"layout-before"];
200
224
  CGSize previous = _scrollView.bounds.size;
201
225
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
202
226
  [self updateFabricContentOrigin];
203
227
  [self emitContentViewportChangeIfNeeded];
228
+ [self offsetDiag:@"layout-after"];
204
229
  if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
205
230
  _needsContentClamp = YES;
206
231
  [self scheduleContentClamp];
@@ -263,6 +288,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
263
288
  - (void)scheduleContentClamp
264
289
  {
265
290
  if (!_needsContentClamp) return;
291
+ [self offsetDiag:@"clamp-scheduled"];
266
292
  NSUInteger generation = _generation;
267
293
  __weak __typeof__(self) weakSelf = self;
268
294
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -274,13 +300,19 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
274
300
 
275
301
  - (void)clampContentOffsetIfNeeded
276
302
  {
303
+ [self offsetDiag:@"clamp-enter-or-deferred"];
277
304
  if (!_needsContentClamp || _scrollView.dragging || _scrollView.decelerating ||
278
305
  _refreshControl.refreshing || _scrollView.bounds.size.height <= 0) return;
279
306
  _needsContentClamp = NO;
280
307
  CGFloat maximum = MAX(-_scrollView.adjustedContentInset.top,
281
308
  _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.adjustedContentInset.bottom);
309
+ #if DEBUG
310
+ [self offsetDiag:[NSString stringWithFormat:@"clamp-candidate-max=%.4f", maximum]];
311
+ #endif
282
312
  if (_scrollView.contentOffset.y > maximum) {
313
+ [self offsetDiag:@"clamp-applied-before"];
283
314
  [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, maximum) animated:NO];
315
+ [self offsetDiag:@"clamp-applied-after"];
284
316
  }
285
317
  }
286
318
 
@@ -319,6 +351,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
319
351
  - (void)stopScrolling
320
352
  {
321
353
  if (_stopping) return;
354
+ [self offsetDiag:@"stop-before"];
322
355
  _stopping = YES;
323
356
  [_scrollView setContentOffset:_scrollView.contentOffset animated:NO];
324
357
  if (_dragActive) {
@@ -330,6 +363,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
330
363
  }
331
364
  [self finishMomentum];
332
365
  _stopping = NO;
366
+ [self offsetDiag:@"stop-after"];
333
367
  }
334
368
 
335
369
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView