@onekeyfe/react-native-pager-view 3.0.173 → 3.0.174

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;
@@ -1478,6 +1481,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1478
1481
  NSArray<NSString *> *_pageKeys;
1479
1482
  NSString *_retainedPages;
1480
1483
  NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
1484
+ NSMutableSet<NSString *> *_commandBodyKeys;
1485
+ __weak UIScrollView *_pagerTopScrollView;
1486
+ NSString *_pagerTopPageKey;
1487
+ NSUInteger _pagerTopGeneration;
1488
+ NSInteger _pagerTopPageIndex;
1489
+ CGFloat _pagerTopLogicalTarget;
1490
+ BOOL _dispatchingPagerTop;
1491
+ NSUInteger _pagerTopToken;
1492
+ NSMutableSet<NSString *> *_alignedBodyKeys;
1493
+ NSMutableSet<NSString *> *_suspendedBodyKeys;
1494
+ CGFloat _transitionHeaderOffset;
1495
+ CGFloat _transitionSourceDisplayBody;
1496
+ NSMutableDictionary<NSString *, NSNumber *> *_displayBodyOverrides;
1481
1497
  NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
1482
1498
  NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
1483
1499
  NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
@@ -1491,6 +1507,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1491
1507
  BOOL _needsScrollObserverReattachAfterMount;
1492
1508
  NSHashTable<UIScrollView *> *_mountingRemovedScrollViews;
1493
1509
  __weak UIScrollView *_observedScrollView;
1510
+ NSString *_observedPageKey;
1511
+ NSInteger _observedPageIndex;
1512
+ NSUInteger _observedPageGeneration;
1513
+ NSDictionary<NSString *, NSNumber *> *_transitionPageOffsets;
1514
+ NSMutableIndexSet *_transitionCompensatedPages;
1515
+ #if DEBUG
1516
+ NSInteger _transitionDiagBucket;
1517
+ #endif
1518
+ NSInteger _transitionSourceIndex;
1519
+ BOOL _syntheticPageRestore;
1520
+ BOOL _finishingPageStateTransition;
1521
+ NSMutableSet<NSString *> *_pendingPageRestoreKeys;
1494
1522
  __weak UIScrollView *_sharedHeaderScrollView;
1495
1523
  __weak UIScrollView *_pendingFallbackScrollView;
1496
1524
  __weak UIView *_sharedHeaderPressCancellationGestureHost;
@@ -1525,6 +1553,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1525
1553
  _logicalChildren = [NSMutableArray new];
1526
1554
  _pageControllers = [NSMutableArray new];
1527
1555
  _pageOffsets = [NSMutableDictionary new];
1556
+ _commandBodyKeys = [NSMutableSet new];
1557
+ _alignedBodyKeys = [NSMutableSet new];
1558
+ _suspendedBodyKeys = [NSMutableSet new];
1559
+ _displayBodyOverrides = [NSMutableDictionary new];
1560
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nativeListScrollCommandAccepted:)
1561
+ name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
1562
+ _pendingPageRestoreKeys = [NSMutableSet new];
1528
1563
  _releasedPageScrollStates = [NSMutableDictionary new];
1529
1564
  _mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
1530
1565
  _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
@@ -1668,6 +1703,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1668
1703
 
1669
1704
  - (void)dealloc
1670
1705
  {
1706
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
1671
1707
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1672
1708
  [self updateNativeScrollerViewport:scrollView stickyHeight:0];
1673
1709
  }
@@ -1899,18 +1935,124 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1899
1935
  [self applyHeaderOffset];
1900
1936
  }
1901
1937
 
1938
+ - (void)offsetDiag:(NSString *)reason page:(NSInteger)index scroll:(UIScrollView *)scroll
1939
+ {
1940
+ #if DEBUG
1941
+ NSString *key = index >= 0 && index < _pageControllers.count ? [self pageKeyForIndex:index] : @"none";
1942
+ if (![@[@"portfolio", @"perps", @"defi", @"nft", @"history"] containsObject:key]) key = @"other";
1943
+ NSNumber *saved = index >= 0 && index < _pageControllers.count ? _pageOffsets[[self pageKeyForIndex:index]] : nil;
1944
+ UIView *component = scroll == nil ? nil : RNCScrollComponentView(scroll);
1945
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1946
+ @"[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",
1947
+ reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
1948
+ (__bridge void *)self, (unsigned long)_generation, (unsigned long)_transitionId, (long)index, key,
1949
+ (long)_currentIndex, (long)_destinationIndex, _transitioning, _isPagerDragging,
1950
+ (__bridge void *)scroll, (long)scroll.tag, (__bridge void *)component, (long)component.tag,
1951
+ (__bridge void *)_observedScrollView, (long)_observedPageIndex, (unsigned long)_observedPageGeneration, _syntheticPageRestore, saved != nil, saved.doubleValue, _headerOffset, (long)_headerHeight,
1952
+ scroll.contentOffset.y, scroll.contentInset.top, scroll.contentInset.bottom,
1953
+ scroll.contentOffset.y + scroll.contentInset.top, scroll.contentSize.height, scroll.bounds.size.height]);
1954
+ #endif
1955
+ }
1956
+
1902
1957
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
1903
1958
  {
1904
- if (!_nativeSmoothHeaderScrollEnabled || _headerOffset >= _headerHeight ||
1959
+ [self offsetDiag:@"prepare-check" page:pageIndex scroll:nil];
1960
+ if (!_nativeSmoothHeaderScrollEnabled ||
1905
1961
  pageIndex < 0 || pageIndex >= _pageControllers.count) return;
1906
-
1907
- _pageOffsets[[self pageKeyForIndex:pageIndex]] = @(_headerOffset);
1908
1962
  UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:pageIndex];
1909
1963
  if (scrollView != nil && scrollView != _observedScrollView) {
1910
1964
  [self applyInsetsToScrollView:scrollView pageIndex:pageIndex restore:YES];
1911
1965
  }
1912
1966
  }
1913
1967
 
