@onekeyfe/react-native-pager-view 3.0.113 → 3.0.115
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/CollapsiblePagerHost.kt +143 -16
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +9 -2
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +3 -1
- package/ios/RNCCollapsiblePagerViewComponentView.mm +291 -38
- package/ios/RNCPagerViewComponentView.mm +4 -2
- package/lib/module/CollapsiblePagerView.js +21 -6
- package/lib/module/CollapsiblePagerView.web.js +149 -35
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +2 -0
- package/lib/module/index.js +2 -2
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +12 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -0
- package/lib/typescript/src/index.d.ts +5 -5
- package/package.json +1 -1
- package/src/CollapsiblePagerView.tsx +41 -6
- package/src/CollapsiblePagerView.web.tsx +198 -47
- package/src/CollapsiblePagerViewNativeComponent.ts +2 -0
- package/src/index.tsx +5 -5
|
@@ -15,8 +15,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
15
15
|
RCTRNCCollapsiblePagerViewViewProtocol,
|
|
16
16
|
UIPageViewControllerDataSource,
|
|
17
17
|
UIPageViewControllerDelegate,
|
|
18
|
-
UIScrollViewDelegate
|
|
18
|
+
UIScrollViewDelegate,
|
|
19
|
+
UIGestureRecognizerDelegate
|
|
19
20
|
>
|
|
21
|
+
- (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
|
|
20
22
|
@end
|
|
21
23
|
|
|
22
24
|
@implementation RNCCollapsiblePagerViewComponentView {
|
|
@@ -34,13 +36,23 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
34
36
|
NSInteger _stickyHeaderHeight;
|
|
35
37
|
CGFloat _headerOffset;
|
|
36
38
|
BOOL _scrollEnabled;
|
|
39
|
+
// OneKey patch: Coordinate this inner pager with an outer horizontal pager.
|
|
40
|
+
BOOL _nestedScrollEnabled;
|
|
41
|
+
UIPanGestureRecognizer *_blockerGesture;
|
|
37
42
|
BOOL _transitioning;
|
|
43
|
+
BOOL _isPagerDragging;
|
|
44
|
+
BOOL _hasReceivedPageCommand;
|
|
45
|
+
NSUInteger _transitionId;
|
|
46
|
+
// OneKey patch: Retain the latest tab tap while an animation is in flight.
|
|
47
|
+
NSInteger _pendingGoToIndex;
|
|
38
48
|
BOOL _hasAppliedInitialPage;
|
|
49
|
+
BOOL _needsPropsReapply;
|
|
39
50
|
NSString *_layoutDirection;
|
|
40
51
|
NSArray<NSString *> *_pageKeys;
|
|
41
52
|
NSString *_retainedPages;
|
|
42
53
|
NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
|
|
43
54
|
NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
|
|
55
|
+
NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
|
|
44
56
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
45
57
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
46
58
|
NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
|
|
@@ -64,6 +76,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
64
76
|
_pageControllers = [NSMutableArray new];
|
|
65
77
|
_pageOffsets = [NSMutableDictionary new];
|
|
66
78
|
_originalInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
79
|
+
_appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
67
80
|
_originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
68
81
|
_originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
|
|
69
82
|
_originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
|
|
@@ -73,7 +86,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
73
86
|
_destinationIndex = 0;
|
|
74
87
|
_pendingInitialPage = 0;
|
|
75
88
|
_scrollEnabled = YES;
|
|
89
|
+
_nestedScrollEnabled = NO;
|
|
90
|
+
_pendingGoToIndex = -1;
|
|
76
91
|
_hasAppliedInitialPage = NO;
|
|
92
|
+
_needsPropsReapply = YES;
|
|
77
93
|
_layoutDirection = @"ltr";
|
|
78
94
|
|
|
79
95
|
_containerView = [UIView new];
|
|
@@ -94,6 +110,17 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
- (void)didMoveToWindow
|
|
114
|
+
{
|
|
115
|
+
[super didMoveToWindow];
|
|
116
|
+
// OneKey patch: removing a decelerating page from its window can suppress
|
|
117
|
+
// UIKit's final scroll callback. Restore the last acknowledged page on reattach.
|
|
118
|
+
if (self.window != nil && _isPagerDragging &&
|
|
119
|
+
!_pagerScrollView.dragging && !_pagerScrollView.decelerating) {
|
|
120
|
+
[self finishPagerScrollEmittingSelection:NO];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
- (void)initializePageViewController
|
|
98
125
|
{
|
|
99
126
|
_pageViewController = [[UIPageViewController alloc]
|
|
@@ -102,7 +129,9 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
102
129
|
options:nil];
|
|
103
130
|
_pageViewController.dataSource = self;
|
|
104
131
|
_pageViewController.delegate = self;
|
|
105
|
-
|
|
132
|
+
// Fabric may mount header slots before a recycled pager reattaches.
|
|
133
|
+
// Keep the recreated page container below those existing headers.
|
|
134
|
+
[_containerView insertSubview:_pageViewController.view atIndex:0];
|
|
106
135
|
|
|
107
136
|
for (UIView *subview in _pageViewController.view.subviews) {
|
|
108
137
|
if ([subview isKindOfClass:UIScrollView.class]) {
|
|
@@ -113,6 +142,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
113
142
|
break;
|
|
114
143
|
}
|
|
115
144
|
}
|
|
145
|
+
[self applyNestedScrollBlocker];
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
#pragma mark - React mounting
|
|
@@ -140,6 +170,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
140
170
|
- (void)rebuildSlots
|
|
141
171
|
{
|
|
142
172
|
[self detachScrollObserver];
|
|
173
|
+
// OneKey patch: Fabric reuses these views without resetting transforms
|
|
174
|
+
// written by a native parent. Release our collapse translation with the slot.
|
|
175
|
+
_headerView.transform = CGAffineTransformIdentity;
|
|
176
|
+
_stickyHeaderView.transform = CGAffineTransformIdentity;
|
|
143
177
|
[_headerView removeFromSuperview];
|
|
144
178
|
[_stickyHeaderView removeFromSuperview];
|
|
145
179
|
_headerView = nil;
|
|
@@ -166,7 +200,8 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
166
200
|
_pendingInitialPage >= 0 &&
|
|
167
201
|
_pendingInitialPage < _pageControllers.count) {
|
|
168
202
|
_currentIndex = _pendingInitialPage;
|
|
169
|
-
|
|
203
|
+
// A recycled host may receive slots before its page controller exists.
|
|
204
|
+
_hasAppliedInitialPage = _pageViewController != nil;
|
|
170
205
|
} else {
|
|
171
206
|
_currentIndex = MIN(MAX(_currentIndex, 0), _pageControllers.count - 1);
|
|
172
207
|
}
|
|
@@ -192,6 +227,19 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
192
227
|
[super layoutSubviews];
|
|
193
228
|
_containerView.frame = self.bounds;
|
|
194
229
|
_pageViewController.view.frame = _containerView.bounds;
|
|
230
|
+
// OneKey patch: Fabric may mount page slots before applying the host props.
|
|
231
|
+
// Commit the pending initial page once both props and page controllers exist.
|
|
232
|
+
if (!_hasAppliedInitialPage && _pageViewController != nil &&
|
|
233
|
+
_pendingInitialPage >= 0 && _pendingInitialPage < _pageControllers.count) {
|
|
234
|
+
[self detachScrollObserver];
|
|
235
|
+
_currentIndex = _pendingInitialPage;
|
|
236
|
+
_destinationIndex = _currentIndex;
|
|
237
|
+
_hasAppliedInitialPage = YES;
|
|
238
|
+
[_pageViewController setViewControllers:@[_pageControllers[_currentIndex]]
|
|
239
|
+
direction:UIPageViewControllerNavigationDirectionForward
|
|
240
|
+
animated:NO
|
|
241
|
+
completion:nil];
|
|
242
|
+
}
|
|
195
243
|
// UIKit frame assignment is undefined while a view has a non-identity transform.
|
|
196
244
|
// Lay out the original slots, then restore the shared collapse translation.
|
|
197
245
|
_headerView.transform = CGAffineTransformIdentity;
|
|
@@ -203,6 +251,8 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
203
251
|
self.bounds.size.width,
|
|
204
252
|
_stickyHeaderHeight
|
|
205
253
|
);
|
|
254
|
+
// Settle UIKit page containers before sizing their autoresizing child views.
|
|
255
|
+
[_pageViewController.view layoutIfNeeded];
|
|
206
256
|
for (UIViewController *controller in _pageControllers) {
|
|
207
257
|
controller.view.frame = _pageViewController.view.bounds;
|
|
208
258
|
controller.view.subviews.firstObject.frame = controller.view.bounds;
|
|
@@ -223,7 +273,14 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
223
273
|
[self detachScrollObserver];
|
|
224
274
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
225
275
|
NSValue *value = [_originalInsets objectForKey:scrollView];
|
|
226
|
-
if (value != nil)
|
|
276
|
+
if (value != nil) {
|
|
277
|
+
UIEdgeInsets original = value.UIEdgeInsetsValue;
|
|
278
|
+
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
279
|
+
if (appliedTop != nil && scrollView.refreshControl != nil) {
|
|
280
|
+
original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
|
|
281
|
+
}
|
|
282
|
+
scrollView.contentInset = original;
|
|
283
|
+
}
|
|
227
284
|
NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
|
|
228
285
|
if (indicatorValue != nil) {
|
|
229
286
|
scrollView.verticalScrollIndicatorInsets = indicatorValue.UIEdgeInsetsValue;
|
|
@@ -236,9 +293,14 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
236
293
|
}
|
|
237
294
|
}
|
|
238
295
|
[_originalInsets removeAllObjects];
|
|
296
|
+
[_appliedTopInsets removeAllObjects];
|
|
239
297
|
[_originalIndicatorInsets removeAllObjects];
|
|
240
298
|
[_originalAlwaysBounceVertical removeAllObjects];
|
|
241
299
|
[_originalInsetAdjustmentBehavior removeAllObjects];
|
|
300
|
+
// OneKey patch: Fabric reuses these views without resetting transforms
|
|
301
|
+
// written by a native parent. Release our collapse translation with the slot.
|
|
302
|
+
_headerView.transform = CGAffineTransformIdentity;
|
|
303
|
+
_stickyHeaderView.transform = CGAffineTransformIdentity;
|
|
242
304
|
[_headerView removeFromSuperview];
|
|
243
305
|
[_stickyHeaderView removeFromSuperview];
|
|
244
306
|
[_logicalChildren removeAllObjects];
|
|
@@ -248,6 +310,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
248
310
|
_pageViewController.dataSource = nil;
|
|
249
311
|
_pageViewController.delegate = nil;
|
|
250
312
|
_pagerScrollView.delegate = nil;
|
|
313
|
+
if (_blockerGesture != nil) {
|
|
314
|
+
[self removeGestureRecognizer:_blockerGesture];
|
|
315
|
+
_blockerGesture = nil;
|
|
316
|
+
}
|
|
251
317
|
[_pageViewController.view removeFromSuperview];
|
|
252
318
|
_pageViewController = nil;
|
|
253
319
|
_pagerScrollView = nil;
|
|
@@ -262,8 +328,14 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
262
328
|
_stickyHeaderHeight = 0;
|
|
263
329
|
_headerOffset = 0;
|
|
264
330
|
_scrollEnabled = YES;
|
|
331
|
+
_nestedScrollEnabled = NO;
|
|
265
332
|
_transitioning = NO;
|
|
333
|
+
_isPagerDragging = NO;
|
|
334
|
+
_hasReceivedPageCommand = NO;
|
|
335
|
+
_pendingGoToIndex = -1;
|
|
266
336
|
_hasAppliedInitialPage = NO;
|
|
337
|
+
// OneKey patch: Fabric retains old props when recycling this view.
|
|
338
|
+
_needsPropsReapply = YES;
|
|
267
339
|
_layoutDirection = @"ltr";
|
|
268
340
|
_isBeingRecycled = NO;
|
|
269
341
|
}
|
|
@@ -275,38 +347,46 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
275
347
|
const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
|
|
276
348
|
const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
|
|
277
349
|
|
|
278
|
-
if (oldViewProps.initialPage != newViewProps.initialPage) {
|
|
350
|
+
if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
|
|
279
351
|
_pendingInitialPage = MAX(0, newViewProps.initialPage);
|
|
280
|
-
|
|
352
|
+
// Focus commands can arrive before the recycled host receives its props.
|
|
353
|
+
_hasAppliedInitialPage = _hasReceivedPageCommand;
|
|
354
|
+
// OneKey patch: Prop updates can arrive after the last slot was mounted.
|
|
355
|
+
[self setNeedsLayout];
|
|
281
356
|
}
|
|
282
|
-
if (oldViewProps.scrollEnabled != newViewProps.scrollEnabled) {
|
|
357
|
+
if (_needsPropsReapply || oldViewProps.scrollEnabled != newViewProps.scrollEnabled) {
|
|
283
358
|
_scrollEnabled = newViewProps.scrollEnabled;
|
|
284
359
|
_pagerScrollView.scrollEnabled = _scrollEnabled;
|
|
285
360
|
}
|
|
286
|
-
if (oldViewProps.
|
|
361
|
+
if (_needsPropsReapply || oldViewProps.nestedScrollEnabled != newViewProps.nestedScrollEnabled) {
|
|
362
|
+
_nestedScrollEnabled = newViewProps.nestedScrollEnabled;
|
|
363
|
+
[self applyNestedScrollBlocker];
|
|
364
|
+
}
|
|
365
|
+
if (_needsPropsReapply || oldViewProps.layoutDirection != newViewProps.layoutDirection) {
|
|
287
366
|
_layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
|
|
288
367
|
}
|
|
289
368
|
BOOL insetsChanged = NO;
|
|
290
|
-
if (oldViewProps.headerHeight != newViewProps.headerHeight) {
|
|
369
|
+
if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
|
|
291
370
|
_headerHeight = MAX(0, newViewProps.headerHeight);
|
|
292
371
|
_headerOffset = MIN(_headerOffset, _headerHeight);
|
|
293
372
|
insetsChanged = YES;
|
|
294
373
|
}
|
|
295
|
-
if (oldViewProps.stickyHeaderHeight != newViewProps.stickyHeaderHeight) {
|
|
374
|
+
if (_needsPropsReapply || oldViewProps.stickyHeaderHeight != newViewProps.stickyHeaderHeight) {
|
|
296
375
|
_stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
|
|
297
376
|
insetsChanged = YES;
|
|
298
377
|
}
|
|
299
|
-
if (oldViewProps.pageKeys != newViewProps.pageKeys) {
|
|
378
|
+
if (_needsPropsReapply || oldViewProps.pageKeys != newViewProps.pageKeys) {
|
|
300
379
|
NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
|
|
301
380
|
NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
|
|
302
381
|
id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
303
382
|
_pageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
|
|
304
383
|
}
|
|
305
|
-
if (oldViewProps.retainedPages != newViewProps.retainedPages) {
|
|
384
|
+
if (_needsPropsReapply || oldViewProps.retainedPages != newViewProps.retainedPages) {
|
|
306
385
|
_retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
|
|
307
386
|
[self setNeedsLayout];
|
|
308
387
|
}
|
|
309
388
|
|
|
389
|
+
_needsPropsReapply = NO;
|
|
310
390
|
[super updateProps:props oldProps:oldProps];
|
|
311
391
|
if (insetsChanged) {
|
|
312
392
|
[self reapplyInsetsToObservedScrollView];
|
|
@@ -358,7 +438,12 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
358
438
|
[self restoreDetachedScrollInsets];
|
|
359
439
|
if (_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
|
|
360
440
|
UIScrollView *candidate = [self findVerticalScrollViewInView:_pageControllers[_currentIndex].view];
|
|
361
|
-
if (candidate == nil
|
|
441
|
+
if (candidate == nil) return;
|
|
442
|
+
// A decelerating list can otherwise claim a new horizontal touch immediately.
|
|
443
|
+
if (_pagerScrollView != nil) {
|
|
444
|
+
[candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
|
|
445
|
+
}
|
|
446
|
+
if (candidate == _observedScrollView) return;
|
|
362
447
|
|
|
363
448
|
BOOL replacingCurrentScrollView = _observingContentOffset;
|
|
364
449
|
[self detachScrollObserver];
|
|
@@ -416,6 +501,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
416
501
|
// pager-owned insets before that view is used as a search or dialog list.
|
|
417
502
|
CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
|
|
418
503
|
UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
504
|
+
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
505
|
+
if (appliedTop != nil && scrollView.refreshControl != nil) {
|
|
506
|
+
original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
|
|
507
|
+
}
|
|
419
508
|
scrollView.contentInset = original;
|
|
420
509
|
scrollView.verticalScrollIndicatorInsets =
|
|
421
510
|
[_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
@@ -426,6 +515,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
426
515
|
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
|
|
427
516
|
animated:NO];
|
|
428
517
|
[_originalInsets removeObjectForKey:scrollView];
|
|
518
|
+
[_appliedTopInsets removeObjectForKey:scrollView];
|
|
429
519
|
[_originalIndicatorInsets removeObjectForKey:scrollView];
|
|
430
520
|
[_originalAlwaysBounceVertical removeObjectForKey:scrollView];
|
|
431
521
|
[_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
|
|
@@ -452,9 +542,17 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
452
542
|
// This host already includes the fixed header and safe area in its frame.
|
|
453
543
|
// UIKit automatic adjustment would add them a second time to short pages.
|
|
454
544
|
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
455
|
-
CGFloat
|
|
545
|
+
CGFloat previousTopInset = scrollView.contentInset.top;
|
|
546
|
+
CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
|
|
547
|
+
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
548
|
+
// UIRefreshControl owns its temporary top inset, including while ending.
|
|
549
|
+
CGFloat refreshInset = appliedTop != nil && scrollView.refreshControl != nil
|
|
550
|
+
? MAX(0, previousTopInset - appliedTop.doubleValue) : 0;
|
|
456
551
|
UIEdgeInsets next = original;
|
|
457
552
|
next.top += _headerHeight + _stickyHeaderHeight;
|
|
553
|
+
CGFloat pagerTopInset = next.top;
|
|
554
|
+
next.top += refreshInset;
|
|
555
|
+
[_appliedTopInsets setObject:@(pagerTopInset) forKey:scrollView];
|
|
458
556
|
// Short pages still need enough range to collapse the header; long pages
|
|
459
557
|
// must end at the last row rather than leave a header-sized blank footer.
|
|
460
558
|
next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
|
|
@@ -467,7 +565,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
467
565
|
}
|
|
468
566
|
scrollView.contentInset = next;
|
|
469
567
|
UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
|
|
470
|
-
indicatorInsets.top =
|
|
568
|
+
indicatorInsets.top = pagerTopInset;
|
|
471
569
|
scrollView.verticalScrollIndicatorInsets = indicatorInsets;
|
|
472
570
|
scrollView.alwaysBounceVertical = YES;
|
|
473
571
|
|
|
@@ -479,7 +577,10 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
479
577
|
targetOffset = (saved == nil ? _headerOffset : saved.doubleValue) - next.top;
|
|
480
578
|
targetOffset = MAX(targetOffset, -next.top + _headerOffset);
|
|
481
579
|
}
|
|
482
|
-
|
|
580
|
+
// A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
|
|
581
|
+
if (restore || previousTopInset != next.top) {
|
|
582
|
+
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
|
|
583
|
+
}
|
|
483
584
|
if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
|
|
484
585
|
}
|
|
485
586
|
|
|
@@ -553,7 +654,24 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
553
654
|
- (void)goTo:(NSInteger)index animated:(BOOL)animated
|
|
554
655
|
{
|
|
555
656
|
if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
|
|
556
|
-
|
|
657
|
+
_hasReceivedPageCommand = YES;
|
|
658
|
+
_hasAppliedInitialPage = YES;
|
|
659
|
+
if (_transitioning && animated) {
|
|
660
|
+
_pendingGoToIndex = index;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
_pendingGoToIndex = -1;
|
|
665
|
+
|
|
666
|
+
// Re-selecting the displayed controller removes its view briefly and cancels
|
|
667
|
+
// an in-flight list touch. Focus synchronization must be idempotent.
|
|
668
|
+
if (!_transitioning && index == _currentIndex &&
|
|
669
|
+
_pageViewController.viewControllers.firstObject == _pageControllers[index]) {
|
|
670
|
+
[self attachScrollObserverForCurrentPage];
|
|
671
|
+
[self emitPageSelected:index];
|
|
672
|
+
[self emitDiagnostics:@"page-selected"];
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
557
675
|
|
|
558
676
|
[self detachScrollObserver];
|
|
559
677
|
_destinationIndex = index;
|
|
@@ -563,6 +681,8 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
563
681
|
? UIPageViewControllerNavigationDirectionForward
|
|
564
682
|
: UIPageViewControllerNavigationDirectionReverse;
|
|
565
683
|
_transitioning = YES;
|
|
684
|
+
_isPagerDragging = NO;
|
|
685
|
+
NSUInteger capturedTransition = ++_transitionId;
|
|
566
686
|
NSUInteger capturedGeneration = _generation;
|
|
567
687
|
__weak __typeof__(self) weakSelf = self;
|
|
568
688
|
[_pageViewController setViewControllers:@[_pageControllers[index]]
|
|
@@ -570,12 +690,22 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
570
690
|
animated:animated && index != _currentIndex
|
|
571
691
|
completion:^(BOOL finished) {
|
|
572
692
|
__strong __typeof__(weakSelf) self = weakSelf;
|
|
573
|
-
if (self == nil || self->_generation != capturedGeneration ||
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
693
|
+
if (self == nil || self->_generation != capturedGeneration ||
|
|
694
|
+
self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
|
|
695
|
+
if (finished) {
|
|
696
|
+
self->_currentIndex = index;
|
|
697
|
+
[self attachScrollObserverForCurrentPage];
|
|
698
|
+
[self emitPageSelected:index];
|
|
699
|
+
[self emitDiagnostics:@"page-selected"];
|
|
700
|
+
}
|
|
701
|
+
// UIKit is still unwinding its transition when this completion runs.
|
|
702
|
+
// Start a queued animation only after that callback has returned.
|
|
703
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
704
|
+
if (self->_generation != capturedGeneration ||
|
|
705
|
+
self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
|
|
706
|
+
self->_transitioning = NO;
|
|
707
|
+
[self drainPendingGoTo];
|
|
708
|
+
});
|
|
579
709
|
}];
|
|
580
710
|
}
|
|
581
711
|
|
|
@@ -604,27 +734,23 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
604
734
|
return [self adjacentController:viewController delta:self.isLtrLayout ? -1 : 1];
|
|
605
735
|
}
|
|
606
736
|
|
|
607
|
-
- (void)
|
|
608
|
-
didFinishAnimating:(BOOL)finished
|
|
609
|
-
previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers
|
|
610
|
-
transitionCompleted:(BOOL)completed
|
|
737
|
+
- (void)drainPendingGoTo
|
|
611
738
|
{
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
[self detachScrollObserver];
|
|
618
|
-
_currentIndex = index;
|
|
619
|
-
[self attachScrollObserverForCurrentPage];
|
|
620
|
-
[self emitPageSelected:index];
|
|
621
|
-
[self emitDiagnostics:@"page-selected"];
|
|
739
|
+
NSInteger pending = _pendingGoToIndex;
|
|
740
|
+
_pendingGoToIndex = -1;
|
|
741
|
+
if (pending >= 0 && pending != _currentIndex) {
|
|
742
|
+
[self goTo:pending animated:YES];
|
|
743
|
+
}
|
|
622
744
|
}
|
|
623
745
|
|
|
624
746
|
#pragma mark - Pager UIScrollViewDelegate
|
|
625
747
|
|
|
626
748
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
|
627
749
|
{
|
|
750
|
+
// A user drag owns the result even if UIKit later completes the old animation.
|
|
751
|
+
++_transitionId;
|
|
752
|
+
_isPagerDragging = YES;
|
|
753
|
+
_transitioning = YES;
|
|
628
754
|
const auto emitter = [self eventEmitter];
|
|
629
755
|
if (emitter) {
|
|
630
756
|
emitter->onPageScrollStateChanged({
|
|
@@ -645,8 +771,57 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
645
771
|
}
|
|
646
772
|
}
|
|
647
773
|
|
|
648
|
-
- (void)
|
|
774
|
+
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
|
|
775
|
+
willDecelerate:(BOOL)decelerate
|
|
649
776
|
{
|
|
777
|
+
if (!decelerate) [self scrollViewDidEndDecelerating:scrollView];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
- (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection
|
|
781
|
+
{
|
|
782
|
+
if (_isPagerDragging) {
|
|
783
|
+
UIViewController *controller = nil;
|
|
784
|
+
if (!emitSelection && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
|
|
785
|
+
// Detaching can make UIKit settle on a neighbouring controller without a
|
|
786
|
+
// completion callback. Keep the last page acknowledged by JavaScript.
|
|
787
|
+
controller = _pageControllers[_currentIndex];
|
|
788
|
+
} else {
|
|
789
|
+
controller = _pageViewController.viewControllers.firstObject;
|
|
790
|
+
// An interrupted programmatic animation can report its target while a
|
|
791
|
+
// different controller is centered. Read the settled UIKit viewport.
|
|
792
|
+
for (UIViewController *candidate in _pageControllers) {
|
|
793
|
+
if (candidate.view.window == nil) continue;
|
|
794
|
+
CGRect frame = [candidate.view convertRect:candidate.view.bounds toView:_containerView];
|
|
795
|
+
if (fabs(frame.origin.x) < 1 &&
|
|
796
|
+
fabs(frame.size.width - _containerView.bounds.size.width) < 1) {
|
|
797
|
+
controller = candidate;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
|
|
803
|
+
if (index != NSNotFound) {
|
|
804
|
+
if (_pageViewController.viewControllers.firstObject != controller) {
|
|
805
|
+
// Correct UIKit only after its animation completion has returned.
|
|
806
|
+
[_pageViewController setViewControllers:@[controller]
|
|
807
|
+
direction:UIPageViewControllerNavigationDirectionForward
|
|
808
|
+
animated:NO
|
|
809
|
+
completion:nil];
|
|
810
|
+
}
|
|
811
|
+
[self detachScrollObserver];
|
|
812
|
+
_currentIndex = index;
|
|
813
|
+
_destinationIndex = index;
|
|
814
|
+
[self attachScrollObserverForCurrentPage];
|
|
815
|
+
// A newer tab tap remains authoritative while its queued command runs.
|
|
816
|
+
if (emitSelection &&
|
|
817
|
+
(_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
|
|
818
|
+
[self emitPageSelected:index];
|
|
819
|
+
[self emitDiagnostics:@"page-selected"];
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
_isPagerDragging = NO;
|
|
823
|
+
_transitioning = NO;
|
|
824
|
+
}
|
|
650
825
|
const auto emitter = [self eventEmitter];
|
|
651
826
|
if (emitter) {
|
|
652
827
|
emitter->onPageScrollStateChanged({
|
|
@@ -654,6 +829,12 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
654
829
|
});
|
|
655
830
|
}
|
|
656
831
|
[self emitDiagnostics:@"pager-idle"];
|
|
832
|
+
[self drainPendingGoTo];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
|
836
|
+
{
|
|
837
|
+
[self finishPagerScrollEmittingSelection:YES];
|
|
657
838
|
}
|
|
658
839
|
|
|
659
840
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
|
@@ -725,6 +906,78 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
|
|
|
725
906
|
_pagerScrollView.scrollEnabled = scrollEnabled;
|
|
726
907
|
}
|
|
727
908
|
|
|
909
|
+
#pragma mark - Nested pager gesture coordination
|
|
910
|
+
|
|
911
|
+
- (void)applyNestedScrollBlocker
|
|
912
|
+
{
|
|
913
|
+
if (_pagerScrollView == nil) return;
|
|
914
|
+
|
|
915
|
+
if (_nestedScrollEnabled && _blockerGesture == nil) {
|
|
916
|
+
_blockerGesture = [[UIPanGestureRecognizer alloc]
|
|
917
|
+
initWithTarget:self
|
|
918
|
+
action:@selector(blockerGestureFired:)];
|
|
919
|
+
_blockerGesture.delegate = self;
|
|
920
|
+
_blockerGesture.cancelsTouchesInView = NO;
|
|
921
|
+
_blockerGesture.delaysTouchesBegan = NO;
|
|
922
|
+
[self addGestureRecognizer:_blockerGesture];
|
|
923
|
+
[_pagerScrollView.panGestureRecognizer requireGestureRecognizerToFail:_blockerGesture];
|
|
924
|
+
} else if (!_nestedScrollEnabled && _blockerGesture != nil) {
|
|
925
|
+
[self removeGestureRecognizer:_blockerGesture];
|
|
926
|
+
_blockerGesture = nil;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
- (void)blockerGestureFired:(UIPanGestureRecognizer *)recognizer
|
|
931
|
+
{
|
|
932
|
+
// Reset through the public enabled API. UIGestureRecognizer.state is
|
|
933
|
+
// read-only for clients; only recognizer subclasses may assign it.
|
|
934
|
+
if (recognizer.state == UIGestureRecognizerStateBegan) {
|
|
935
|
+
recognizer.enabled = NO;
|
|
936
|
+
recognizer.enabled = YES;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
|
941
|
+
{
|
|
942
|
+
if (gestureRecognizer != _blockerGesture) return YES;
|
|
943
|
+
if (!_nestedScrollEnabled) return NO;
|
|
944
|
+
if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) return NO;
|
|
945
|
+
|
|
946
|
+
UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
|
|
947
|
+
CGPoint velocity = [pan velocityInView:self];
|
|
948
|
+
NSInteger pageCount = _pageControllers.count;
|
|
949
|
+
if (pageCount == 0 || _currentIndex < 0 || _currentIndex >= pageCount) return NO;
|
|
950
|
+
|
|
951
|
+
CGFloat absVelocityX = fabs(velocity.x);
|
|
952
|
+
CGFloat absVelocityY = fabs(velocity.y);
|
|
953
|
+
if (absVelocityX <= absVelocityY || absVelocityX < 1.0f) return NO;
|
|
954
|
+
|
|
955
|
+
BOOL isLtr = self.isLtrLayout;
|
|
956
|
+
NSInteger firstPageIndex = isLtr ? 0 : pageCount - 1;
|
|
957
|
+
NSInteger lastPageIndex = isLtr ? pageCount - 1 : 0;
|
|
958
|
+
if (_currentIndex == firstPageIndex && velocity.x > 0) return YES;
|
|
959
|
+
if (_currentIndex == lastPageIndex && velocity.x < 0) return YES;
|
|
960
|
+
return NO;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
|
|
964
|
+
{
|
|
965
|
+
// OneKey patch: a new touch can cancel UIKit's animation before its first
|
|
966
|
+
// frame without starting a drag or completing the animation. Finish the
|
|
967
|
+
// commanded page before handing that touch to the nested scroll views.
|
|
968
|
+
if (gestureRecognizer == _blockerGesture && _transitioning && !_isPagerDragging &&
|
|
969
|
+
[touch.view isDescendantOfView:_pagerScrollView]) {
|
|
970
|
+
[self goTo:_destinationIndex animated:NO];
|
|
971
|
+
}
|
|
972
|
+
return YES;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
976
|
+
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
977
|
+
{
|
|
978
|
+
return gestureRecognizer == _blockerGesture;
|
|
979
|
+
}
|
|
980
|
+
|
|
728
981
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
729
982
|
{
|
|
730
983
|
return concreteComponentDescriptorProvider<RNCCollapsiblePagerViewComponentDescriptor>();
|
|
@@ -593,9 +593,11 @@ using namespace facebook::react;
|
|
|
593
593
|
|
|
594
594
|
// The blocker gesture never actually does anything — it only serves as a gate
|
|
595
595
|
- (void)blockerGestureFired:(UIPanGestureRecognizer *)recognizer {
|
|
596
|
-
//
|
|
596
|
+
// Reset through the public enabled API. UIGestureRecognizer.state is
|
|
597
|
+
// read-only for clients; only recognizer subclasses may assign it.
|
|
597
598
|
if (recognizer.state == UIGestureRecognizerStateBegan) {
|
|
598
|
-
recognizer.
|
|
599
|
+
recognizer.enabled = NO;
|
|
600
|
+
recognizer.enabled = YES;
|
|
599
601
|
}
|
|
600
602
|
}
|
|
601
603
|
|
|
@@ -4,6 +4,11 @@ import React from "react";
|
|
|
4
4
|
import { I18nManager, Platform, StyleSheet, View } from "react-native";
|
|
5
5
|
import CollapsiblePagerViewNativeComponent, { Commands as CollapsiblePagerViewNativeCommands } from "./CollapsiblePagerViewNativeComponent";
|
|
6
6
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
const pageIndex = value => {
|
|
8
|
+
const index = Math.trunc(value);
|
|
9
|
+
return Number.isFinite(index) ? index : null;
|
|
10
|
+
};
|
|
11
|
+
const clampedPageIndex = (value, pageCount) => Math.min(Math.max(0, pageIndex(value) ?? 0), Math.max(0, pageCount - 1));
|
|
7
12
|
const pageKey = (child, index) => {
|
|
8
13
|
if (/*#__PURE__*/React.isValidElement(child) && child.key != null) {
|
|
9
14
|
return String(child.key);
|
|
@@ -18,7 +23,7 @@ const pageKey = (child, index) => {
|
|
|
18
23
|
*/
|
|
19
24
|
export class CollapsiblePagerView extends React.PureComponent {
|
|
20
25
|
state = {
|
|
21
|
-
selectedPage:
|
|
26
|
+
selectedPage: clampedPageIndex(this.props.initialPage ?? 0, React.Children.toArray(this.props.children).length)
|
|
22
27
|
};
|
|
23
28
|
nativeRef = null;
|
|
24
29
|
lastMountSignature = null;
|
|
@@ -26,16 +31,26 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
26
31
|
this.notifyMountedPagesChanged();
|
|
27
32
|
}
|
|
28
33
|
componentDidUpdate() {
|
|
34
|
+
const pageCount = this.pages().length;
|
|
35
|
+
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
36
|
+
if (selectedPage !== this.state.selectedPage) {
|
|
37
|
+
this.setState({
|
|
38
|
+
selectedPage
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
29
42
|
this.notifyMountedPagesChanged();
|
|
30
43
|
}
|
|
31
44
|
setPage = selectedPage => {
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
const position = pageIndex(selectedPage);
|
|
46
|
+
if (this.nativeRef && position !== null && position >= 0 && position < this.pages().length) {
|
|
47
|
+
CollapsiblePagerViewNativeCommands.setPage(this.nativeRef, position);
|
|
34
48
|
}
|
|
35
49
|
};
|
|
36
50
|
setPageWithoutAnimation = selectedPage => {
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
const position = pageIndex(selectedPage);
|
|
52
|
+
if (this.nativeRef && position !== null && position >= 0 && position < this.pages().length) {
|
|
53
|
+
CollapsiblePagerViewNativeCommands.setPageWithoutAnimation(this.nativeRef, position);
|
|
39
54
|
}
|
|
40
55
|
};
|
|
41
56
|
setScrollEnabled = scrollEnabled => {
|
|
@@ -78,7 +93,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
78
93
|
this.props.onMountedPagesChanged(state);
|
|
79
94
|
};
|
|
80
95
|
onPageSelected = event => {
|
|
81
|
-
const position =
|
|
96
|
+
const position = clampedPageIndex(event.nativeEvent.position, this.pages().length);
|
|
82
97
|
if (position !== this.state.selectedPage) {
|
|
83
98
|
this.setState({
|
|
84
99
|
selectedPage: position
|