@onekeyfe/react-native-pager-view 3.0.114 → 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.
Files changed (29) hide show
  1. package/README.md +40 -0
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +630 -0
  3. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +222 -0
  4. package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +3 -1
  5. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  6. package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +39 -0
  7. package/ios/RNCCollapsiblePagerViewComponentView.h +9 -0
  8. package/ios/RNCCollapsiblePagerViewComponentView.mm +991 -0
  9. package/ios/RNCPagerViewComponentView.mm +4 -2
  10. package/lib/module/CollapsiblePagerView.js +167 -0
  11. package/lib/module/CollapsiblePagerView.web.js +478 -0
  12. package/lib/module/CollapsiblePagerViewNativeComponent.ts +87 -0
  13. package/lib/module/PagerView.web.js +177 -0
  14. package/lib/module/index.js +2 -1
  15. package/lib/module/index.web.js +6 -0
  16. package/lib/module/usePagerView.js +1 -1
  17. package/lib/typescript/src/CollapsiblePagerView.d.ts +58 -0
  18. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +105 -0
  19. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +51 -0
  20. package/lib/typescript/src/PagerView.web.d.ts +55 -0
  21. package/lib/typescript/src/index.d.ts +1 -0
  22. package/lib/typescript/src/index.web.d.ts +5 -0
  23. package/package.json +5 -2
  24. package/src/CollapsiblePagerView.tsx +293 -0
  25. package/src/CollapsiblePagerView.web.tsx +710 -0
  26. package/src/CollapsiblePagerViewNativeComponent.ts +87 -0
  27. package/src/PagerView.web.tsx +280 -0
  28. package/src/index.tsx +1 -0
  29. package/src/index.web.tsx +12 -0