1968
+ - (void)beginPageStateTransition
1969
+ {
1970
+ [self cancelPagerTop];
1971
+ if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0 || _transitionPageOffsets != nil) return;
1972
+ _transitionSourceDisplayBody = MAX(_observedScrollView.contentOffset.y + _observedScrollView.contentInset.top - _headerHeight, 0);
1973
+ [self detachScrollObserver];
1974
+ _transitionSourceIndex = _currentIndex;
1975
+ _transitionHeaderOffset = _headerOffset;
1976
+ _transitionPageOffsets = [_pageOffsets copy];
1977
+ _transitionCompensatedPages = [NSMutableIndexSet new];
1978
+ #if DEBUG
1979
+ _transitionDiagBucket = -1;
1980
+ [self offsetDiag:[NSString stringWithFormat:@"screen-begin-source=%ld-savedQ=%.4f", (long)_transitionSourceIndex,
1981
+ [_transitionPageOffsets[self.currentPageKey] doubleValue]] page:_currentIndex scroll:nil];
1982
+ #endif
1983
+ }
1984
+
1985
+ - (void)updatePageStateTransitionProgress:(CGFloat)progress
1986
+ {
1987
+ if (_transitionPageOffsets == nil || _finishingPageStateTransition) return;
1988
+ NSInteger first = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
1989
+ NSInteger second = MIN(first + 1, (NSInteger)_pageControllers.count - 1);
1990
+ CGFloat fraction = RNCClamp(progress - first, 0, 1);
1991
+ if (!_isPagerDragging && _destinationIndex != _transitionSourceIndex) {
1992
+ first = _transitionSourceIndex;
1993
+ second = _destinationIndex;
1994
+ fraction = RNCClamp((progress - first) / (second - first), 0, 1);
1995
+ }
1996
+ CGFloat firstQ = [_transitionPageOffsets[[self pageKeyForIndex:first]] doubleValue];
1997
+ CGFloat secondQ = [_transitionPageOffsets[[self pageKeyForIndex:second]] doubleValue];
1998
+ _headerOffset = _transitionHeaderOffset;
1999
+ #if DEBUG
2000
+ NSInteger bucket = (NSInteger)floor(fraction * 4);
2001
+ if (_transitionDiagBucket != bucket) {
2002
+ _transitionDiagBucket = bucket;
2003
+ [self offsetDiag:[NSString stringWithFormat:
2004
+ @"screen-progress-kind=%@-source=%ld-target=%ld-sourceQ=%.4f-targetQ=%.4f-position=%.4f-fraction=%.4f",
2005
+ _isPagerDragging ? @"interactive" : @"programmatic-endpoint", (long)first, (long)second,
2006
+ firstQ, secondQ, progress, fraction] page:_currentIndex scroll:nil];
2007
+ }
2008
+ #endif
2009
+ _syntheticPageRestore = YES;
2010
+ NSInteger visibleFirst = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
2011
+ NSInteger visibleLast = MIN(visibleFirst + 1, (NSInteger)_pageControllers.count - 1);
2012
+ NSMutableIndexSet *displayPages = [NSMutableIndexSet indexSetWithIndexesInRange:
2013
+ NSMakeRange(visibleFirst, visibleLast - visibleFirst + 1)];
2014
+ if (!_isPagerDragging) {
2015
+ // UIView tap animation reports its endpoint once while the source exits.
2016
+ [displayPages addIndex:_transitionSourceIndex];
2017
+ [displayPages addIndex:_destinationIndex];
2018
+ }
2019
+ for (NSUInteger index = displayPages.firstIndex; index != NSNotFound;
2020
+ index = [displayPages indexGreaterThanIndex:index]) {
2021
+ UIScrollView *scroll = [self verticalScrollViewForPageAtIndex:index];
2022
+ if (scroll != nil) [self applyInsetsToScrollView:scroll pageIndex:index restore:YES];
2023
+ }
2024
+ _syntheticPageRestore = NO;
2025
+ [self layoutSharedHeaders];
2026
+ }
2027
+
2028
+ - (void)finishPageStateTransition
2029
+ {
2030
+ if (_transitionPageOffsets == nil) return;
2031
+ _finishingPageStateTransition = YES;
2032
+ _headerOffset = _transitionHeaderOffset;
2033
+ if (_currentIndex == _transitionSourceIndex) _displayBodyOverrides[self.currentPageKey] = @(_transitionSourceDisplayBody);
2034
+ else [_displayBodyOverrides removeObjectForKey:self.currentPageKey];
2035
+ if (_headerOffset >= _headerHeight) [_suspendedBodyKeys removeObject:self.currentPageKey];
2036
+ _syntheticPageRestore = YES;
2037
+ // Both completion and cancellation undo every temporary viewport offset.
2038
+ for (NSUInteger index = _transitionCompensatedPages.firstIndex; index != NSNotFound;
2039
+ index = [_transitionCompensatedPages indexGreaterThanIndex:index]) {
2040
+ UIScrollView *scroll = [self verticalScrollViewForPageAtIndex:index];
2041
+ if (scroll == nil) continue;
2042
+ [self applyInsetsToScrollView:scroll pageIndex:index restore:YES];
2043
+
2044
+ }
2045
+ _syntheticPageRestore = NO;
2046
+ _finishingPageStateTransition = NO;
2047
+ _transitionPageOffsets = nil;
2048
+ _transitionCompensatedPages = nil;
2049
+ [self layoutSharedHeaders];
2050
+ #if DEBUG
2051
+ [self offsetDiag:[NSString stringWithFormat:@"screen-finish-source=%ld-cancelled=%d", (long)_transitionSourceIndex,
2052
+ _currentIndex == _transitionSourceIndex] page:_currentIndex scroll:nil];
2053
+ #endif
2054
+ }
2055
+
1914
2056
  #pragma mark - React mounting
1915
2057
 
1916
2058
  // OneKey patch: Fabric can recycle a removed list as a descendant of our header
@@ -1958,8 +2100,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1958
2100
  _releasedPageScrollStates[pageKey] = state;
1959
2101
  // Retained pages may be visually aligned to the expanded header while
1960
2102
  // their saved offsets still hold the user's independent list position.
