@onekeyfe/react-native-pager-view 3.0.133 → 3.0.135
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/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +5 -3
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +10 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +14 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +281 -44
- package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.m +36 -0
- package/package.json +2 -2
- package/react-native-pager-view.podspec +5 -0
|
@@ -184,10 +184,12 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
private fun selectedBackground(selected: Boolean) = GradientDrawable().apply {
|
|
187
|
+
private fun selectedBackground(selected: Boolean, height: Int) = GradientDrawable().apply {
|
|
188
188
|
shape = GradientDrawable.RECTANGLE
|
|
189
189
|
setColor(if (selected) selectedBackgroundColor else Color.TRANSPARENT)
|
|
190
|
-
|
|
190
|
+
// OneKey patch: match the pill shape of the React category filters.
|
|
191
|
+
// cornerRadius = dp(10.0).toFloat()
|
|
192
|
+
cornerRadius = height / 2f
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
@@ -278,7 +280,7 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
278
280
|
buttons.forEachIndexed { index, button ->
|
|
279
281
|
val selected = index == selectedIndex
|
|
280
282
|
button.setTextColor(if (selected) activeTextColor else inactiveTextColor)
|
|
281
|
-
button.background = selectedBackground(selected)
|
|
283
|
+
button.background = selectedBackground(selected, button.height)
|
|
282
284
|
button.isSelected = selected
|
|
283
285
|
}
|
|
284
286
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import "RNCCollapsiblePagerReleasedStatePolicy.h"
|
|
2
|
+
|
|
3
|
+
void RNCPruneReleasedPageStates(
|
|
4
|
+
NSMutableDictionary<NSString *, id> *releasedStates,
|
|
5
|
+
NSArray<NSString *> *validPageKeys
|
|
6
|
+
)
|
|
7
|
+
{
|
|
8
|
+
NSSet<NSString *> *validKeys = [NSSet setWithArray:validPageKeys];
|
|
9
|
+
for (NSString *pageKey in releasedStates.allKeys) {
|
|
10
|
+
if (![validKeys containsObject:pageKey]) {
|
|
11
|
+
[releasedStates removeObjectForKey:pageKey];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
2
|
+
#import "RNCCollapsiblePagerReleasedStatePolicy.h"
|
|
2
3
|
|
|
3
4
|
#import <react/renderer/components/pagerview/ComponentDescriptors.h>
|
|
4
5
|
#import <react/renderer/components/pagerview/EventEmitters.h>
|
|
@@ -11,12 +12,39 @@
|
|
|
11
12
|
#import "React/RCTSurfaceTouchHandler.h"
|
|
12
13
|
#import "React/RCTTouchHandler.h"
|
|
13
14
|
|
|
15
|
+
// OneKey patch: deduplicate structural mutation parents before scanning the page.
|
|
16
|
+
#include <unordered_set>
|
|
17
|
+
|
|
14
18
|
using namespace facebook::react;
|
|
15
19
|
|
|
16
20
|
static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
|
|
17
21
|
static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
|
|
18
22
|
@"rnc-collapsible-pager-native-scroller:";
|
|
19
23
|
|
|
24
|
+
static const CFTimeInterval RNCCollapsiblePagerFallbackDebounceInterval = 0.5;
|
|
25
|
+
|
|
26
|
+
// OneKey patch: native scroll views can survive a move or be reused for a new
|
|
27
|
+
// Fabric component. Keep weak identity until their page is attached again.
|
|
28
|
+
@interface RNCCollapsiblePagerReleasedScrollState : NSObject
|
|
29
|
+
@property (nonatomic, weak) UIScrollView *scrollView;
|
|
30
|
+
@property (nonatomic, weak) UIView *componentView;
|
|
31
|
+
@property (nonatomic, assign) NSInteger componentTag;
|
|
32
|
+
@property (nonatomic, weak) UIScrollView *pendingFallbackScrollView;
|
|
33
|
+
@property (nonatomic, assign) CFTimeInterval pendingFallbackDetectedAt;
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
@implementation RNCCollapsiblePagerReleasedScrollState
|
|
37
|
+
@end
|
|
38
|
+
|
|
39
|
+
static UIView *RNCScrollComponentView(UIScrollView *scrollView)
|
|
40
|
+
{
|
|
41
|
+
for (UIView *view = scrollView; view != nil; view = view.superview) {
|
|
42
|
+
// UIView's Fabric category adopts the protocol even for unregistered views.
|
|
43
|
+
if ([view isKindOfClass:RCTViewComponentView.class] && view.tag > 0) return view;
|
|
44
|
+
}
|
|
45
|
+
return nil;
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
typedef void (^RNCCollapsiblePagerNativeTabPressHandler)(NSInteger index, NSString *key);
|
|
21
49
|
|
|
22
50
|
static CGFloat RNCClamp(CGFloat value, CGFloat minimum, CGFloat maximum)
|
|
@@ -716,7 +744,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
716
744
|
NSString *key = [item[@"key"] isKindOfClass:NSString.class] ? item[@"key"] : @"";
|
|
717
745
|
BOOL selected = [key isEqualToString:self->_selectedKey];
|
|
718
746
|
button.backgroundColor = selected ? self->_selectedBackgroundColor : UIColor.clearColor;
|
|
719
|
-
|
|
747
|
+
// OneKey patch: match the pill shape of the React category filters.
|
|
748
|
+
// button.layer.cornerRadius = 10;
|
|
749
|
+
button.layer.cornerRadius = CGRectGetHeight(button.bounds) / 2;
|
|
720
750
|
[button setTitleColor:selected ? self->_activeTextColor : self->_inactiveTextColor
|
|
721
751
|
forState:UIControlStateNormal];
|
|
722
752
|
button.accessibilityTraits = selected
|
|
@@ -989,6 +1019,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
989
1019
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
990
1020
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
991
1021
|
NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
|
|
1022
|
+
// OneKey patch: pair pre-mount release with recovery, including deferred mounts.
|
|
1023
|
+
NSMutableDictionary<NSString *, RNCCollapsiblePagerReleasedScrollState *> *_releasedPageScrollStates;
|
|
1024
|
+
BOOL _needsScrollObserverReattachAfterMount;
|
|
992
1025
|
__weak UIScrollView *_observedScrollView;
|
|
993
1026
|
__weak UIScrollView *_sharedHeaderScrollView;
|
|
994
1027
|
__weak UIScrollView *_pendingFallbackScrollView;
|
|
@@ -1023,6 +1056,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1023
1056
|
_logicalChildren = [NSMutableArray new];
|
|
1024
1057
|
_pageControllers = [NSMutableArray new];
|
|
1025
1058
|
_pageOffsets = [NSMutableDictionary new];
|
|
1059
|
+
_releasedPageScrollStates = [NSMutableDictionary new];
|
|
1026
1060
|
_originalInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1027
1061
|
_appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1028
1062
|
_originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
@@ -1132,6 +1166,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1132
1166
|
!_pagerScrollView.dragging && !_pagerScrollView.decelerating) {
|
|
1133
1167
|
[self finishPagerScrollEmittingSelection:NO];
|
|
1134
1168
|
}
|
|
1169
|
+
// OneKey patch: a detached pager can return without a new Fabric transaction.
|
|
1170
|
+
// Wait until the current mount has finished before inspecting its new lists.
|
|
1171
|
+
if (self.window != nil &&
|
|
1172
|
+
(_needsScrollObserverReattachAfterMount || _releasedPageScrollStates.count > 0)) {
|
|
1173
|
+
NSUInteger generation = _generation;
|
|
1174
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1175
|
+
if (self->_generation == generation &&
|
|
1176
|
+
(self->_needsScrollObserverReattachAfterMount || self->_releasedPageScrollStates.count > 0)) {
|
|
1177
|
+
self->_needsScrollObserverReattachAfterMount = YES;
|
|
1178
|
+
[self attachScrollObserverForCurrentPage];
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1135
1182
|
}
|
|
1136
1183
|
|
|
1137
1184
|
- (void)dealloc
|
|
@@ -1375,6 +1422,63 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1375
1422
|
|
|
1376
1423
|
#pragma mark - React mounting
|
|
1377
1424
|
|
|
1425
|
+
// OneKey patch: Fabric can recycle a removed list as a descendant of our header
|
|
1426
|
+
// within the same transaction. Release native ownership before that reuse can
|
|
1427
|
+
// form a cycle; did-mount recovery runs too late to prevent it.
|
|
1428
|
+
- (void)mountingTransactionWillMount:(const MountingTransaction &)transaction
|
|
1429
|
+
withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
|
|
1430
|
+
{
|
|
1431
|
+
if (_isBeingRecycled || (_originalInsets.count == 0 && _releasedPageScrollStates.count == 0)) return;
|
|
1432
|
+
std::unordered_set<NSInteger> removedTags;
|
|
1433
|
+
std::unordered_set<NSInteger> deletedTags;
|
|
1434
|
+
for (const auto &mutation : transaction.getMutations()) {
|
|
1435
|
+
if ((mutation.type == ShadowViewMutation::Remove ||
|
|
1436
|
+
mutation.type == ShadowViewMutation::Delete) && mutation.oldChildShadowView.tag > 0) {
|
|
1437
|
+
removedTags.insert(mutation.oldChildShadowView.tag);
|
|
1438
|
+
if (mutation.type == ShadowViewMutation::Delete) {
|
|
1439
|
+
deletedTags.insert(mutation.oldChildShadowView.tag);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// A transition can defer reattachment across several mounting transactions.
|
|
1444
|
+
for (RNCCollapsiblePagerReleasedScrollState *state in _releasedPageScrollStates.allValues) {
|
|
1445
|
+
if (deletedTags.find(state.componentTag) != deletedTags.end()) state.componentView = nil;
|
|
1446
|
+
}
|
|
1447
|
+
if (removedTags.empty()) return;
|
|
1448
|
+
|
|
1449
|
+
// Inspect only the managed scroll views' ancestor chains, including wrappers
|
|
1450
|
+
// above the pager. Row removals and unrelated commits keep ownership intact.
|
|
1451
|
+
for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
|
|
1452
|
+
for (UIView *ancestor = scrollView; ancestor != nil; ancestor = ancestor.superview) {
|
|
1453
|
+
if (removedTags.find(ancestor.tag) == removedTags.end()) continue;
|
|
1454
|
+
BOOL belongsToPage = NO;
|
|
1455
|
+
for (NSInteger index = 0; index < _pageControllers.count; index++) {
|
|
1456
|
+
if (![scrollView isDescendantOfView:_pageControllers[index].view]) continue;
|
|
1457
|
+
belongsToPage = YES;
|
|
1458
|
+
NSString *pageKey = [self pageKeyForIndex:index];
|
|
1459
|
+
UIView *componentView = RNCScrollComponentView(scrollView);
|
|
1460
|
+
RNCCollapsiblePagerReleasedScrollState *state = [RNCCollapsiblePagerReleasedScrollState new];
|
|
1461
|
+
state.scrollView = scrollView;
|
|
1462
|
+
state.componentTag = componentView.tag;
|
|
1463
|
+
// A deleted component can reuse both the native object and its tag.
|
|
1464
|
+
if (deletedTags.find(componentView.tag) == deletedTags.end()) {
|
|
1465
|
+
state.componentView = componentView;
|
|
1466
|
+
}
|
|
1467
|
+
_releasedPageScrollStates[pageKey] = state;
|
|
1468
|
+
// Retained pages may be visually aligned to the expanded header while
|
|
1469
|
+
// their saved offsets still hold the user's independent list position.
|
|
1470
|
+
if (scrollView == _observedScrollView || _pageOffsets[pageKey] == nil) {
|
|
1471
|
+
_pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
|
|
1472
|
+
}
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
_needsScrollObserverReattachAfterMount = YES;
|
|
1476
|
+
[self restoreScrollViewState:scrollView resetSavedOffset:!belongsToPage];
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1378
1482
|
- (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
|
|
1379
1483
|
withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
|
|
1380
1484
|
{
|
|
@@ -1382,16 +1486,55 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1382
1486
|
_isPagerDragging || _isBeingRecycled || self.window == nil ||
|
|
1383
1487
|
_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
|
|
1384
1488
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1489
|
+
// OneKey patch: a release must recover even when the changed parent is above
|
|
1490
|
+
// the page subtree. Missing lists keep their identity for a later insertion.
|
|
1491
|
+
if (_needsScrollObserverReattachAfterMount) {
|
|
1492
|
+
[self attachScrollObserverForCurrentPage];
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
UIView *page = _pageControllers[_currentIndex].view;
|
|
1496
|
+
BOOL headerIsAttached = [_sharedHeaderHostView isDescendantOfView:self];
|
|
1497
|
+
BOOL currentListIsAttached = _observedScrollView != nil &&
|
|
1498
|
+
[_observedScrollView isDescendantOfView:page] && headerIsAttached;
|
|
1499
|
+
BOOL currentListIsFallback = currentListIsAttached &&
|
|
1500
|
+
[_observedScrollView isKindOfClass:UICollectionView.class];
|
|
1501
|
+
if (currentListIsAttached && !currentListIsFallback &&
|
|
1502
|
+
_releasedPageScrollStates.count == 0) return;
|
|
1503
|
+
if (!headerIsAttached) {
|
|
1504
|
+
[self attachScrollObserverForCurrentPage];
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
// Property-only updates cannot replace a list. Collect changed parents first
|
|
1508
|
+
// so a burst of unrelated mutations scans the current page at most once.
|
|
1509
|
+
std::unordered_set<NSInteger> changedParentTags;
|
|
1510
|
+
for (const auto &mutation : transaction.getMutations()) {
|
|
1511
|
+
if ((mutation.type == ShadowViewMutation::Insert ||
|
|
1512
|
+
mutation.type == ShadowViewMutation::Remove) && mutation.parentTag > 0) {
|
|
1513
|
+
changedParentTags.insert(mutation.parentTag);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (changedParentTags.empty()) return;
|
|
1517
|
+
|
|
1518
|
+
// A missing list can return in a later transaction. Inspect only the current
|
|
1519
|
+
// page needing an observer and pages whose released identities are retained.
|
|
1520
|
+
for (NSInteger index = 0; index < _pageControllers.count; index++) {
|
|
1521
|
+
BOOL hasReleasedList = _releasedPageScrollStates[[self pageKeyForIndex:index]] != nil;
|
|
1522
|
+
BOOL currentPageNeedsResolve = index == _currentIndex &&
|
|
1523
|
+
(!currentListIsAttached || currentListIsFallback);
|
|
1524
|
+
if (!hasReleasedList && !currentPageNeedsResolve) continue;
|
|
1525
|
+
NSMutableArray<UIView *> *pendingViews =
|
|
1526
|
+
[NSMutableArray arrayWithObject:_pageControllers[index].view];
|
|
1527
|
+
while (pendingViews.count > 0) {
|
|
1528
|
+
UIView *view = pendingViews.lastObject;
|
|
1529
|
+
[pendingViews removeLastObject];
|
|
1530
|
+
if (changedParentTags.find(view.tag) != changedParentTags.end()) {
|
|
1531
|
+
if (hasReleasedList) _needsScrollObserverReattachAfterMount = YES;
|
|
1532
|
+
[self attachScrollObserverForCurrentPage];
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
[pendingViews addObjectsFromArray:view.subviews];
|
|
1536
|
+
}
|
|
1393
1537
|
}
|
|
1394
|
-
if (shouldResolve) [self attachScrollObserverForCurrentPage];
|
|
1395
1538
|
}
|
|
1396
1539
|
|
|
1397
1540
|
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
@@ -1573,6 +1716,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1573
1716
|
{
|
|
1574
1717
|
_isBeingRecycled = YES;
|
|
1575
1718
|
_generation++;
|
|
1719
|
+
_needsScrollObserverReattachAfterMount = NO;
|
|
1720
|
+
[_releasedPageScrollStates removeAllObjects];
|
|
1576
1721
|
[self detachSharedHeaderPressCancellationGesture];
|
|
1577
1722
|
[self detachScrollObserver];
|
|
1578
1723
|
[self restoreSharedHeadersToContainer];
|
|
@@ -1732,7 +1877,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1732
1877
|
NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
|
|
1733
1878
|
NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
|
|
1734
1879
|
id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
1735
|
-
|
|
1880
|
+
NSArray<NSString *> *nextPageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
|
|
1881
|
+
// Stable keys keep their released identity across reorders. A missing key
|
|
1882
|
+
// represents an unmounted React page and must not be reused by a later page.
|
|
1883
|
+
RNCPruneReleasedPageStates(_releasedPageScrollStates, nextPageKeys);
|
|
1884
|
+
_pageKeys = nextPageKeys;
|
|
1736
1885
|
}
|
|
1737
1886
|
if (_needsPropsReapply || oldViewProps.retainedPages != newViewProps.retainedPages) {
|
|
1738
1887
|
_retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
|
|
@@ -1943,6 +2092,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1943
2092
|
|
|
1944
2093
|
- (void)attachScrollObserverForCurrentPage
|
|
1945
2094
|
{
|
|
2095
|
+
[self attachScrollObserverForCurrentPageAfterTransition:NO];
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// OneKey patch: selection completion restores list state synchronously, before
|
|
2099
|
+
// the transition flags are cleared on the next run loop. Other callers wait.
|
|
2100
|
+
- (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled
|
|
2101
|
+
{
|
|
2102
|
+
if (_isBeingRecycled) return;
|
|
2103
|
+
if (_needsScrollObserverReattachAfterMount) {
|
|
2104
|
+
if (self.window == nil || ((_transitioning || _isPagerDragging) && !selectionSettled)) return;
|
|
2105
|
+
[self prepareAdjacentPageInsets];
|
|
2106
|
+
}
|
|
1946
2107
|
[self restoreDetachedScrollInsets];
|
|
1947
2108
|
if (_scrollResolveRetryPageIndex != _currentIndex) {
|
|
1948
2109
|
_scrollResolveRetryToken += 1;
|
|
@@ -1956,7 +2117,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1956
2117
|
if (candidate == nil) {
|
|
1957
2118
|
[self detachScrollObserver];
|
|
1958
2119
|
[self restoreSharedHeadersToContainer];
|
|
1959
|
-
|
|
2120
|
+
// Stop retrying released lists on unrelated commits without discarding a
|
|
2121
|
+
// surviving identity or offset. A relevant insertion resumes recovery.
|
|
2122
|
+
if (_needsScrollObserverReattachAfterMount) {
|
|
2123
|
+
_needsScrollObserverReattachAfterMount = NO;
|
|
2124
|
+
_scrollResolveRetryToken += 1;
|
|
2125
|
+
_scrollResolveRetryScheduled = NO;
|
|
2126
|
+
_scrollResolveRetryAttempt = 0;
|
|
2127
|
+
_pendingFallbackScrollView = nil;
|
|
2128
|
+
_pendingFallbackScrollViewDetectedAt = 0;
|
|
2129
|
+
} else {
|
|
2130
|
+
[self scheduleScrollObserverRetryForCurrentPage];
|
|
2131
|
+
}
|
|
1960
2132
|
return;
|
|
1961
2133
|
}
|
|
1962
2134
|
if ([candidate isKindOfClass:UICollectionView.class] &&
|
|
@@ -1966,7 +2138,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1966
2138
|
_pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
|
|
1967
2139
|
_scrollResolveRetryAttempt = 0;
|
|
1968
2140
|
}
|
|
1969
|
-
if (CACurrentMediaTime() - _pendingFallbackScrollViewDetectedAt <
|
|
2141
|
+
if (CACurrentMediaTime() - _pendingFallbackScrollViewDetectedAt <
|
|
2142
|
+
RNCCollapsiblePagerFallbackDebounceInterval) {
|
|
1970
2143
|
[self scheduleScrollObserverRetryForCurrentPage];
|
|
1971
2144
|
return;
|
|
1972
2145
|
}
|
|
@@ -1983,6 +2156,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1983
2156
|
[candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
|
|
1984
2157
|
}
|
|
1985
2158
|
if (candidate == _observedScrollView) {
|
|
2159
|
+
_needsScrollObserverReattachAfterMount = NO;
|
|
1986
2160
|
[self attachSharedHeadersToScrollView:candidate];
|
|
1987
2161
|
[self updateSharedHeaderPressCancellationGesture];
|
|
1988
2162
|
return;
|
|
@@ -2006,6 +2180,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2006
2180
|
context:RNCCollapsiblePagerContentOffsetContext];
|
|
2007
2181
|
[candidate.panGestureRecognizer addTarget:self action:@selector(observedListPanChanged:)];
|
|
2008
2182
|
_observingContentOffset = YES;
|
|
2183
|
+
_needsScrollObserverReattachAfterMount = NO;
|
|
2009
2184
|
_currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
|
|
2010
2185
|
[self attachSharedHeadersToScrollView:candidate];
|
|
2011
2186
|
[self updateSharedHeaderPressCancellationGesture];
|
|
@@ -2235,6 +2410,39 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2235
2410
|
}
|
|
2236
2411
|
}
|
|
2237
2412
|
|
|
2413
|
+
// OneKey patch: release observers, headers and saved state while the scroll view
|
|
2414
|
+
// still represents the old list, before Fabric applies its next owner's props.
|
|
2415
|
+
- (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
|
|
2416
|
+
{
|
|
2417
|
+
if (scrollView == _observedScrollView) {
|
|
2418
|
+
[self detachScrollObserver];
|
|
2419
|
+
if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
|
|
2420
|
+
}
|
|
2421
|
+
if (scrollView == _sharedHeaderScrollView) {
|
|
2422
|
+
[self restoreSharedHeadersToContainer];
|
|
2423
|
+
}
|
|
2424
|
+
CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
|
|
2425
|
+
UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
2426
|
+
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
2427
|
+
if (appliedTop != nil && scrollView.refreshControl != nil) {
|
|
2428
|
+
original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
|
|
2429
|
+
}
|
|
2430
|
+
scrollView.contentInset = original;
|
|
2431
|
+
scrollView.verticalScrollIndicatorInsets =
|
|
2432
|
+
[_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
2433
|
+
scrollView.alwaysBounceVertical =
|
|
2434
|
+
[_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
|
|
2435
|
+
scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
|
|
2436
|
+
[_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
|
|
2437
|
+
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
|
|
2438
|
+
animated:NO];
|
|
2439
|
+
[_originalInsets removeObjectForKey:scrollView];
|
|
2440
|
+
[_appliedTopInsets removeObjectForKey:scrollView];
|
|
2441
|
+
[_originalIndicatorInsets removeObjectForKey:scrollView];
|
|
2442
|
+
[_originalAlwaysBounceVertical removeObjectForKey:scrollView];
|
|
2443
|
+
[_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2238
2446
|
- (void)restoreDetachedScrollInsets
|
|
2239
2447
|
{
|
|
2240
2448
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
|
|
@@ -2246,32 +2454,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2246
2454
|
}
|
|
2247
2455
|
}
|
|
2248
2456
|
if (belongsToPage) continue;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
_pageOffsets[self.currentPageKey] = @(_headerOffset);
|
|
2252
|
-
}
|
|
2253
|
-
// Fabric can reuse an empty RN ScrollView in another screen. Release all
|
|
2254
|
-
// pager-owned insets before that view is used as a search or dialog list.
|
|
2255
|
-
CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
|
|
2256
|
-
UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
2257
|
-
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
2258
|
-
if (appliedTop != nil && scrollView.refreshControl != nil) {
|
|
2259
|
-
original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
|
|
2260
|
-
}
|
|
2261
|
-
scrollView.contentInset = original;
|
|
2262
|
-
scrollView.verticalScrollIndicatorInsets =
|
|
2263
|
-
[_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
2264
|
-
scrollView.alwaysBounceVertical =
|
|
2265
|
-
[_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
|
|
2266
|
-
scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
|
|
2267
|
-
[_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
|
|
2268
|
-
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
|
|
2269
|
-
animated:NO];
|
|
2270
|
-
[_originalInsets removeObjectForKey:scrollView];
|
|
2271
|
-
[_appliedTopInsets removeObjectForKey:scrollView];
|
|
2272
|
-
[_originalIndicatorInsets removeObjectForKey:scrollView];
|
|
2273
|
-
[_originalAlwaysBounceVertical removeObjectForKey:scrollView];
|
|
2274
|
-
[_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
|
|
2457
|
+
// OneKey patch: share the full release path with pre-mount recycling.
|
|
2458
|
+
[self restoreScrollViewState:scrollView resetSavedOffset:YES];
|
|
2275
2459
|
}
|
|
2276
2460
|
}
|
|
2277
2461
|
|
|
@@ -2279,6 +2463,57 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2279
2463
|
pageIndex:(NSInteger)pageIndex
|
|
2280
2464
|
restore:(BOOL)restore
|
|
2281
2465
|
{
|
|
2466
|
+
// OneKey patch: a surviving list keeps its full offset. A replacement keeps
|
|
2467
|
+
// only header collapse, even if Fabric reused the same native scroll object.
|
|
2468
|
+
NSString *pageKey = [self pageKeyForIndex:pageIndex];
|
|
2469
|
+
RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
|
|
2470
|
+
BOOL deferredReleasedIdentity = NO;
|
|
2471
|
+
if (released != nil) {
|
|
2472
|
+
UIView *componentView = RNCScrollComponentView(scrollView);
|
|
2473
|
+
BOOL sameList = released.scrollView == scrollView && componentView != nil &&
|
|
2474
|
+
released.componentView == componentView && released.componentTag == componentView.tag;
|
|
2475
|
+
// Adjacent Nitro lists are visible only as generic UICollectionViews, so
|
|
2476
|
+
// debounce each replacement candidate independently before rejecting the
|
|
2477
|
+
// released identity. A surviving native list is safe to accept immediately.
|
|
2478
|
+
BOOL needsFallbackDebounce = !sameList &&
|
|
2479
|
+
[scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView;
|
|
2480
|
+
if (needsFallbackDebounce) {
|
|
2481
|
+
CFTimeInterval now = CACurrentMediaTime();
|
|
2482
|
+
if (released.pendingFallbackScrollView != scrollView) {
|
|
2483
|
+
released.pendingFallbackScrollView = scrollView;
|
|
2484
|
+
released.pendingFallbackDetectedAt = now;
|
|
2485
|
+
__weak __typeof__(self) weakSelf = self;
|
|
2486
|
+
__weak RNCCollapsiblePagerReleasedScrollState *weakReleased = released;
|
|
2487
|
+
__weak UIScrollView *weakScrollView = scrollView;
|
|
2488
|
+
NSString *capturedPageKey = [pageKey copy];
|
|
2489
|
+
NSUInteger capturedGeneration = _generation;
|
|
2490
|
+
dispatch_after(
|
|
2491
|
+
dispatch_time(
|
|
2492
|
+
DISPATCH_TIME_NOW,
|
|
2493
|
+
(int64_t)(RNCCollapsiblePagerFallbackDebounceInterval * NSEC_PER_SEC)
|
|
2494
|
+
),
|
|
2495
|
+
dispatch_get_main_queue(),
|
|
2496
|
+
^{
|
|
2497
|
+
RNCCollapsiblePagerViewComponentView *strongSelf = weakSelf;
|
|
2498
|
+
RNCCollapsiblePagerReleasedScrollState *strongReleased = weakReleased;
|
|
2499
|
+
UIScrollView *strongScrollView = weakScrollView;
|
|
2500
|
+
if (strongSelf == nil || strongReleased == nil || strongScrollView == nil ||
|
|
2501
|
+
strongSelf->_isBeingRecycled || strongSelf.window == nil ||
|
|
2502
|
+
strongSelf->_generation != capturedGeneration ||
|
|
2503
|
+
strongSelf->_releasedPageScrollStates[capturedPageKey] != strongReleased ||
|
|
2504
|
+
strongReleased.pendingFallbackScrollView != strongScrollView) return;
|
|
2505
|
+
[strongSelf prepareAdjacentPageInsets];
|
|
2506
|
+
}
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
|
|
2510
|
+
RNCCollapsiblePagerFallbackDebounceInterval;
|
|
2511
|
+
}
|
|
2512
|
+
if (!deferredReleasedIdentity) {
|
|
2513
|
+
if (!sameList) _pageOffsets[pageKey] = @(_headerOffset);
|
|
2514
|
+
[_releasedPageScrollStates removeObjectForKey:pageKey];
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2282
2517
|
NSValue *storedOriginal = [_originalInsets objectForKey:scrollView];
|
|
2283
2518
|
UIEdgeInsets original = storedOriginal == nil
|
|
2284
2519
|
? scrollView.contentInset
|
|
@@ -2327,7 +2562,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2327
2562
|
NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
|
|
2328
2563
|
// The sticky slot can change height between pages. Cache offsets relative
|
|
2329
2564
|
// to content start so a round trip does not add that height difference.
|
|
2330
|
-
CGFloat restoredLogicalOffset = saved == nil
|
|
2565
|
+
CGFloat restoredLogicalOffset = deferredReleasedIdentity || saved == nil
|
|
2566
|
+
? _headerOffset
|
|
2567
|
+
: saved.doubleValue;
|
|
2331
2568
|
// While the shared header is visible, retained pages must agree on its
|
|
2332
2569
|
// collapse position. Deep offsets remain independent after full collapse.
|
|
2333
2570
|
if (_nativeSmoothHeaderScrollEnabled && _headerOffset < _headerHeight) {
|
|
@@ -2483,7 +2720,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2483
2720
|
self->_currentIndex = index;
|
|
2484
2721
|
self->_destinationIndex = index;
|
|
2485
2722
|
[self->_nativeTabBarView setProgress:index];
|
|
2486
|
-
[self
|
|
2723
|
+
[self attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
2487
2724
|
[self emitPageSelected:index];
|
|
2488
2725
|
[self emitDiagnostics:@"page-selected"];
|
|
2489
2726
|
} else if (self->_pendingGoToIndex < 0) {
|
|
@@ -2520,7 +2757,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2520
2757
|
self->_currentIndex = index;
|
|
2521
2758
|
self->_destinationIndex = index;
|
|
2522
2759
|
[self->_nativeTabBarView setProgress:index];
|
|
2523
|
-
[self
|
|
2760
|
+
[self attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
2524
2761
|
[self emitPageSelected:index];
|
|
2525
2762
|
[self emitDiagnostics:@"page-selected"];
|
|
2526
2763
|
} else {
|
|
@@ -2660,7 +2897,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2660
2897
|
0
|
|
2661
2898
|
) animated:NO];
|
|
2662
2899
|
[_nativeTabBarView setProgress:index];
|
|
2663
|
-
[self
|
|
2900
|
+
[self attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
2664
2901
|
if (emitSelection &&
|
|
2665
2902
|
(_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
|
|
2666
2903
|
[self emitPageSelected:index];
|
|
@@ -2711,7 +2948,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2711
2948
|
_currentIndex = index;
|
|
2712
2949
|
_destinationIndex = index;
|
|
2713
2950
|
[_nativeTabBarView setProgress:index];
|
|
2714
|
-
[self
|
|
2951
|
+
[self attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
2715
2952
|
// A newer tab tap remains authoritative while its queued command runs.
|
|
2716
2953
|
if (emitSelection &&
|
|
2717
2954
|
(_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
|
|
3
|
+
#import "RNCCollapsiblePagerReleasedStatePolicy.h"
|
|
4
|
+
|
|
5
|
+
@interface RNCCollapsiblePagerReleasedStatePolicyTests : XCTestCase
|
|
6
|
+
@end
|
|
7
|
+
|
|
8
|
+
@implementation RNCCollapsiblePagerReleasedStatePolicyTests
|
|
9
|
+
|
|
10
|
+
- (void)testPrunesRemovedKeysAndKeepsStableKeysAcrossReorders
|
|
11
|
+
{
|
|
12
|
+
NSObject *alpha = [NSObject new];
|
|
13
|
+
NSObject *beta = [NSObject new];
|
|
14
|
+
NSMutableDictionary<NSString *, id> *releasedStates = [@{
|
|
15
|
+
@"alpha": alpha,
|
|
16
|
+
@"beta": beta,
|
|
17
|
+
} mutableCopy];
|
|
18
|
+
|
|
19
|
+
RNCPruneReleasedPageStates(releasedStates, @[@"beta", @"gamma"]);
|
|
20
|
+
|
|
21
|
+
XCTAssertEqualObjects(releasedStates, (@{ @"beta": beta }));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
- (void)testClearsEveryReleasedStateWhenAllPagesUnmount
|
|
25
|
+
{
|
|
26
|
+
NSMutableDictionary<NSString *, id> *releasedStates = [@{
|
|
27
|
+
@"page-0": [NSObject new],
|
|
28
|
+
@"page-1": [NSObject new],
|
|
29
|
+
} mutableCopy];
|
|
30
|
+
|
|
31
|
+
RNCPruneReleasedPageStates(releasedStates, @[]);
|
|
32
|
+
|
|
33
|
+
XCTAssertEqual(releasedStates.count, 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.135",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"typescript": "^5.9.2"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@onekeyfe/react-native-native-logger": "3.0.
|
|
69
|
+
"@onekeyfe/react-native-native-logger": "3.0.135",
|
|
70
70
|
"react": "*",
|
|
71
71
|
"react-native": "*"
|
|
72
72
|
},
|
|
@@ -14,6 +14,11 @@ Pod::Spec.new do |s|
|
|
|
14
14
|
s.source = { :git => "https://github.com/nicholasxuu/app-modules.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm}"
|
|
17
|
+
s.exclude_files = "ios/tests/**/*"
|
|
18
|
+
|
|
19
|
+
s.test_spec "Tests" do |test_spec|
|
|
20
|
+
test_spec.source_files = "ios/tests/**/*.{m,mm}"
|
|
21
|
+
end
|
|
17
22
|
|
|
18
23
|
install_modules_dependencies(s)
|
|
19
24
|
|