@@ -0,0 +1,991 @@
1
+ #import "RNCCollapsiblePagerViewComponentView.h"
2
+
3
+ #import <react/renderer/components/pagerview/ComponentDescriptors.h>
4
+ #import <react/renderer/components/pagerview/EventEmitters.h>
5
+ #import <react/renderer/components/pagerview/Props.h>
6
+ #import <react/renderer/components/pagerview/RCTComponentViewHelpers.h>
7
+
8
+ #import "React/RCTConversions.h"
9
+
10
+ using namespace facebook::react;
11
+
12
+ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
13
+
14
+ @interface RNCCollapsiblePagerViewComponentView () <
15
+ RCTRNCCollapsiblePagerViewViewProtocol,
16
+ UIPageViewControllerDataSource,
17
+ UIPageViewControllerDelegate,
18
+ UIScrollViewDelegate,
19
+ UIGestureRecognizerDelegate
20
+ >
21
+ - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
22
+ @end
23
+
24
+ @implementation RNCCollapsiblePagerViewComponentView {
25
+ UIView *_containerView;
26
+ UIPageViewController *_pageViewController;
27
+ UIScrollView *_pagerScrollView;
28
+ NSMutableArray<UIView<RCTComponentViewProtocol> *> *_logicalChildren;
29
+ NSMutableArray<UIViewController *> *_pageControllers;
30
+ UIView *_headerView;
31
+ UIView *_stickyHeaderView;
32
+ NSInteger _currentIndex;
33
+ NSInteger _destinationIndex;
34
+ NSInteger _pendingInitialPage;
35
+ NSInteger _headerHeight;
36
+ NSInteger _stickyHeaderHeight;
37
+ CGFloat _headerOffset;
38
+ BOOL _scrollEnabled;
39
+ // OneKey patch: Coordinate this inner pager with an outer horizontal pager.
40
+ BOOL _nestedScrollEnabled;
41
+ UIPanGestureRecognizer *_blockerGesture;
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;
48
+ BOOL _hasAppliedInitialPage;
49
+ BOOL _needsPropsReapply;
50
+ NSString *_layoutDirection;
51
+ NSArray<NSString *> *_pageKeys;
52
+ NSString *_retainedPages;
53
+ NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
54
+ NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
55
+ NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
56
+ NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
57
+ NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
58
+ NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
59
+ __weak UIScrollView *_observedScrollView;
60
+ BOOL _observingContentOffset;
61
+ BOOL _isBeingRecycled;
62
+ NSUInteger _generation;
63
+ }
64
+
65
+ + (void)load
66
+ {
67
+ [super load];
68
+ }
69
+
70
+ - (instancetype)initWithFrame:(CGRect)frame
71
+ {
72
+ if (self = [super initWithFrame:frame]) {
73
+ static const auto defaultProps = std::make_shared<const RNCCollapsiblePagerViewProps>();
74
+ _props = defaultProps;
75
+ _logicalChildren = [NSMutableArray new];
76
+ _pageControllers = [NSMutableArray new];
77
+ _pageOffsets = [NSMutableDictionary new];
78
+ _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
79
+ _appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
80
+ _originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
81
+ _originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
82
+ _originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
83
+ _pageKeys = @[];
84
+ _retainedPages = @"[]";
85
+ _currentIndex = 0;
86
+ _destinationIndex = 0;
87
+ _pendingInitialPage = 0;
88
+ _scrollEnabled = YES;
89
+ _nestedScrollEnabled = NO;
90
+ _pendingGoToIndex = -1;
91
+ _hasAppliedInitialPage = NO;
92
+ _needsPropsReapply = YES;
93
+ _layoutDirection = @"ltr";
94
+
95
+ _containerView = [UIView new];
96
+ _containerView.clipsToBounds = YES;
97
+ self.contentView = _containerView;
98
+
99
+ [self initializePageViewController];
100
+ }
101
+ return self;
102
+ }
103
+
104
+ - (void)willMoveToSuperview:(UIView *)newSuperview
105
+ {
106
+ [super willMoveToSuperview:newSuperview];
107
+ if (newSuperview != nil && _pageViewController == nil) {
108
+ self.contentView = _containerView;
109
+ [self initializePageViewController];
110
+ }
111
+ }
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
+
124
+ - (void)initializePageViewController
125
+ {
126
+ _pageViewController = [[UIPageViewController alloc]
127
+ initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
128
+ navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
129
+ options:nil];
130
+ _pageViewController.dataSource = self;
131
+ _pageViewController.delegate = self;
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];
135
+
136
+ for (UIView *subview in _pageViewController.view.subviews) {
137
+ if ([subview isKindOfClass:UIScrollView.class]) {
138
+ _pagerScrollView = (UIScrollView *)subview;
139
+ _pagerScrollView.delegate = self;
140
+ _pagerScrollView.delaysContentTouches = NO;
141
+ _pagerScrollView.scrollEnabled = _scrollEnabled;
142
+ break;
143
+ }
144
+ }
145
+ [self applyNestedScrollBlocker];
146
+ }
147
+
148
+ #pragma mark - React mounting
149
+
150
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
151
+ index:(NSInteger)index
152
+ {
153
+ if (_isBeingRecycled) return;
154
+ NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
155
+ [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
156
+ [self rebuildSlots];
157
+ }
158
+
159
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
160
+ index:(NSInteger)index
161
+ {
162
+ if (_isBeingRecycled) return;
163
+ NSUInteger existingIndex = [_logicalChildren indexOfObjectIdenticalTo:childComponentView];
164
+ if (existingIndex == NSNotFound) return;
165
+ [childComponentView removeFromSuperview];
166
+ [_logicalChildren removeObjectAtIndex:existingIndex];
167
+ [self rebuildSlots];
168
+ }
169
+
170
+ - (void)rebuildSlots
171
+ {
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;
177
+ [_headerView removeFromSuperview];
178
+ [_stickyHeaderView removeFromSuperview];
179
+ _headerView = nil;
180
+ _stickyHeaderView = nil;
181
+ [_pageControllers removeAllObjects];
182
+
183
+ if (_logicalChildren.count > 0) {
184
+ _headerView = _logicalChildren[0];
185
+ [_containerView addSubview:_headerView];
186
+ }
187
+ if (_logicalChildren.count > 1) {
188
+ _stickyHeaderView = _logicalChildren[1];
189
+ [_containerView addSubview:_stickyHeaderView];
190
+ }
191
+ for (NSInteger index = 2; index < _logicalChildren.count; index++) {
192
+ UIView *page = _logicalChildren[index];
193
+ UIViewController *controller = [UIViewController new];
194
+ [controller.view addSubview:page];
195
+ [_pageControllers addObject:controller];
196
+ }
197
+
198
+ if (_pageControllers.count > 0) {
199
+ if (!_hasAppliedInitialPage &&
200
+ _pendingInitialPage >= 0 &&
201
+ _pendingInitialPage < _pageControllers.count) {
202
+ _currentIndex = _pendingInitialPage;
203
+ // A recycled host may receive slots before its page controller exists.
204
+ _hasAppliedInitialPage = _pageViewController != nil;
205
+ } else {
206
+ _currentIndex = MIN(MAX(_currentIndex, 0), _pageControllers.count - 1);
207
+ }
208
+ UIViewController *controller = _pageControllers[_currentIndex];
209
+ [_pageViewController setViewControllers:@[controller]
210
+ direction:UIPageViewControllerNavigationDirectionForward
211
+ animated:NO
212
+ completion:nil];
213
+ }
214
+ if (_headerView != nil) [_containerView bringSubviewToFront:_headerView];
215
+ if (_stickyHeaderView != nil) [_containerView bringSubviewToFront:_stickyHeaderView];
216
+ [self setNeedsLayout];
217
+ dispatch_async(dispatch_get_main_queue(), ^{
218
+ if (!self->_isBeingRecycled) {
219
+ [self prepareAdjacentPageInsets];
220
+ [self attachScrollObserverForCurrentPage];
221
+ }
222
+ });
223
+ }
224
+
225
+ - (void)layoutSubviews
226
+ {
227
+ [super layoutSubviews];
228
+ _containerView.frame = self.bounds;
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
+ }
243
+ // UIKit frame assignment is undefined while a view has a non-identity transform.
244
+ // Lay out the original slots, then restore the shared collapse translation.
245
+ _headerView.transform = CGAffineTransformIdentity;
246
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
247
+ _headerView.frame = CGRectMake(0, 0, self.bounds.size.width, _headerHeight);
248
+ _stickyHeaderView.frame = CGRectMake(
249
+ 0,
250
+ _headerHeight,
251
+ self.bounds.size.width,
252
+ _stickyHeaderHeight
253
+ );
254
+ // Settle UIKit page containers before sizing their autoresizing child views.
255
+ [_pageViewController.view layoutIfNeeded];
256
+ for (UIViewController *controller in _pageControllers) {
257
+ controller.view.frame = _pageViewController.view.bounds;
258
+ controller.view.subviews.firstObject.frame = controller.view.bounds;
259
+ }
260
+ [self applyHeaderOffset];
261
+ dispatch_async(dispatch_get_main_queue(), ^{
262
+ if (!self->_isBeingRecycled) {
263
+ [self prepareAdjacentPageInsets];
264
+ [self attachScrollObserverForCurrentPage];
265
+ }
266
+ });
267
+ }
268
+
269
+ - (void)prepareForRecycle
270
+ {
271
+ _isBeingRecycled = YES;
272
+ _generation++;
273
+ [self detachScrollObserver];
274
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
275
+ NSValue *value = [_originalInsets objectForKey:scrollView];
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
+ }
284
+ NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
285
+ if (indicatorValue != nil) {
286
+ scrollView.verticalScrollIndicatorInsets = indicatorValue.UIEdgeInsetsValue;
287
+ }
288
+ NSNumber *alwaysBounce = [_originalAlwaysBounceVertical objectForKey:scrollView];
289
+ if (alwaysBounce != nil) scrollView.alwaysBounceVertical = alwaysBounce.boolValue;
290
+ NSNumber *adjustment = [_originalInsetAdjustmentBehavior objectForKey:scrollView];
291
+ if (adjustment != nil) {
292
+ scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)adjustment.integerValue;
293
+ }
294
+ }
295
+ [_originalInsets removeAllObjects];
296
+ [_appliedTopInsets removeAllObjects];
297
+ [_originalIndicatorInsets removeAllObjects];
298
+ [_originalAlwaysBounceVertical removeAllObjects];
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;
304
+ [_headerView removeFromSuperview];
305
+ [_stickyHeaderView removeFromSuperview];
306
+ [_logicalChildren removeAllObjects];
307
+ [_pageControllers removeAllObjects];
308
+ _headerView = nil;
309
+ _stickyHeaderView = nil;
310
+ _pageViewController.dataSource = nil;
311
+ _pageViewController.delegate = nil;
312
+ _pagerScrollView.delegate = nil;
313
+ if (_blockerGesture != nil) {
314
+ [self removeGestureRecognizer:_blockerGesture];
315
+ _blockerGesture = nil;
316
+ }
317
+ [_pageViewController.view removeFromSuperview];
318
+ _pageViewController = nil;
319
+ _pagerScrollView = nil;
320
+ [super prepareForRecycle];
321
+ [_pageOffsets removeAllObjects];
322
+ _pageKeys = @[];
323
+ _retainedPages = @"[]";
324
+ _currentIndex = 0;
325
+ _destinationIndex = 0;
326
+ _pendingInitialPage = 0;
327
+ _headerHeight = 0;
328
+ _stickyHeaderHeight = 0;
329
+ _headerOffset = 0;
330
+ _scrollEnabled = YES;
331
+ _nestedScrollEnabled = NO;
332
+ _transitioning = NO;
333
+ _isPagerDragging = NO;
334
+ _hasReceivedPageCommand = NO;
335
+ _pendingGoToIndex = -1;
336
+ _hasAppliedInitialPage = NO;
337
+ // OneKey patch: Fabric retains old props when recycling this view.
338
+ _needsPropsReapply = YES;
339
+ _layoutDirection = @"ltr";
340
+ _isBeingRecycled = NO;
341
+ }
342
+
343
+ #pragma mark - Props
344
+
345
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
346
+ {
347
+ const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
348
+ const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
349
+
350
+ if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
351
+ _pendingInitialPage = MAX(0, newViewProps.initialPage);
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];
356
+ }
357
+ if (_needsPropsReapply || oldViewProps.scrollEnabled != newViewProps.scrollEnabled) {
358
+ _scrollEnabled = newViewProps.scrollEnabled;
359
+ _pagerScrollView.scrollEnabled = _scrollEnabled;
360
+ }
361
+ if (_needsPropsReapply || oldViewProps.nestedScrollEnabled != newViewProps.nestedScrollEnabled) {
362
+ _nestedScrollEnabled = newViewProps.nestedScrollEnabled;
363
+ [self applyNestedScrollBlocker];
364
+ }
365
+ if (_needsPropsReapply || oldViewProps.layoutDirection != newViewProps.layoutDirection) {
366
+ _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
367
+ }
368
+ BOOL insetsChanged = NO;
369
+ if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
370
+ _headerHeight = MAX(0, newViewProps.headerHeight);
371
+ _headerOffset = MIN(_headerOffset, _headerHeight);
372
+ insetsChanged = YES;
373
+ }
374
+ if (_needsPropsReapply || oldViewProps.stickyHeaderHeight != newViewProps.stickyHeaderHeight) {
375
+ _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
376
+ insetsChanged = YES;
377
+ }
378
+ if (_needsPropsReapply || oldViewProps.pageKeys != newViewProps.pageKeys) {
379
+ NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
380
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
381
+ id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
382
+ _pageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
383
+ }
384
+ if (_needsPropsReapply || oldViewProps.retainedPages != newViewProps.retainedPages) {
385
+ _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
386
+ [self setNeedsLayout];
387
+ }
388
+
389
+ _needsPropsReapply = NO;
390
+ [super updateProps:props oldProps:oldProps];
391
+ if (insetsChanged) {
392
+ [self reapplyInsetsToObservedScrollView];
393
+ [self setNeedsLayout];
394
+ }
395
+ }
396
+
397
+ #pragma mark - Collapsible scroll coordination
398
+
399
+ - (NSString *)currentPageKey
400
+ {
401
+ return [self pageKeyForIndex:_currentIndex];
402
+ }
403
+
404
+ - (NSString *)pageKeyForIndex:(NSInteger)index
405
+ {
406
+ if (index >= 0 && index < _pageKeys.count) return _pageKeys[index];
407
+ return [NSString stringWithFormat:@"page-%ld", (long)index];
408
+ }
409
+
410
+ - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view
411
+ {
412
+ if ([view isKindOfClass:UICollectionView.class]) {
413
+ UICollectionView *collectionView = (UICollectionView *)view;
414
+ UICollectionViewLayout *layout = collectionView.collectionViewLayout;
415
+ if (![layout isKindOfClass:UICollectionViewFlowLayout.class] ||
416
+ ((UICollectionViewFlowLayout *)layout).scrollDirection == UICollectionViewScrollDirectionVertical) {
417
+ return collectionView;
418
+ }
419
+ }
420
+ // Empty pages may use an RN ScrollView instead of a native row list.
421
+ if ([view isKindOfClass:UIScrollView.class] &&
422
+ ![view isKindOfClass:UICollectionView.class]) {
423
+ UIScrollView *scrollView = (UIScrollView *)view;
424
+ if (!scrollView.alwaysBounceHorizontal &&
425
+ scrollView.contentSize.width <= scrollView.bounds.size.width + 1) {
426
+ return scrollView;
427
+ }
428
+ }
429
+ for (UIView *subview in view.subviews) {
430
+ UIScrollView *candidate = [self findVerticalScrollViewInView:subview];
431
+ if (candidate != nil) return candidate;
432
+ }
433
+ return nil;
434
+ }
435
+
436
+ - (void)attachScrollObserverForCurrentPage
437
+ {
438
+ [self restoreDetachedScrollInsets];
439
+ if (_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
440
+ UIScrollView *candidate = [self findVerticalScrollViewInView:_pageControllers[_currentIndex].view];
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;
447
+
448
+ BOOL replacingCurrentScrollView = _observingContentOffset;
449
+ [self detachScrollObserver];
450
+ if (replacingCurrentScrollView) {
451
+ // A list replaced by an empty RN view keeps collapse, not the old row offset.
452
+ _pageOffsets[self.currentPageKey] = @(_headerOffset);
453
+ }
454
+ _observedScrollView = candidate;
455
+ [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
456
+ [candidate addObserver:self
457
+ forKeyPath:@"contentOffset"
458
+ options:NSKeyValueObservingOptionNew
459
+ context:RNCCollapsiblePagerContentOffsetContext];
460
+ [candidate addObserver:self
461
+ forKeyPath:@"contentSize"
462
+ options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
463
+ context:RNCCollapsiblePagerContentOffsetContext];
464
+ _observingContentOffset = YES;
465
+ }
466
+
467
+ - (void)detachScrollObserver
468
+ {
469
+ UIScrollView *scrollView = _observedScrollView;
470
+ if (scrollView != nil) {
471
+ _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
472
+ if (_observingContentOffset) {
473
+ [scrollView removeObserver:self
474
+ forKeyPath:@"contentOffset"
475
+ context:RNCCollapsiblePagerContentOffsetContext];
476
+ [scrollView removeObserver:self
477
+ forKeyPath:@"contentSize"
478
+ context:RNCCollapsiblePagerContentOffsetContext];
479
+ }
480
+ }
481
+ _observingContentOffset = NO;
482
+ _observedScrollView = nil;
483
+ }
484
+
485
+ - (void)restoreDetachedScrollInsets
486
+ {
487
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
488
+ BOOL belongsToPage = NO;
489
+ for (UIViewController *controller in _pageControllers) {
490
+ if ([scrollView isDescendantOfView:controller.view]) {
491
+ belongsToPage = YES;
492
+ break;
493
+ }
494
+ }
495
+ if (belongsToPage) continue;
496
+ if (scrollView == _observedScrollView) {
497
+ [self detachScrollObserver];
498
+ _pageOffsets[self.currentPageKey] = @(_headerOffset);
499
+ }
500
+ // Fabric can reuse an empty RN ScrollView in another screen. Release all
501
+ // pager-owned insets before that view is used as a search or dialog list.
502
+ CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
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
+ }
508
+ scrollView.contentInset = original;
509
+ scrollView.verticalScrollIndicatorInsets =
510
+ [_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
511
+ scrollView.alwaysBounceVertical =
512
+ [_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
513
+ scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
514
+ [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
515
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
516
+ animated:NO];
517
+ [_originalInsets removeObjectForKey:scrollView];
518
+ [_appliedTopInsets removeObjectForKey:scrollView];
519
+ [_originalIndicatorInsets removeObjectForKey:scrollView];
520
+ [_originalAlwaysBounceVertical removeObjectForKey:scrollView];
521
+ [_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
522
+ }
523
+ }
524
+
525
+ - (void)applyInsetsToScrollView:(UIScrollView *)scrollView
526
+ pageIndex:(NSInteger)pageIndex
527
+ restore:(BOOL)restore
528
+ {
529
+ NSValue *storedOriginal = [_originalInsets objectForKey:scrollView];
530
+ UIEdgeInsets original = storedOriginal == nil
531
+ ? scrollView.contentInset
532
+ : storedOriginal.UIEdgeInsetsValue;
533
+ if (storedOriginal == nil) {
534
+ [_originalInsets setObject:[NSValue valueWithUIEdgeInsets:original] forKey:scrollView];
535
+ [_originalIndicatorInsets
536
+ setObject:[NSValue valueWithUIEdgeInsets:scrollView.verticalScrollIndicatorInsets]
537
+ forKey:scrollView];
538
+ [_originalAlwaysBounceVertical setObject:@(scrollView.alwaysBounceVertical) forKey:scrollView];
539
+ [_originalInsetAdjustmentBehavior setObject:@(scrollView.contentInsetAdjustmentBehavior) forKey:scrollView];
540
+ }
541
+
542
+ // This host already includes the fixed header and safe area in its frame.
543
+ // UIKit automatic adjustment would add them a second time to short pages.
544
+ scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
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;
551
+ UIEdgeInsets next = original;
552
+ next.top += _headerHeight + _stickyHeaderHeight;
553
+ CGFloat pagerTopInset = next.top;
554
+ next.top += refreshInset;
555
+ [_appliedTopInsets setObject:@(pagerTopInset) forKey:scrollView];
556
+ // Short pages still need enough range to collapse the header; long pages
557
+ // must end at the last row rather than leave a header-sized blank footer.
558
+ next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
559
+ - original.top - original.bottom - scrollView.contentSize.height);
560
+ if (UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, next) && !restore) {
561
+ // UICollectionView may report contentSize during a bounce without changing
562
+ // the required insets. Re-setting contentOffset here interrupts that bounce.
563
+ if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
564
+ return;
565
+ }
566
+ scrollView.contentInset = next;
567
+ UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
568
+ indicatorInsets.top = pagerTopInset;
569
+ scrollView.verticalScrollIndicatorInsets = indicatorInsets;
570
+ scrollView.alwaysBounceVertical = YES;
571
+
572
+ CGFloat targetOffset = previousLogicalOffset - next.top;
573
+ if (restore) {
574
+ NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
575
+ // The sticky slot can change height between pages. Cache offsets relative
576
+ // to content start so a round trip does not add that height difference.
577
+ targetOffset = (saved == nil ? _headerOffset : saved.doubleValue) - next.top;
578
+ targetOffset = MAX(targetOffset, -next.top + _headerOffset);
579
+ }
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
+ }
584
+ if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
585
+ }
586
+
587
+ - (void)reapplyInsetsToObservedScrollView
588
+ {
589
+ [self restoreDetachedScrollInsets];
590
+ UIScrollView *scrollView = _observedScrollView;
591
+ if (scrollView != nil) {
592
+ [self applyInsetsToScrollView:scrollView pageIndex:_currentIndex restore:NO];
593
+ }
594
+ [self prepareAdjacentPageInsets];
595
+ }
596
+
597
+ - (void)prepareAdjacentPageInsets
598
+ {
599
+ [self restoreDetachedScrollInsets];
600
+ NSInteger first = MAX(0, _currentIndex - 1);
601
+ NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
602
+ if (last < first) return;
603
+ for (NSInteger index = first; index <= last; index++) {
604
+ UIScrollView *scrollView = [self findVerticalScrollViewInView:_pageControllers[index].view];
605
+ if (scrollView != nil && scrollView != _observedScrollView) {
606
+ [self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
607
+ }
608
+ }
609
+ }
610
+
611
+ - (void)observeValueForKeyPath:(NSString *)keyPath
612
+ ofObject:(id)object
613
+ change:(NSDictionary<NSKeyValueChangeKey,id> *)change
614
+ context:(void *)context
615
+ {
616
+ if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
617
+ if ([keyPath isEqualToString:@"contentSize"]) {
618
+ NSValue *oldSize = change[NSKeyValueChangeOldKey];
619
+ NSValue *newSize = change[NSKeyValueChangeNewKey];
620
+ if (![oldSize isEqual:newSize]) {
621
+ [self applyInsetsToScrollView:_observedScrollView pageIndex:_currentIndex restore:NO];
622
+ }
623
+ return;
624
+ }
625
+ UIScrollView *scrollView = (UIScrollView *)object;
626
+ _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
627
+ [self updateHeaderForScrollView:scrollView];
628
+ return;
629
+ }
630
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
631
+ }
632
+
633
+ - (void)updateHeaderForScrollView:(UIScrollView *)scrollView
634
+ {
635
+ CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
636
+ _headerOffset = MIN(MAX(logicalOffset, 0), _headerHeight);
637
+ [self applyHeaderOffset];
638
+ }
639
+
640
+ - (void)applyHeaderOffset
641
+ {
642
+ CGAffineTransform transform = CGAffineTransformMakeTranslation(0, -_headerOffset);
643
+ _headerView.transform = transform;
644
+ _stickyHeaderView.transform = transform;
645
+ }
646
+
647
+ #pragma mark - Pager navigation
648
+
649
+ - (BOOL)isLtrLayout
650
+ {
651
+ return [_layoutDirection isEqualToString:@"ltr"];
652
+ }
653
+
654
+ - (void)goTo:(NSInteger)index animated:(BOOL)animated
655
+ {
656
+ if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
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
+ }
675
+
676
+ [self detachScrollObserver];
677
+ _destinationIndex = index;
678
+ BOOL forward = (index > _currentIndex && self.isLtrLayout) ||
679
+ (index < _currentIndex && !self.isLtrLayout);
680
+ UIPageViewControllerNavigationDirection direction = forward
681
+ ? UIPageViewControllerNavigationDirectionForward
682
+ : UIPageViewControllerNavigationDirectionReverse;
683
+ _transitioning = YES;
684
+ _isPagerDragging = NO;
685
+ NSUInteger capturedTransition = ++_transitionId;
686
+ NSUInteger capturedGeneration = _generation;
687
+ __weak __typeof__(self) weakSelf = self;
688
+ [_pageViewController setViewControllers:@[_pageControllers[index]]
689
+ direction:direction
690
+ animated:animated && index != _currentIndex
691
+ completion:^(BOOL finished) {
692
+ __strong __typeof__(weakSelf) self = weakSelf;
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
+ });
709
+ }];
710
+ }
711
+
712
+ - (UIViewController *)adjacentController:(UIViewController *)controller delta:(NSInteger)delta
713
+ {
714
+ NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
715
+ if (index == NSNotFound) return nil;
716
+ NSInteger target = index + delta;
717
+ if (target < 0 || target >= _pageControllers.count) return nil;
718
+ UIScrollView *scrollView = [self findVerticalScrollViewInView:_pageControllers[target].view];
719
+ if (scrollView != nil) {
720
+ [self applyInsetsToScrollView:scrollView pageIndex:target restore:YES];
721
+ }
722
+ return _pageControllers[target];
723
+ }
724
+
725
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
726
+ viewControllerAfterViewController:(UIViewController *)viewController
727
+ {
728
+ return [self adjacentController:viewController delta:self.isLtrLayout ? 1 : -1];
729
+ }
730
+
731
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
732
+ viewControllerBeforeViewController:(UIViewController *)viewController
733
+ {
734
+ return [self adjacentController:viewController delta:self.isLtrLayout ? -1 : 1];
735
+ }
736
+
737
+ - (void)drainPendingGoTo
738
+ {
739
+ NSInteger pending = _pendingGoToIndex;
740
+ _pendingGoToIndex = -1;
741
+ if (pending >= 0 && pending != _currentIndex) {
742
+ [self goTo:pending animated:YES];
743
+ }
744
+ }
745
+
746
+ #pragma mark - Pager UIScrollViewDelegate
747
+
748
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
749
+ {
750
+ // A user drag owns the result even if UIKit later completes the old animation.
751
+ ++_transitionId;
752
+ _isPagerDragging = YES;
753
+ _transitioning = YES;
754
+ const auto emitter = [self eventEmitter];
755
+ if (emitter) {
756
+ emitter->onPageScrollStateChanged({
757
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging
758
+ });
759
+ }
760
+ }
761
+
762
+ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
763
+ withVelocity:(CGPoint)velocity
764
+ targetContentOffset:(inout CGPoint *)targetContentOffset
765
+ {
766
+ const auto emitter = [self eventEmitter];
767
+ if (emitter) {
768
+ emitter->onPageScrollStateChanged({
769
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Settling
770
+ });
771
+ }
772
+ }
773
+
774
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
775
+ willDecelerate:(BOOL)decelerate
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
+ }
825
+ const auto emitter = [self eventEmitter];
826
+ if (emitter) {
827
+ emitter->onPageScrollStateChanged({
828
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Idle
829
+ });
830
+ }
831
+ [self emitDiagnostics:@"pager-idle"];
832
+ [self drainPendingGoTo];
833
+ }
834
+
835
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
836
+ {
837
+ [self finishPagerScrollEmittingSelection:YES];
838
+ }
839
+
840
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
841
+ {
842
+ CGFloat width = scrollView.bounds.size.width;
843
+ if (width <= 0) return;
844
+ CGFloat rawOffset = (scrollView.contentOffset.x - width) / width;
845
+ BOOL backwards = self.isLtrLayout ? rawOffset < 0 : rawOffset > 0;
846
+ NSInteger position = backwards ? _currentIndex - 1 : _currentIndex;
847
+ CGFloat offset = backwards ? 1 - fabs(rawOffset) : fabs(rawOffset);
848
+ position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
849
+ const auto emitter = [self eventEmitter];
850
+ if (emitter) {
851
+ emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
852
+ }
853
+ }
854
+
855
+ #pragma mark - Events and commands
856
+
857
+ - (std::shared_ptr<const RNCCollapsiblePagerViewEventEmitter>)eventEmitter
858
+ {
859
+ if (!_eventEmitter) return nullptr;
860
+ return std::static_pointer_cast<const RNCCollapsiblePagerViewEventEmitter>(_eventEmitter);
861
+ }
862
+
863
+ - (void)emitPageSelected:(NSInteger)position
864
+ {
865
+ const auto emitter = [self eventEmitter];
866
+ if (emitter) emitter->onPageSelected({.position = (double)position});
867
+ }
868
+
869
+ - (void)emitDiagnostics:(NSString *)reason
870
+ {
871
+ const auto emitter = [self eventEmitter];
872
+ if (!emitter) return;
873
+ NSInteger attached = 0;
874
+ for (UIViewController *controller in _pageControllers) {
875
+ if (controller.view.superview != nil) attached++;
876
+ }
877
+ emitter->onCollapsibleStateChanged({
878
+ .position = (int)_currentIndex,
879
+ .headerOffset = (double)_headerOffset,
880
+ .nativePageCount = (int)_pageControllers.count,
881
+ .attachedPageCount = (int)attached,
882
+ .observedScrollableCount = (int)_originalInsets.count,
883
+ .retainedPages = std::string(_retainedPages.UTF8String ?: "[]"),
884
+ .reason = std::string(reason.UTF8String ?: "unknown")
885
+ });
886
+ }
887
+
888
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
889
+ {
890
+ RCTRNCCollapsiblePagerViewHandleCommand(self, commandName, args);
891
+ }
892
+
893
+ - (void)setPage:(NSInteger)index
894
+ {
895
+ [self goTo:index animated:YES];
896
+ }
897
+
898
+ - (void)setPageWithoutAnimation:(NSInteger)index
899
+ {
900
+ [self goTo:index animated:NO];
901
+ }
902
+
903
+ - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled
904
+ {
905
+ _scrollEnabled = scrollEnabled;
906
+ _pagerScrollView.scrollEnabled = scrollEnabled;
907
+ }
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
+
981
+ + (ComponentDescriptorProvider)componentDescriptorProvider
982
+ {
983
+ return concreteComponentDescriptorProvider<RNCCollapsiblePagerViewComponentDescriptor>();
984
+ }
985
+
986
+ @end
987
+
988
+ Class<RCTComponentViewProtocol> RNCCollapsiblePagerViewCls(void)
989
+ {
990
+ return RNCCollapsiblePagerViewComponentView.class;
991
+ }