1961
- if (scrollView == _observedScrollView || _pageOffsets[pageKey] == nil) {
2103
+ BOOL ownsObservedPage = scrollView == _observedScrollView &&
2104
+ (!_nativeSmoothHeaderScrollEnabled ||
2105
+ (_observedPageGeneration == _generation && _observedPageIndex == index &&
2106
+ [_observedPageKey isEqualToString:pageKey]));
2107
+ if (!_nativeSmoothHeaderScrollEnabled && _transitionPageOffsets == nil && !_syntheticPageRestore &&
2108
+ ![_pendingPageRestoreKeys containsObject:pageKey] &&
2109
+ (ownsObservedPage || _pageOffsets[pageKey] == nil)) {
2110
+ [self offsetDiag:@"mount-release-save-before" page:index scroll:scrollView];
1962
2111
  _pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2112
+ [self offsetDiag:@"mount-release-save-after" page:index scroll:scrollView];
1963
2113
  }
1964
2114
  break;
1965
2115
  }
@@ -2243,7 +2393,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2243
2393
  if (appliedTop != nil && scrollView.refreshControl != nil) {
2244
2394
  original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2245
2395
  }
2246
- scrollView.contentInset = original;
2396
+ [self offsetDiag:@"release-original-inset-before" page:_currentIndex scroll:scrollView];
2397
+ scrollView.contentInset = original;
2398
+ [self offsetDiag:@"release-original-inset-after" page:_currentIndex scroll:scrollView];
2247
2399
  }
2248
2400
  NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
2249
2401
  if (indicatorValue != nil) {
@@ -2295,6 +2447,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2295
2447
  _pagerScrollView = nil;
2296
2448
  [super prepareForRecycle];
2297
2449
  [_pageOffsets removeAllObjects];
2450
+ [_commandBodyKeys removeAllObjects];
2451
+ [_alignedBodyKeys removeAllObjects];
2452
+ [_suspendedBodyKeys removeAllObjects];
2453
+ [_displayBodyOverrides removeAllObjects];
2454
+ [_pendingPageRestoreKeys removeAllObjects];
2455
+ _transitionPageOffsets = nil;
2456
+ _transitionCompensatedPages = nil;
2457
+ _syntheticPageRestore = NO;
2458
+ _finishingPageStateTransition = NO;
2298
2459
  _pageKeys = @[];
2299
2460
  _retainedPages = @"[]";
2300
2461
  _currentIndex = 0;
@@ -2627,6 +2788,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2627
2788
  - (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled
2628
2789
  {
2629
2790
  if (_isBeingRecycled) return;
2791
+ if (_nativeSmoothHeaderScrollEnabled &&
2792
+ (_transitioning || _isPagerDragging || _transitionPageOffsets != nil) && !selectionSettled) return;
2793
+ if (selectionSettled) [self finishPageStateTransition];
2630
2794
  if (_needsScrollObserverReattachAfterMount) {
2631
2795
  if (self.window == nil || ((_transitioning || _isPagerDragging) && !selectionSettled)) return;
2632
2796
  [self prepareAdjacentPageInsets];
@@ -2685,24 +2849,38 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2685
2849
  if (_pagerScrollView != nil && !_nativeSmoothHeaderScrollEnabled && !_needsPropsReapply) {
2686
2850
  [candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
2687
2851
  }
2688
- if (candidate == _observedScrollView) {
2852
+ if (candidate == _observedScrollView &&
2853
+ (!_nativeSmoothHeaderScrollEnabled ||
2854
+ ([_observedPageKey isEqualToString:self.currentPageKey] &&
2855
+ _observedPageIndex == _currentIndex && _observedPageGeneration == _generation))) {
2689
2856
  _needsScrollObserverReattachAfterMount = NO;
2857
+ if ([_pendingPageRestoreKeys containsObject:self.currentPageKey]) {
2858
+ [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2859
+ }
2690
2860
  [self attachSharedHeadersToScrollView:candidate];
2691
2861
  [self updateSharedHeaderPressCancellationGesture];
2692
2862
  return;
2693
2863
  }
2694
2864
 
2695
- BOOL replacingCurrentScrollView = _observingContentOffset;
2865
+ [self offsetDiag:@"observer-candidate" page:_currentIndex scroll:candidate];
2866
+ BOOL replacingCurrentScrollView = _observingContentOffset &&
2867
+ (!_nativeSmoothHeaderScrollEnabled || [_observedPageKey isEqualToString:self.currentPageKey]);
2696
2868
  [self detachScrollObserver];
2697
2869
  if (replacingCurrentScrollView) {
2698
- // A list replaced by an empty RN view keeps collapse, not the old row offset.
2699
- _pageOffsets[self.currentPageKey] = @(_headerOffset);
2870
+ // Legacy replacement discards body state; smooth replacement waits for layout.
2871
+ [self offsetDiag:@"observer-replacement-cache-before" page:_currentIndex scroll:candidate];
2872
+ if (!_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2873
+ [self offsetDiag:@"observer-replacement-cache-after" page:_currentIndex scroll:candidate];
2700
2874
  }
2701
2875
  _observedScrollView = candidate;
2876
+ _observedPageKey = [self.currentPageKey copy];
2877
+ _observedPageIndex = _currentIndex;
2878
+ _observedPageGeneration = _generation;
2879
+ [self offsetDiag:@"observer-attached-before-restore" page:_currentIndex scroll:candidate];
2702
2880
  [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2703
2881
  [candidate addObserver:self
2704
2882
  forKeyPath:@"contentOffset"
2705
- options:NSKeyValueObservingOptionNew
2883
+ options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
2706
2884
  context:RNCCollapsiblePagerContentOffsetContext];
2707
2885
  [candidate addObserver:self
2708
2886
  forKeyPath:@"contentInset"
@@ -2722,10 +2900,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2722
2900
 
2723
2901
  - (void)detachScrollObserver
2724
2902
  {
2903
+ [self cancelPagerTop];
2725
2904
  UIScrollView *scrollView = _observedScrollView;
2726
2905
  if (scrollView != nil) {
2727
2906
  [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
2728
- _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2907
+ [self offsetDiag:@"detach-save-before" page:_currentIndex scroll:scrollView];
2908
+ NSString *ownerKey = _nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey;
2909
+ BOOL ownerKeyExists = ownerKey != nil && ([_pageKeys containsObject:ownerKey] ||
2910
+ (_observedPageIndex >= 0 && _observedPageIndex < _pageControllers.count &&
2911
+ [[self pageKeyForIndex:_observedPageIndex] isEqualToString:ownerKey]));
2912
+ if (ownerKey != nil && !_nativeSmoothHeaderScrollEnabled) {
2913
+ _pageOffsets[ownerKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2914
+ }
2915
+ [self offsetDiag:@"detach-save-after" page:_currentIndex scroll:scrollView];
2729
2916
  if (_observingContentOffset) {
2730
2917
  [scrollView removeObserver:self
2731
2918
  forKeyPath:@"contentOffset"
@@ -2738,8 +2925,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2738
2925
  context:RNCCollapsiblePagerContentOffsetContext];
2739
2926
  }
2740
2927
  }
2928
+ if (!_nativeSmoothHeaderScrollEnabled && _observedPageKey != nil) {
2929
+ [_alignedBodyKeys removeObject:_observedPageKey];
2930
+ }
2741
2931
  _observingContentOffset = NO;
2742
2932
  _observedScrollView = nil;
2933
+ _observedPageKey = nil;
2934
+ _observedPageIndex = NSNotFound;
2743
2935
  if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
2744
2936
  [(RNCNativeScrollerComponentView *)scrollView.superview stopScrolling];
2745
2937
  }
@@ -3010,7 +3202,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3010
3202
  [self releaseRefreshControlForScrollView:scrollView];
3011
3203
  if (scrollView == _observedScrollView) {
3012
3204
  [self detachScrollObserver];
3013
- if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3205
+ [self offsetDiag:resetSavedOffset ? @"release-reset-before" : @"release-retain-before" page:_currentIndex scroll:scrollView];
3206
+ if (resetSavedOffset && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3207
+ [self offsetDiag:@"release-reset-after" page:_currentIndex scroll:scrollView];
3014
3208
  }
3015
3209
  if (scrollView == _sharedHeaderScrollView) {
3016
3210
  [self restoreSharedHeadersToContainer];
@@ -3030,8 +3224,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3030
3224
  [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3031
3225
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
3032
3226
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
3227
+ [self offsetDiag:@"release-original-offset-before" page:_currentIndex scroll:scrollView];
3033
3228
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
3034
3229
  animated:NO];
3230
+ [self offsetDiag:@"release-original-offset-after" page:_currentIndex scroll:scrollView];
3035
3231
  [_originalInsets removeObjectForKey:scrollView];
3036
3232
  [_appliedTopInsets removeObjectForKey:scrollView];
3037
3233
  [_originalIndicatorInsets removeObjectForKey:scrollView];
@@ -3060,8 +3256,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3060
3256
  pageIndex:(NSInteger)pageIndex
3061
3257
  restore:(BOOL)restore
3062
3258
  {
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.
3259
+ // Smooth pages retain their complete offset while replacement identity and
3260
+ // layout settle. Legacy pages retain the historical header-only policy.
3261
+ [self offsetDiag:restore ? @"restore-enter" : @"insets-enter" page:pageIndex scroll:scrollView];
3065
3262
  NSString *pageKey = [self pageKeyForIndex:pageIndex];
3066
3263
  RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
3067
3264
  BOOL deferredReleasedIdentity = NO;
@@ -3107,8 +3304,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3107
3304
  deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
3108
3305
  RNCCollapsiblePagerFallbackDebounceInterval;
3109
3306
  }
3307
+ #if DEBUG
3308
+ [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];
3309
+ #endif
3110
3310
  if (!deferredReleasedIdentity) {
3111
- if (!sameList) _pageOffsets[pageKey] = @(_headerOffset);
3311
+ [self offsetDiag:@"released-identity-cache-before" page:pageIndex scroll:scrollView];
3312
+ if (!sameList && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[pageKey] = @(_headerOffset);
3313
+ [self offsetDiag:@"released-identity-cache-after" page:pageIndex scroll:scrollView];
3112
3314
  [_releasedPageScrollStates removeObjectForKey:pageKey];
3113
3315
  }
3114
3316
  }
@@ -3156,29 +3358,60 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3156
3358
  if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
3157
3359
  return;
3158
3360
  }
3361
+ BOOL previousSyntheticRestore = _syntheticPageRestore;
3362
+ if (_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:pageKey]) _syntheticPageRestore = YES;
3363
+ [self offsetDiag:@"inset-assignment-before" page:pageIndex scroll:scrollView];
3159
3364
  scrollView.contentInset = next;
3365
+ [self offsetDiag:@"inset-assignment-after" page:pageIndex scroll:scrollView];
3160
3366
  UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
3161
3367
  indicatorInsets.top = pagerTopInset;
3162
3368
  scrollView.verticalScrollIndicatorInsets = indicatorInsets;
3163
3369
  scrollView.alwaysBounceVertical = YES;
3164
3370
 
3165
3371
  CGFloat targetOffset = previousLogicalOffset - next.top;
3372
+ CGFloat intendedLogicalOffset = previousLogicalOffset;
3166
3373
  if (restore) {
3167
- NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
3374
+ NSNumber *saved = _transitionPageOffsets != nil
3375
+ ? _transitionPageOffsets[pageKey] : _pageOffsets[pageKey];
3168
3376
  // The sticky slot can change height between pages. Cache offsets relative
3169
3377
  // 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;
3378
+ CGFloat restoredLogicalOffset;
3379
+ if (_nativeSmoothHeaderScrollEnabled && _headerHeight > 0) {
3380
+ [_alignedBodyKeys addObject:pageKey];
3381
+ if (_headerOffset < _headerHeight && saved.doubleValue > 0) [_suspendedBodyKeys addObject:pageKey];
3382
+ CGFloat body = saved.doubleValue;
3383
+ if (_transitionPageOffsets != nil && pageIndex == _transitionSourceIndex) body = _transitionSourceDisplayBody;
3384
+ else if (_transitionPageOffsets != nil) [_displayBodyOverrides removeObjectForKey:pageKey];
3385
+ else if (_displayBodyOverrides[pageKey] != nil) body = _displayBodyOverrides[pageKey].doubleValue;
3386
+ restoredLogicalOffset = _headerOffset + (_headerOffset >= _headerHeight ? body : 0);
3387
+ if (_transitionPageOffsets != nil) [_transitionCompensatedPages addIndex:pageIndex];
3388
+ if (deferredReleasedIdentity || scrollView.bounds.size.height <= 0 || scrollView.contentSize.height <= 0) {
3389
+ [_pendingPageRestoreKeys addObject:pageKey];
3390
+ _syntheticPageRestore = previousSyntheticRestore;
3391
+ return;
3392
+ }
3393
+ [_pendingPageRestoreKeys removeObject:pageKey];
3394
+ } else {
3395
+ restoredLogicalOffset = deferredReleasedIdentity || saved == nil ? _headerOffset : saved.doubleValue;
3396
+ }
3397
+ #if DEBUG
3398
+ [self offsetDiag:[NSString stringWithFormat:@"restore-candidate-saved=%.4f-headerAlignedLogical=%.4f", saved.doubleValue, restoredLogicalOffset] page:pageIndex scroll:scrollView];
3399
+ #endif
3400
+ intendedLogicalOffset = restoredLogicalOffset;
3401
+ // Cancellation restores the visible source, while retaining its unreachable reading intent.
3402
+ if (_nativeSmoothHeaderScrollEnabled && _headerOffset >= _headerHeight &&
3403
+ _displayBodyOverrides[pageKey] != nil) {
3404
+ intendedLogicalOffset = MAX(intendedLogicalOffset, _headerOffset + _pageOffsets[pageKey].doubleValue);
3177
3405
  }
3178
3406
  targetOffset = restoredLogicalOffset - next.top;
3179
- targetOffset = MAX(targetOffset, -next.top + _headerOffset);
3407
+ if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0) {
3408
+ targetOffset = MAX(targetOffset, -next.top + _headerOffset);
3409
+ }
3180
3410
  // An inactive list may shrink after its offset observer was detached.
3181
3411
  // Do not overwrite its corrected offset with a now-unreachable cached one.
3412
+ #if DEBUG
3413
+ [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];
3414
+ #endif
3182
3415
  targetOffset = RNCLimitRestoredScrollOffset(
3183
3416
  targetOffset,
3184
3417
  scrollView.contentSize.height,
@@ -3187,11 +3420,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3187
3420
  next.bottom
3188
3421
  );
3189
3422
  }
3423
+ #if DEBUG
3424
+ [self offsetDiag:[NSString stringWithFormat:@"restore-final-target=%.4f-restore=%d", targetOffset, restore] page:pageIndex scroll:scrollView];
3425
+ #endif
3190
3426
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
3191
3427
  if (restore || previousTopInset != next.top) {
3192
3428
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
3193
3429
  }
3194
- if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
3430
+ if (_nativeSmoothHeaderScrollEnabled && restore && targetOffset + next.top + 0.5 < intendedLogicalOffset) {
3431
+ [_pendingPageRestoreKeys addObject:pageKey];
3432
+ }
3433
+ _syntheticPageRestore = previousSyntheticRestore;
3434
+ if (pageIndex == _currentIndex) {
3435
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3436
+ if (!_nativeSmoothHeaderScrollEnabled) [self updateHeaderForScrollView:scrollView];
3437
+ else [self applyHeaderOffset];
3438
+ }
3439
+ [self offsetDiag:@"restore-post" page:pageIndex scroll:scrollView];
3195
3440
  }
3196
3441
 
3197
3442
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
@@ -3201,6 +3446,71 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3201
3446
  }
3202
3447
  }
3203
3448
 
3449
+ - (void)nativeListScrollCommandAccepted:(NSNotification *)notification
3450
+ {
3451
+ UIScrollView *scrollView = notification.object;
3452
+ if (scrollView != _observedScrollView ||
3453
+ _observedPageKey == nil || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3454
+ _observedPageIndex != _currentIndex || _observedPageGeneration != _generation ||
3455
+ _transitioning || _isPagerDragging || _transitionPageOffsets != nil || _syntheticPageRestore) return;
3456
+ NSInteger phase = [notification.userInfo[@"phase"] integerValue];
3457
+ if ([self matchesPagerTop:scrollView]) {
3458
+ if (phase == 0) [self cancelPagerTop];
3459
+ else return;
3460
+ }
3461
+ if (phase == 0) [_alignedBodyKeys removeObject:_observedPageKey];
3462
+ if (!_nativeSmoothHeaderScrollEnabled) return;
3463
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3464
+ if (phase == 1) { [_commandBodyKeys addObject:_observedPageKey]; return; }
3465
+ if (phase == 2) {
3466
+ if (![_commandBodyKeys containsObject:_observedPageKey]) return;
3467
+ _pageOffsets[_observedPageKey] = @(MAX(_currentLogicalOffset - _headerHeight, 0));
3468
+ [self updateHeaderForScrollView:scrollView];
3469
+ [_commandBodyKeys removeObject:_observedPageKey];
3470
+ return;
3471
+ }
3472
+ [_commandBodyKeys removeObject:_observedPageKey];
3473
+ [_pendingPageRestoreKeys removeObject:_observedPageKey];
3474
+ [_alignedBodyKeys removeObject:_observedPageKey];
3475
+ [_suspendedBodyKeys removeObject:_observedPageKey];
3476
+ [_displayBodyOverrides removeObjectForKey:_observedPageKey];
3477
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3478
+ BOOL explicitTop = [notification.userInfo[@"explicitTop"] boolValue];
3479
+ _pageOffsets[_observedPageKey] = @(explicitTop ? 0 : MAX(_currentLogicalOffset - _headerHeight, 0));
3480
+ if (explicitTop) {
3481
+ _headerOffset = 0;
3482
+ [self applyHeaderOffset];
3483
+ }
3484
+ [self offsetDiag:@"list-command-body-takeover" page:_currentIndex scroll:scrollView];
3485
+ }
3486
+
3487
+ - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller
3488
+ {
3489
+ UIScrollView *scrollView = scroller.scrollView;
3490
+ NSString *ownerKey = _observedPageKey;
3491
+ if (scrollView != _observedScrollView ||
3492
+ ownerKey == nil || ![ownerKey isEqualToString:self.currentPageKey] ||
3493
+ _observedPageIndex != _currentIndex || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3494
+ _observedPageGeneration != _generation || _transitioning || _isPagerDragging ||
3495
+ _transitionPageOffsets != nil || _syntheticPageRestore) return;
3496
+ if (!_dispatchingPagerTop) {
3497
+ [self cancelPagerTop];
3498
+ [_alignedBodyKeys removeObject:ownerKey];
3499
+ }
3500
+ if (!_nativeSmoothHeaderScrollEnabled) return;
3501
+ [self offsetDiag:@"command-pending-cancel-before" page:_observedPageIndex scroll:scrollView];
3502
+ [_pendingPageRestoreKeys removeObject:ownerKey];
3503
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3504
+ [_commandBodyKeys removeObject:ownerKey];
3505
+ [_alignedBodyKeys removeObject:ownerKey];
3506
+ [_suspendedBodyKeys removeObject:ownerKey];
3507
+ [_displayBodyOverrides removeObjectForKey:ownerKey];
3508
+ _pageOffsets[ownerKey] = @(MAX(_currentLogicalOffset - _headerHeight, 0));
3509
+ [self updateHeaderForScrollView:scrollView];
3510
+ [self offsetDiag:@"command-pending-cancel-after" page:_observedPageIndex scroll:scrollView];
3511
+ }
3512
+
3513
+
3204
3514
  - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
3205
3515
  callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next
3206
3516
  {
@@ -3252,25 +3562,86 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3252
3562
  if (context == RNCScrollRefreshControlContext) {
3253
3563
  UIScrollView *scrollView = (UIScrollView *)object;
3254
3564
  [self updateRefreshControlForScrollView:scrollView];
3255
- if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
3565
+ if (scrollView == _observedScrollView && !_syntheticPageRestore &&
3566
+ (!_nativeSmoothHeaderScrollEnabled || (!_transitioning && _transitionPageOffsets == nil))) {
3567
+ [self updateHeaderForScrollView:scrollView];
3568
+ }
3256
3569
  return;
3257
3570
  }
3258
3571
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
3572
+ if ((_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:_observedPageKey]) &&
3573
+ (_observedPageGeneration != _generation || _observedPageKey == nil ||
3574
+ _observedPageIndex != _currentIndex || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3575
+ _transitioning || _isPagerDragging ||
3576
+ _transitionPageOffsets != nil || _syntheticPageRestore)) return;
3577
+ UIScrollView *ownedScroll = (UIScrollView *)object;
3578
+ NSValue *oldPoint = change[NSKeyValueChangeOldKey];
3579
+ NSValue *newPoint = change[NSKeyValueChangeNewKey];
3580
+ if ([keyPath isEqualToString:@"contentOffset"] && [self matchesPagerTop:ownedScroll] && ownedScroll.panGestureRecognizer.state == UIGestureRecognizerStateChanged &&
3581
+ [oldPoint isKindOfClass:NSValue.class] && [newPoint isKindOfClass:NSValue.class] &&
3582
+ oldPoint.CGPointValue.y != newPoint.CGPointValue.y) {
3583
+ CGPoint interruptedOffset = ownedScroll.contentOffset;
3584
+ [self cancelPagerTop];
3585
+ [ownedScroll setContentOffset:interruptedOffset animated:NO];
3586
+ }
3587
+ if ([keyPath isEqualToString:@"contentOffset"] &&
3588
+ ownedScroll.panGestureRecognizer.state == UIGestureRecognizerStateChanged &&
3589
+ [oldPoint isKindOfClass:NSValue.class] && [newPoint isKindOfClass:NSValue.class] &&
3590
+ oldPoint.CGPointValue.y != newPoint.CGPointValue.y) {
3591
+ [_alignedBodyKeys removeObject:_observedPageKey];
3592
+ }
3593
+ if (_nativeSmoothHeaderScrollEnabled && [keyPath isEqualToString:@"contentOffset"] &&
3594
+ ownedScroll.panGestureRecognizer.state == UIGestureRecognizerStateChanged &&
3595
+ [oldPoint isKindOfClass:NSValue.class] && [newPoint isKindOfClass:NSValue.class] &&
3596
+ oldPoint.CGPointValue.y != newPoint.CGPointValue.y) {
3597
+ [self cancelPagerTop];
3598
+ [_commandBodyKeys removeObject:_observedPageKey];
3599
+ [_pendingPageRestoreKeys removeObject:_observedPageKey];
3600
+ [_alignedBodyKeys removeObject:_observedPageKey];
3601
+ [_suspendedBodyKeys removeObject:_observedPageKey];
3602
+ [_displayBodyOverrides removeObjectForKey:_observedPageKey];
3603
+ }
3604
+ if (_nativeSmoothHeaderScrollEnabled && [_pendingPageRestoreKeys containsObject:_observedPageKey]) {
3605
+ if ([keyPath isEqualToString:@"contentSize"] &&
3606
+ ![change[NSKeyValueChangeOldKey] isEqual:change[NSKeyValueChangeNewKey]]) {
3607
+ if (_headerOffset >= _headerHeight) [_displayBodyOverrides removeObjectForKey:_observedPageKey];
3608
+ [self applyInsetsToScrollView:_observedScrollView pageIndex:_observedPageIndex restore:YES];
3609
+ }
3610
+ return;
3611
+ }
3259
3612
  if ([keyPath isEqualToString:@"contentInset"]) {
3260
- [self updateHeaderForScrollView:(UIScrollView *)object];
3613
+ if (!_nativeSmoothHeaderScrollEnabled) [self updateHeaderForScrollView:(UIScrollView *)object];
3261
3614
  return;
3262
3615
  }
3263
3616
  if ([keyPath isEqualToString:@"contentSize"]) {
3264
3617
  NSValue *oldSize = change[NSKeyValueChangeOldKey];
3265
3618
  NSValue *newSize = change[NSKeyValueChangeNewKey];
3266
3619
  if (![oldSize isEqual:newSize]) {
3267
- [self applyInsetsToScrollView:_observedScrollView pageIndex:_currentIndex restore:NO];
3620
+ BOOL restoreAlignedBody =
3621
+ _observedPageGeneration == _generation && _observedPageIndex == _currentIndex &&
3622
+ _observedPageKey != nil && [_observedPageKey isEqualToString:self.currentPageKey] &&
3623
+ [_alignedBodyKeys containsObject:_observedPageKey] &&
3624
+ ![_commandBodyKeys containsObject:_observedPageKey] &&
3625
+ _observedScrollView.panGestureRecognizer.state != UIGestureRecognizerStateChanged &&
3626
+ !_observedScrollView.isDragging && !_observedScrollView.isDecelerating;
3627
+ [self applyInsetsToScrollView:_observedScrollView pageIndex:_currentIndex restore:restoreAlignedBody];
3268
3628
  }
3269
3629
  return;
3270
3630
  }
3271
3631
  UIScrollView *scrollView = (UIScrollView *)object;
3272
3632
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3273
- _pageOffsets[self.currentPageKey] = @(_currentLogicalOffset);
3633
+ #if DEBUG
3634
+ BOOL offsetDiagChanged = _pageOffsets[self.currentPageKey] == nil || _pageOffsets[self.currentPageKey].doubleValue != (_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3635
+ if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-before" page:_currentIndex scroll:scrollView];
3636
+ #endif
3637
+ if ([_alignedBodyKeys containsObject:_observedPageKey] ||
3638
+ (_nativeSmoothHeaderScrollEnabled && scrollView.panGestureRecognizer.state != UIGestureRecognizerStateChanged &&
3639
+ !scrollView.isDecelerating && ![_commandBodyKeys containsObject:_observedPageKey])) return;
3640
+ _pageOffsets[_nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey] =
3641
+ @(_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3642
+ #if DEBUG
3643
+ if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-after" page:_currentIndex scroll:scrollView];
3644
+ #endif
3274
3645
  [self updateHeaderForScrollView:scrollView];
3275
3646
  return;
3276
3647
  }
@@ -3279,6 +3650,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3279
3650
 
3280
3651
  - (void)updateHeaderForScrollView:(UIScrollView *)scrollView
3281
3652
  {
3653
+ if ([self matchesPagerTop:scrollView]) {
3654
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3655
+ _headerOffset = _pagerTopLogicalTarget;
3656
+ [self applyHeaderOffset];
3657
+ [self finishPagerTopIfReached];
3658
+ return;
3659
+ }
3660
+ if ([_alignedBodyKeys containsObject:self.currentPageKey] ||
3661
+ (_nativeSmoothHeaderScrollEnabled && (_syntheticPageRestore || _transitionPageOffsets != nil ||
3662
+ [_pendingPageRestoreKeys containsObject:self.currentPageKey]))) return;
3282
3663
  CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3283
3664
  _currentLogicalOffset = logicalOffset;
3284
3665
  _headerOffset = MIN(MAX(logicalOffset, 0), _headerHeight);
@@ -3322,6 +3703,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3322
3703
 
3323
3704
  - (void)goTo:(NSInteger)index animated:(BOOL)animated
3324
3705
  {
3706
+ [self offsetDiag:@"go-to" page:index scroll:_observedScrollView];
3325
3707
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
3326
3708
  _hasReceivedPageCommand = YES;
3327
3709
  _hasAppliedInitialPage = YES;
@@ -3347,6 +3729,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3347
3729
  return;
3348
3730
  }
3349
3731
 
3732
+ [self beginPageStateTransition];
3350
3733
  [self preparePageForHorizontalTransitionAtIndex:index];
3351
3734
  _destinationIndex = index;
3352
3735
  [self liftSharedHeadersForPagerTransition:@"programmatic"];
@@ -3366,6 +3749,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3366
3749
  void (^animations)(void) = ^{
3367
3750
  [self->_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0)];
3368
3751
  [self->_nativeTabBarView setProgress:index];
3752
+ [self updatePageStateTransitionProgress:index];
3369
3753
  };
3370
3754
  void (^completion)(BOOL) = ^(BOOL finished) {
3371
3755
  __strong __typeof__(weakSelf) self = weakSelf;
@@ -3373,6 +3757,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3373
3757
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
3374
3758
  self->_pagerScrollView.scrollEnabled = self->_scrollEnabled;
3375
3759
  if (finished) {
3760
+ [self detachScrollObserver];
3376
3761
  self->_currentIndex = index;
3377
3762
  self->_destinationIndex = index;
3378
3763
  [self->_nativeTabBarView setProgress:index];
@@ -3410,6 +3795,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3410
3795
  if (self == nil || self->_generation != capturedGeneration ||
3411
3796
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
3412
3797
  if (finished) {
3798
+ [self detachScrollObserver];
3413
3799
  self->_currentIndex = index;
3414
3800
  self->_destinationIndex = index;
3415
3801
  [self->_nativeTabBarView setProgress:index];
@@ -3434,6 +3820,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3434
3820
  dispatch_async(dispatch_get_main_queue(), ^{
3435
3821
  if (self->_generation != generation || self->_transitionId != transition ||
3436
3822
  self->_isBeingRecycled) return;
3823
+ [self finishPageStateTransition];
3437
3824
  self->_transitioning = NO;
3438
3825
  if (self->_hasPendingDirectNativePagerChange) {
3439
3826
  BOOL enabled = self->_pendingDirectNativePagerEnabled;
@@ -3497,9 +3884,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3497
3884
 
3498
3885
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
3499
3886
  {
3887
+ [self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
3500
3888
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3501
3889
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3502
3890
  }
3891
+ [self beginPageStateTransition];
3503
3892
  if (_directNativePagerEnabled) {
3504
3893
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex - 1];
3505
3894
  [self preparePageForHorizontalTransitionAtIndex:_currentIndex + 1];
@@ -3651,6 +4040,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3651
4040
  NSInteger position = (NSInteger)floor(progress);
3652
4041
  CGFloat offset = progress - position;
3653
4042
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
4043
+ [self updatePageStateTransitionProgress:progress];
3654
4044
  [_nativeTabBarView setProgress:progress];
3655
4045
  const auto emitter = [self eventEmitter];
3656
4046
  if (emitter) {
@@ -3668,6 +4058,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3668
4058
  CGFloat transitionProgress = RNCClamp(fabs(rawOffset), 0, 1);
3669
4059
  tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
3670
4060
  }
4061
+ [self updatePageStateTransitionProgress:tabProgress];
3671
4062
  [_nativeTabBarView setProgress:tabProgress];
3672
4063
  const auto emitter = [self eventEmitter];
3673
4064
  if (emitter) {
@@ -3685,6 +4076,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3685
4076
 
3686
4077
  - (void)emitPageSelected:(NSInteger)position
3687
4078
  {
4079
+ [self offsetDiag:@"selection-complete" page:position scroll:_observedScrollView];
3688
4080
  const auto emitter = [self eventEmitter];
3689
4081
  if (emitter) emitter->onPageSelected({.position = (double)position});
3690
4082
  }
@@ -3713,6 +4105,77 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3713
4105
  RCTRNCCollapsiblePagerViewHandleCommand(self, commandName, args);
3714
4106
  }
3715
4107
 
4108
+ - (BOOL)matchesPagerTop:(UIScrollView *)scrollView
4109
+ {
4110
+ return scrollView != nil && scrollView == _pagerTopScrollView && scrollView == _observedScrollView &&
4111
+ _pagerTopPageKey != nil && [_pagerTopPageKey isEqualToString:self.currentPageKey] &&
4112
+ [_observedPageKey isEqualToString:_pagerTopPageKey] && _pagerTopPageIndex == _currentIndex &&
4113
+ _observedPageIndex == _currentIndex && _pagerTopGeneration == _generation &&
4114
+ _observedPageGeneration == _generation && scrollView.window != nil;
4115
+ }
4116
+
4117
+ - (void)cancelPagerTop
4118
+ {
4119
+ if (_pagerTopPageKey != nil) [_commandBodyKeys removeObject:_pagerTopPageKey];
4120
+ _pagerTopScrollView = nil;
4121
+ _pagerTopPageKey = nil;
4122
+ }
4123
+
4124
+ - (void)finishPagerTopIfReached
4125
+ {
4126
+ UIScrollView *scrollView = _pagerTopScrollView;
4127
+ if (_dispatchingPagerTop || ![self matchesPagerTop:scrollView]) return;
4128
+ CGFloat logical = scrollView.contentOffset.y + scrollView.contentInset.top;
4129
+ if (fabs(logical - _pagerTopLogicalTarget) > 0.5) return;
4130
+ _pageOffsets[_pagerTopPageKey] = @(_nativeSmoothHeaderScrollEnabled ? 0 : _pagerTopLogicalTarget);
4131
+ [_alignedBodyKeys addObject:_pagerTopPageKey];
4132
+ _currentLogicalOffset = logical;
4133
+ _headerOffset = _pagerTopLogicalTarget;
4134
+ [self applyHeaderOffset];
4135
+ [self offsetDiag:[NSString stringWithFormat:@"pager-top-complete-token=%lu-target=%.4f", (unsigned long)_pagerTopToken, _pagerTopLogicalTarget] page:_currentIndex scroll:scrollView];
4136
+ [self cancelPagerTop];
4137
+ }
4138
+
4139
+ - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
4140
+ {
4141
+ UIScrollView *scrollView = _observedScrollView;
4142
+ NSString *key = _observedPageKey;
4143
+ if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
4144
+ _transitionPageOffsets != nil || _syntheticPageRestore || scrollView == nil || scrollView.window == nil ||
4145
+ key == nil || ![key isEqualToString:self.currentPageKey] || _observedPageIndex != _currentIndex ||
4146
+ _observedPageGeneration != _generation || self.window == nil) return;
4147
+ [self cancelPagerTop];
4148
+ // Old List phase2 notifications must run before the new target owns this page.
4149
+ [NSNotificationCenter.defaultCenter postNotificationName:@"OneKeyNativeListCancelScrollCommand" object:scrollView];
4150
+ if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
4151
+ [(RNCNativeScrollerComponentView *)scrollView.superview stopScrolling];
4152
+ } else {
4153
+ [scrollView setContentOffset:scrollView.contentOffset animated:NO];
4154
+ }
4155
+ [_pendingPageRestoreKeys removeObject:key];
4156
+ [_alignedBodyKeys removeObject:key];
4157
+ [_suspendedBodyKeys removeObject:key];
4158
+ [_displayBodyOverrides removeObjectForKey:key];
4159
+ CGFloat logicalTarget = target == 1 ? _headerHeight : 0;
4160
+ _pageOffsets[key] = @(_nativeSmoothHeaderScrollEnabled ? 0 : logicalTarget);
4161
+ _pagerTopScrollView = scrollView;
4162
+ _pagerTopPageKey = [key copy];
4163
+ _pagerTopPageIndex = _currentIndex;
4164
+ _pagerTopGeneration = _generation;
4165
+ _pagerTopLogicalTarget = logicalTarget;
4166
+ _pagerTopToken++;
4167
+ [_commandBodyKeys addObject:key];
4168
+ _headerOffset = logicalTarget;
4169
+ [self applyHeaderOffset];
4170
+ CGFloat physicalTarget = RNCLimitRestoredScrollOffset(logicalTarget - scrollView.contentInset.top,
4171
+ scrollView.contentSize.height, scrollView.bounds.size.height, scrollView.contentInset.top, scrollView.contentInset.bottom);
4172
+ [self offsetDiag:[NSString stringWithFormat:@"pager-top-begin-token=%lu-target=%ld-animated=%d", (unsigned long)_pagerTopToken, (long)target, animated] page:_currentIndex scroll:scrollView];
4173
+ _dispatchingPagerTop = YES;
4174
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:animated];
4175
+ _dispatchingPagerTop = NO;
4176
+ [self finishPagerTopIfReached];
4177
+ }
4178
+
3716
4179
  - (void)setPage:(NSInteger)index
3717
4180
  {
3718
4181
  [self goTo:index animated:YES];