@onekeyfe/react-native-pager-view 1.1.35
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/LICENSE +21 -0
- package/README.md +306 -0
- package/android/Android.mk +45 -0
- package/android/build.gradle +237 -0
- package/android/debug.keystore +0 -0
- package/android/gradle.properties +5 -0
- package/android/registration.cpp +18 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +148 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt +222 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewManagerImpl.kt +228 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +17 -0
- package/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt +121 -0
- package/android/src/main/java/com/reactnativepagerview/ViewPagerViewHolder.kt +21 -0
- package/android/src/main/java/com/reactnativepagerview/event/PageScrollEvent.kt +47 -0
- package/android/src/main/java/com/reactnativepagerview/event/PageScrollStateChangedEvent.kt +34 -0
- package/android/src/main/java/com/reactnativepagerview/event/PageSelectedEvent.kt +38 -0
- package/ios/PagerView.xcodeproj/project.pbxproj +274 -0
- package/ios/RCTOnPageScrollEvent.h +14 -0
- package/ios/RCTOnPageScrollEvent.m +60 -0
- package/ios/RNCPagerViewComponentView.h +11 -0
- package/ios/RNCPagerViewComponentView.mm +704 -0
- package/lib/module/PagerView.js +136 -0
- package/lib/module/PagerView.js.map +1 -0
- package/lib/module/PagerViewNativeComponent.ts +82 -0
- package/lib/module/codegen-types.d.js +2 -0
- package/lib/module/codegen-types.d.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/usePagerView.js +106 -0
- package/lib/module/usePagerView.js.map +1 -0
- package/lib/module/utils.js +27 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/PagerView.d.ts +70 -0
- package/lib/typescript/src/PagerView.d.ts.map +1 -0
- package/lib/typescript/src/PagerViewNativeComponent.d.ts +51 -0
- package/lib/typescript/src/PagerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/usePagerView.d.ts +38 -0
- package/lib/typescript/src/usePagerView.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +3 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/package.json +101 -0
- package/react-native-pager-view.podspec +20 -0
- package/src/PagerView.tsx +170 -0
- package/src/PagerViewNativeComponent.ts +82 -0
- package/src/codegen-types.d.ts +28 -0
- package/src/index.tsx +27 -0
- package/src/usePagerView.ts +148 -0
- package/src/utils.tsx +22 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import "RNCPagerViewComponentView.h"
|
|
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 "RCTFabricComponentsPlugins.h"
|
|
9
|
+
#import "React/RCTConversions.h"
|
|
10
|
+
|
|
11
|
+
#import "RCTOnPageScrollEvent.h"
|
|
12
|
+
|
|
13
|
+
using namespace facebook::react;
|
|
14
|
+
|
|
15
|
+
@interface RNCPagerViewComponentView () <RCTRNCViewPagerViewProtocol, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
|
16
|
+
|
|
17
|
+
@property(nonatomic, strong) UIPageViewController *nativePageViewController;
|
|
18
|
+
@property(nonatomic, strong) NSMutableArray<UIViewController *> *nativeChildrenViewControllers;
|
|
19
|
+
|
|
20
|
+
@end
|
|
21
|
+
|
|
22
|
+
@implementation RNCPagerViewComponentView {
|
|
23
|
+
LayoutMetrics _layoutMetrics;
|
|
24
|
+
LayoutMetrics _oldLayoutMetrics;
|
|
25
|
+
UIScrollView *scrollView;
|
|
26
|
+
BOOL transitioning;
|
|
27
|
+
NSInteger _currentIndex;
|
|
28
|
+
NSInteger _destinationIndex;
|
|
29
|
+
BOOL _overdrag;
|
|
30
|
+
NSString *_layoutDirection;
|
|
31
|
+
BOOL _scrollEnabled;
|
|
32
|
+
// _isBeingRecycled: synchronous guard, only true during prepareForRecycle execution
|
|
33
|
+
BOOL _isBeingRecycled;
|
|
34
|
+
// _generation: async guard for recycle-and-reuse, incremented in prepareForRecycle
|
|
35
|
+
NSUInteger _generation;
|
|
36
|
+
// _transitionId: async guard for superseded transitions, incremented each setPagerViewControllers call
|
|
37
|
+
NSUInteger _transitionId;
|
|
38
|
+
// _pendingGoToIndex: stores the latest blocked animated transition target, drained after current transition completes
|
|
39
|
+
NSInteger _pendingGoToIndex;
|
|
40
|
+
// _nestedScrollEnabled: when YES, this inner PagerView coordinates gestures with an outer PagerView
|
|
41
|
+
BOOL _nestedScrollEnabled;
|
|
42
|
+
// _blockerGesture: gate gesture that blocks inner scroll at page boundaries when nested
|
|
43
|
+
UIPanGestureRecognizer *_blockerGesture;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Needed because of this: https://github.com/facebook/react-native/pull/37274
|
|
47
|
+
+ (void)load
|
|
48
|
+
{
|
|
49
|
+
[super load];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)initializeNativePageViewController {
|
|
53
|
+
const auto &viewProps = *std::static_pointer_cast<const RNCViewPagerProps>(_props);
|
|
54
|
+
NSDictionary *options = @{ UIPageViewControllerOptionInterPageSpacingKey: @(viewProps.pageMargin) };
|
|
55
|
+
UIPageViewControllerNavigationOrientation orientation = UIPageViewControllerNavigationOrientationHorizontal;
|
|
56
|
+
switch (viewProps.orientation) {
|
|
57
|
+
case RNCViewPagerOrientation::Horizontal:
|
|
58
|
+
orientation = UIPageViewControllerNavigationOrientationHorizontal;
|
|
59
|
+
break;
|
|
60
|
+
case RNCViewPagerOrientation::Vertical:
|
|
61
|
+
orientation = UIPageViewControllerNavigationOrientationVertical;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
_nativePageViewController = [[UIPageViewController alloc]
|
|
65
|
+
initWithTransitionStyle: UIPageViewControllerTransitionStyleScroll
|
|
66
|
+
navigationOrientation:orientation
|
|
67
|
+
options:options];
|
|
68
|
+
_nativePageViewController.dataSource = self;
|
|
69
|
+
_nativePageViewController.delegate = self;
|
|
70
|
+
_nativePageViewController.view.frame = self.frame;
|
|
71
|
+
self.contentView = _nativePageViewController.view;
|
|
72
|
+
|
|
73
|
+
for (UIView *subview in _nativePageViewController.view.subviews) {
|
|
74
|
+
if([subview isKindOfClass:UIScrollView.class]){
|
|
75
|
+
((UIScrollView *)subview).delegate = self;
|
|
76
|
+
((UIScrollView *)subview).delaysContentTouches = NO;
|
|
77
|
+
scrollView = (UIScrollView *)subview;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[self applyScrollEnabled];
|
|
82
|
+
[self applyNestedScrollBlocker];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
86
|
+
{
|
|
87
|
+
if (self = [super initWithFrame:frame]) {
|
|
88
|
+
static const auto defaultProps = std::make_shared<const RNCViewPagerProps>();
|
|
89
|
+
_props = defaultProps;
|
|
90
|
+
_nativeChildrenViewControllers = [[NSMutableArray alloc] init];
|
|
91
|
+
_currentIndex = -1;
|
|
92
|
+
_destinationIndex = -1;
|
|
93
|
+
_layoutDirection = @"ltr";
|
|
94
|
+
_overdrag = NO;
|
|
95
|
+
_scrollEnabled = YES;
|
|
96
|
+
_isBeingRecycled = NO;
|
|
97
|
+
_pendingGoToIndex = -1;
|
|
98
|
+
_nestedScrollEnabled = NO;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return self;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)willMoveToSuperview:(UIView *)newSuperview {
|
|
105
|
+
if (newSuperview != nil) {
|
|
106
|
+
[self initializeNativePageViewController];
|
|
107
|
+
[self goTo:_currentIndex animated:NO];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
#pragma mark - React API
|
|
113
|
+
|
|
114
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
115
|
+
if (_isBeingRecycled) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
UIViewController *vc = [UIViewController new];
|
|
119
|
+
[vc.view addSubview:childComponentView];
|
|
120
|
+
[_nativeChildrenViewControllers insertObject:vc atIndex:index];
|
|
121
|
+
[self goTo:_currentIndex animated:NO];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
125
|
+
if (_isBeingRecycled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (index >= _nativeChildrenViewControllers.count) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[childComponentView removeFromSuperview];
|
|
134
|
+
[_nativeChildrenViewControllers removeObjectAtIndex:index];
|
|
135
|
+
|
|
136
|
+
NSInteger maxPage = _nativeChildrenViewControllers.count - 1;
|
|
137
|
+
|
|
138
|
+
if (_currentIndex >= maxPage) {
|
|
139
|
+
if (maxPage >= 0) {
|
|
140
|
+
[self goTo:maxPage animated:NO];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
-(void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
|
|
147
|
+
_oldLayoutMetrics = oldLayoutMetrics;
|
|
148
|
+
_layoutMetrics = layoutMetrics;
|
|
149
|
+
|
|
150
|
+
if (transitioning) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_oldLayoutMetrics];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
-(void)prepareForRecycle {
|
|
159
|
+
_isBeingRecycled = YES;
|
|
160
|
+
_generation++;
|
|
161
|
+
|
|
162
|
+
// Cancel any ongoing transitions
|
|
163
|
+
if (_nativePageViewController != nil && transitioning) {
|
|
164
|
+
transitioning = NO;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clear view controllers before recycling
|
|
168
|
+
[_nativeChildrenViewControllers removeAllObjects];
|
|
169
|
+
|
|
170
|
+
// Remove blocker gesture before recycling
|
|
171
|
+
if (_blockerGesture != nil) {
|
|
172
|
+
[self removeGestureRecognizer:_blockerGesture];
|
|
173
|
+
_blockerGesture = nil;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
[super prepareForRecycle];
|
|
177
|
+
_nativePageViewController = nil;
|
|
178
|
+
_currentIndex = -1;
|
|
179
|
+
_scrollEnabled = YES;
|
|
180
|
+
_pendingGoToIndex = -1;
|
|
181
|
+
_nestedScrollEnabled = NO;
|
|
182
|
+
_isBeingRecycled = NO;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
- (void)shouldDismissKeyboard:(RNCViewPagerKeyboardDismissMode)dismissKeyboard {
|
|
186
|
+
#if !TARGET_OS_VISION
|
|
187
|
+
UIScrollViewKeyboardDismissMode dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone;
|
|
188
|
+
switch (dismissKeyboard) {
|
|
189
|
+
case RNCViewPagerKeyboardDismissMode::None:
|
|
190
|
+
dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone;
|
|
191
|
+
break;
|
|
192
|
+
case RNCViewPagerKeyboardDismissMode::OnDrag:
|
|
193
|
+
dismissKeyboardMode = UIScrollViewKeyboardDismissModeOnDrag;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
scrollView.keyboardDismissMode = dismissKeyboardMode;
|
|
197
|
+
#endif
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
- (void)applyScrollEnabled {
|
|
201
|
+
scrollView.scrollEnabled = _scrollEnabled;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
- (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps{
|
|
206
|
+
const auto &oldScreenProps = *std::static_pointer_cast<const RNCViewPagerProps>(_props);
|
|
207
|
+
const auto &newScreenProps = *std::static_pointer_cast<const RNCViewPagerProps>(props);
|
|
208
|
+
|
|
209
|
+
// change index only once
|
|
210
|
+
if (_currentIndex == -1) {
|
|
211
|
+
_currentIndex = newScreenProps.initialPage;
|
|
212
|
+
[self shouldDismissKeyboard: newScreenProps.keyboardDismissMode];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const auto newLayoutDirectionStr = RCTNSStringFromString(toString(newScreenProps.layoutDirection));
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if (_layoutDirection != newLayoutDirectionStr) {
|
|
219
|
+
_layoutDirection = newLayoutDirectionStr;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) {
|
|
223
|
+
[self shouldDismissKeyboard: newScreenProps.keyboardDismissMode];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (oldScreenProps.scrollEnabled != newScreenProps.scrollEnabled) {
|
|
227
|
+
_scrollEnabled = newScreenProps.scrollEnabled;
|
|
228
|
+
[self applyScrollEnabled];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (newScreenProps.overdrag != _overdrag) {
|
|
232
|
+
_overdrag = newScreenProps.overdrag;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (oldScreenProps.nestedScrollEnabled != newScreenProps.nestedScrollEnabled) {
|
|
236
|
+
_nestedScrollEnabled = newScreenProps.nestedScrollEnabled;
|
|
237
|
+
[self applyNestedScrollBlocker];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
[super updateProps:props oldProps:oldProps];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
#pragma mark - Internal methods
|
|
245
|
+
|
|
246
|
+
- (void)disableSwipe {
|
|
247
|
+
self.nativePageViewController.view.userInteractionEnabled = NO;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
- (void)enableSwipe {
|
|
251
|
+
self.nativePageViewController.view.userInteractionEnabled = YES;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
- (void)goTo:(NSInteger)index animated:(BOOL)animated {
|
|
255
|
+
NSInteger numberOfPages = _nativeChildrenViewControllers.count;
|
|
256
|
+
|
|
257
|
+
// Check recycling state BEFORE disableSwipe to avoid state inconsistency
|
|
258
|
+
if (_isBeingRecycled) {
|
|
259
|
+
#ifdef DEBUG
|
|
260
|
+
NSLog(@"[PagerView] goTo blocked: view is being recycled (index=%ld)", (long)index);
|
|
261
|
+
#endif
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
[self disableSwipe];
|
|
266
|
+
|
|
267
|
+
_destinationIndex = index;
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if (numberOfPages == 0 || index < 0 || index > numberOfPages - 1) {
|
|
271
|
+
[self enableSwipe];
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
BOOL isForward = (index > _currentIndex && [self isLtrLayout]) || (index < _currentIndex && ![self isLtrLayout]);
|
|
276
|
+
UIPageViewControllerNavigationDirection direction = isForward ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
|
|
277
|
+
|
|
278
|
+
long diff = labs(index - _currentIndex);
|
|
279
|
+
|
|
280
|
+
[self setPagerViewControllers:index
|
|
281
|
+
direction:direction
|
|
282
|
+
animated:diff == 0 ? NO : animated];
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
- (void)setPagerViewControllers:(NSInteger)index
|
|
287
|
+
direction:(UIPageViewControllerNavigationDirection)direction
|
|
288
|
+
animated:(BOOL)animated{
|
|
289
|
+
if (_nativePageViewController == nil || _isBeingRecycled) {
|
|
290
|
+
#ifdef DEBUG
|
|
291
|
+
NSLog(@"[PagerView] setPagerViewControllers blocked: pageVC=%@ recycled=%d index=%ld",
|
|
292
|
+
_nativePageViewController ? @"EXISTS" : @"NIL", _isBeingRecycled, (long)index);
|
|
293
|
+
#endif
|
|
294
|
+
[self enableSwipe];
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Validate index bounds
|
|
299
|
+
if (index < 0 || index >= _nativeChildrenViewControllers.count) {
|
|
300
|
+
#ifdef DEBUG
|
|
301
|
+
NSLog(@"[PagerView] setPagerViewControllers blocked: index %ld out of bounds (count=%lu)",
|
|
302
|
+
(long)index, (unsigned long)_nativeChildrenViewControllers.count);
|
|
303
|
+
#endif
|
|
304
|
+
[self enableSwipe];
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Validate view controller and its view
|
|
309
|
+
UIViewController *targetVC = [_nativeChildrenViewControllers objectAtIndex:index];
|
|
310
|
+
if (targetVC == nil || targetVC.view == nil) {
|
|
311
|
+
#ifdef DEBUG
|
|
312
|
+
NSLog(@"[PagerView] setPagerViewControllers blocked: targetVC=%@ view=%@ index=%ld",
|
|
313
|
+
targetVC ? @"EXISTS" : @"NIL", targetVC.view ? @"EXISTS" : @"NIL", (long)index);
|
|
314
|
+
#endif
|
|
315
|
+
[self enableSwipe];
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Note: Do NOT check superview/window == nil here!
|
|
320
|
+
// Target page views are expected to have nil superview/window before being added to pageViewController
|
|
321
|
+
// Checking for nil superview/window would block all legitimate page transitions
|
|
322
|
+
|
|
323
|
+
// Prevent calling setViewControllers during an ongoing animated transition.
|
|
324
|
+
// UIPageViewController's internal _UIQueuingScrollView does not support
|
|
325
|
+
// concurrent enqueued transitions and will crash with
|
|
326
|
+
// "Duplicate states in queue" (NSInternalInconsistencyException).
|
|
327
|
+
// Note: Non-animated transitions (animated=NO) are synchronous and bypass
|
|
328
|
+
// the internal _UIQueuingScrollView queue, so they are safe during an ongoing transition.
|
|
329
|
+
if (transitioning && animated) {
|
|
330
|
+
#ifdef DEBUG
|
|
331
|
+
NSLog(@"[PagerView] setPagerViewControllers deferred: already transitioning (index=%ld)", (long)index);
|
|
332
|
+
#endif
|
|
333
|
+
_pendingGoToIndex = index;
|
|
334
|
+
[self enableSwipe];
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clear any pending transition since we are now starting a fresh one
|
|
339
|
+
_pendingGoToIndex = -1;
|
|
340
|
+
|
|
341
|
+
transitioning = YES;
|
|
342
|
+
|
|
343
|
+
__weak RNCPagerViewComponentView *weakSelf = self;
|
|
344
|
+
NSUInteger capturedGeneration = _generation;
|
|
345
|
+
_transitionId++;
|
|
346
|
+
NSUInteger capturedTransitionId = _transitionId;
|
|
347
|
+
[_nativePageViewController setViewControllers:@[targetVC]
|
|
348
|
+
direction:direction
|
|
349
|
+
animated:animated
|
|
350
|
+
completion:^(BOOL finished) {
|
|
351
|
+
__strong RNCPagerViewComponentView *strongSelf = weakSelf;
|
|
352
|
+
if (strongSelf == nil) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// View was recycled and possibly reused since this animation started
|
|
357
|
+
if (strongSelf->_isBeingRecycled || strongSelf->_generation != capturedGeneration) {
|
|
358
|
+
// Do NOT reset transitioning here — a new lifecycle owns that state
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// A newer transition was started, this completion is stale
|
|
363
|
+
if (strongSelf->_transitionId != capturedTransitionId) {
|
|
364
|
+
// Do NOT reset transitioning — the newer transition owns that state
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Only reset transitioning when this completion corresponds to the current transition
|
|
369
|
+
strongSelf->transitioning = NO;
|
|
370
|
+
|
|
371
|
+
[strongSelf enableSwipe];
|
|
372
|
+
strongSelf->_currentIndex = index;
|
|
373
|
+
if (strongSelf->_eventEmitter != nullptr ) {
|
|
374
|
+
const auto eventEmitter = [strongSelf pagerEventEmitter];
|
|
375
|
+
if (eventEmitter) {
|
|
376
|
+
int position = (int) index;
|
|
377
|
+
eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast<double>(position)});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
[strongSelf updateLayoutMetrics:strongSelf->_layoutMetrics oldLayoutMetrics:strongSelf->_oldLayoutMetrics];
|
|
381
|
+
|
|
382
|
+
// Drain pending transition that was blocked while this animation was in-flight
|
|
383
|
+
NSInteger pending = strongSelf->_pendingGoToIndex;
|
|
384
|
+
if (pending >= 0 && pending != index) {
|
|
385
|
+
strongSelf->_pendingGoToIndex = -1;
|
|
386
|
+
[strongSelf goTo:pending animated:YES];
|
|
387
|
+
}
|
|
388
|
+
}];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
- (UIViewController *)nextControllerForController:(UIViewController *)controller
|
|
393
|
+
inDirection:(UIPageViewControllerNavigationDirection)direction {
|
|
394
|
+
NSUInteger numberOfPages = _nativeChildrenViewControllers.count;
|
|
395
|
+
|
|
396
|
+
if (_isBeingRecycled || numberOfPages == 0) {
|
|
397
|
+
return nil;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
NSInteger index = [_nativeChildrenViewControllers indexOfObject:controller];
|
|
401
|
+
|
|
402
|
+
if (index == NSNotFound) {
|
|
403
|
+
return nil;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
direction == UIPageViewControllerNavigationDirectionForward ? index++ : index--;
|
|
407
|
+
|
|
408
|
+
if (index < 0 || (index > (numberOfPages - 1))) {
|
|
409
|
+
return nil;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [_nativeChildrenViewControllers objectAtIndex:index];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
- (UIViewController *)currentlyDisplayed {
|
|
416
|
+
return _nativePageViewController.viewControllers.firstObject;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#pragma mark - UIScrollViewDelegate
|
|
420
|
+
|
|
421
|
+
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
|
422
|
+
if (_isBeingRecycled) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const auto eventEmitter = [self pagerEventEmitter];
|
|
426
|
+
if (!eventEmitter) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
|
|
433
|
+
if (_isBeingRecycled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const auto eventEmitter = [self pagerEventEmitter];
|
|
437
|
+
if (!eventEmitter) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Settling });
|
|
441
|
+
|
|
442
|
+
if (!_overdrag) {
|
|
443
|
+
NSInteger maxIndex = _nativeChildrenViewControllers.count - 1;
|
|
444
|
+
BOOL isFirstPage = [self isLtrLayout] ? _currentIndex == 0 : _currentIndex == maxIndex;
|
|
445
|
+
BOOL isLastPage = [self isLtrLayout] ? _currentIndex == maxIndex : _currentIndex == 0;
|
|
446
|
+
CGFloat contentOffset = [self isHorizontal] ? scrollView.contentOffset.x : scrollView.contentOffset.y;
|
|
447
|
+
CGFloat topBound = [self isHorizontal] ? scrollView.bounds.size.width : scrollView.bounds.size.height;
|
|
448
|
+
|
|
449
|
+
if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
|
|
450
|
+
CGPoint croppedOffset = [self isHorizontal] ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
|
|
451
|
+
*targetContentOffset = croppedOffset;
|
|
452
|
+
|
|
453
|
+
eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
|
459
|
+
if (_isBeingRecycled) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const auto eventEmitter = [self pagerEventEmitter];
|
|
463
|
+
if (!eventEmitter) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
471
|
+
if (_isBeingRecycled) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
BOOL isHorizontal = [self isHorizontal];
|
|
475
|
+
CGFloat contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y;
|
|
476
|
+
CGFloat frameSize = isHorizontal ? scrollView.frame.size.width : scrollView.frame.size.height;
|
|
477
|
+
|
|
478
|
+
if (frameSize == 0) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
float offset = (contentOffset - frameSize) / frameSize;
|
|
483
|
+
float absoluteOffset = fabs(offset);
|
|
484
|
+
NSInteger position = _currentIndex;
|
|
485
|
+
|
|
486
|
+
BOOL isHorizontalRtl = [self isHorizontalRtlLayout];
|
|
487
|
+
BOOL isAnimatingBackwards = isHorizontalRtl ? offset > 0.05f : offset < 0;
|
|
488
|
+
BOOL isBeingMovedByNestedScrollView = !scrollView.isDragging && !scrollView.isTracking;
|
|
489
|
+
if (scrollView.isDragging || isBeingMovedByNestedScrollView) {
|
|
490
|
+
_destinationIndex = isAnimatingBackwards ? _currentIndex - 1 : _currentIndex + 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (isAnimatingBackwards) {
|
|
494
|
+
position = _destinationIndex;
|
|
495
|
+
absoluteOffset = fmax(0, 1 - absoluteOffset);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!_overdrag) {
|
|
499
|
+
NSInteger maxIndex = _nativeChildrenViewControllers.count - 1;
|
|
500
|
+
NSInteger firstPageIndex = isHorizontalRtl ? maxIndex : 0;
|
|
501
|
+
NSInteger lastPageIndex = isHorizontalRtl ? 0 : maxIndex;
|
|
502
|
+
BOOL isFirstPage = _currentIndex == firstPageIndex;
|
|
503
|
+
BOOL isLastPage = _currentIndex == lastPageIndex;
|
|
504
|
+
CGFloat topBound = isHorizontal ? scrollView.bounds.size.width : scrollView.bounds.size.height;
|
|
505
|
+
|
|
506
|
+
if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
|
|
507
|
+
CGPoint croppedOffset = isHorizontal ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
|
|
508
|
+
scrollView.contentOffset = croppedOffset;
|
|
509
|
+
absoluteOffset = 0;
|
|
510
|
+
position = isLastPage ? lastPageIndex : firstPageIndex;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
float interpolatedOffset = absoluteOffset * labs(_destinationIndex - _currentIndex);
|
|
515
|
+
[self sendScrollEventsForPosition:position offset:interpolatedOffset];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
#pragma mark - UIPageViewControllerDelegate
|
|
520
|
+
|
|
521
|
+
- (void)pageViewController:(UIPageViewController *)pageViewController
|
|
522
|
+
didFinishAnimating:(BOOL)finished
|
|
523
|
+
previousViewControllers:(nonnull NSArray<UIViewController *> *)previousViewControllers
|
|
524
|
+
transitionCompleted:(BOOL)completed {
|
|
525
|
+
if (_isBeingRecycled) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (completed) {
|
|
529
|
+
UIViewController* currentVC = [self currentlyDisplayed];
|
|
530
|
+
NSUInteger currentIndex = [_nativeChildrenViewControllers indexOfObject:currentVC];
|
|
531
|
+
_currentIndex = currentIndex;
|
|
532
|
+
int position = (int) currentIndex;
|
|
533
|
+
const auto eventEmitter = [self pagerEventEmitter];
|
|
534
|
+
if (eventEmitter) {
|
|
535
|
+
eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast<double>(position)});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#pragma mark - UIPageViewControllerDataSource
|
|
541
|
+
|
|
542
|
+
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
|
|
543
|
+
viewControllerAfterViewController:(UIViewController *)viewController {
|
|
544
|
+
|
|
545
|
+
UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
|
|
546
|
+
return [self nextControllerForController:viewController inDirection:direction];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
|
|
550
|
+
viewControllerBeforeViewController:(UIViewController *)viewController {
|
|
551
|
+
UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionReverse : UIPageViewControllerNavigationDirectionForward;
|
|
552
|
+
return [self nextControllerForController:viewController inDirection:direction];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#pragma mark - Imperative methods exposed to React Native
|
|
556
|
+
|
|
557
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
|
|
558
|
+
RCTRNCViewPagerHandleCommand(self, commandName, args);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
- (void)setPage:(NSInteger)index {
|
|
562
|
+
[self goTo:index animated:YES];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
- (void)setPageWithoutAnimation:(NSInteger)index {
|
|
566
|
+
[self goTo:index animated:NO];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
- (void)setScrollEnabledImperatively:(BOOL)scrollEnabled {
|
|
570
|
+
_scrollEnabled = scrollEnabled;
|
|
571
|
+
[self applyScrollEnabled];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#pragma mark - Nested Scroll Gesture Coordination
|
|
575
|
+
|
|
576
|
+
- (void)applyNestedScrollBlocker {
|
|
577
|
+
if (scrollView == nil) return;
|
|
578
|
+
|
|
579
|
+
if (_nestedScrollEnabled && _blockerGesture == nil) {
|
|
580
|
+
_blockerGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(blockerGestureFired:)];
|
|
581
|
+
_blockerGesture.delegate = self;
|
|
582
|
+
// Cancel touches so the blocker never actually scrolls anything
|
|
583
|
+
_blockerGesture.cancelsTouchesInView = NO;
|
|
584
|
+
_blockerGesture.delaysTouchesBegan = NO;
|
|
585
|
+
[self addGestureRecognizer:_blockerGesture];
|
|
586
|
+
// Inner scroll view pan must wait for the blocker to fail before it can begin
|
|
587
|
+
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:_blockerGesture];
|
|
588
|
+
} else if (!_nestedScrollEnabled && _blockerGesture != nil) {
|
|
589
|
+
[self removeGestureRecognizer:_blockerGesture];
|
|
590
|
+
_blockerGesture = nil;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// The blocker gesture never actually does anything — it only serves as a gate
|
|
595
|
+
- (void)blockerGestureFired:(UIPanGestureRecognizer *)recognizer {
|
|
596
|
+
// Immediately cancel so it doesn't interfere with other gestures
|
|
597
|
+
if (recognizer.state == UIGestureRecognizerStateBegan) {
|
|
598
|
+
recognizer.state = UIGestureRecognizerStateCancelled;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#pragma mark - UIGestureRecognizerDelegate
|
|
603
|
+
|
|
604
|
+
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
|
605
|
+
if (gestureRecognizer != _blockerGesture) return YES;
|
|
606
|
+
if (!_nestedScrollEnabled) return NO;
|
|
607
|
+
if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) return NO;
|
|
608
|
+
|
|
609
|
+
UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
|
|
610
|
+
CGPoint velocity = [pan velocityInView:self];
|
|
611
|
+
NSInteger pageCount = _nativeChildrenViewControllers.count;
|
|
612
|
+
|
|
613
|
+
if (pageCount == 0) return NO;
|
|
614
|
+
if (_currentIndex < 0 || _currentIndex >= pageCount) return NO;
|
|
615
|
+
|
|
616
|
+
// Nested coordination is designed for horizontal inner/outer pagers only.
|
|
617
|
+
// For vertical pagers, do not block so inner scrolling keeps working.
|
|
618
|
+
BOOL isHorizontal = [self isHorizontal];
|
|
619
|
+
if (!isHorizontal) return NO;
|
|
620
|
+
|
|
621
|
+
// Only edge-block for gestures that are primarily horizontal.
|
|
622
|
+
// This avoids accidental outer-tab switches on vertical/diagonal content scrolls.
|
|
623
|
+
CGFloat absVelocityX = fabs(velocity.x);
|
|
624
|
+
CGFloat absVelocityY = fabs(velocity.y);
|
|
625
|
+
if (absVelocityX <= absVelocityY) return NO;
|
|
626
|
+
if (absVelocityX < 1.0f) return NO;
|
|
627
|
+
|
|
628
|
+
BOOL isLtr = [self isLtrLayout];
|
|
629
|
+
NSInteger firstPageIndex = isLtr ? 0 : pageCount - 1;
|
|
630
|
+
NSInteger lastPageIndex = isLtr ? pageCount - 1 : 0;
|
|
631
|
+
|
|
632
|
+
// At first page, swiping backward → blocker begins → inner scroll blocked → outer takes over
|
|
633
|
+
if (_currentIndex == firstPageIndex && velocity.x > 0) return YES;
|
|
634
|
+
// At last page, swiping forward → blocker begins → inner scroll blocked → outer takes over
|
|
635
|
+
if (_currentIndex == lastPageIndex && velocity.x < 0) return YES;
|
|
636
|
+
|
|
637
|
+
// Not at boundary or swiping inward → blocker fails → inner scroll works normally
|
|
638
|
+
return NO;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Allow the blocker to be recognized simultaneously so it doesn't steal from vertical scrolls
|
|
642
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
643
|
+
if (gestureRecognizer == _blockerGesture) return YES;
|
|
644
|
+
return NO;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#pragma mark - Helpers
|
|
648
|
+
|
|
649
|
+
- (BOOL)isHorizontalRtlLayout {
|
|
650
|
+
return self.isHorizontal && !self.isLtrLayout;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
- (BOOL)isHorizontal {
|
|
654
|
+
return _nativePageViewController.navigationOrientation == UIPageViewControllerNavigationOrientationHorizontal;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
- (BOOL)isLtrLayout {
|
|
658
|
+
return [_layoutDirection isEqualToString: @"ltr"];
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
- (std::shared_ptr<const RNCViewPagerEventEmitter>)pagerEventEmitter
|
|
662
|
+
{
|
|
663
|
+
if (!_eventEmitter) {
|
|
664
|
+
return nullptr;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
assert(std::dynamic_pointer_cast<const RNCViewPagerEventEmitter>(_eventEmitter));
|
|
668
|
+
return std::static_pointer_cast<const RNCViewPagerEventEmitter>(_eventEmitter);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
- (void)sendScrollEventsForPosition:(NSInteger)position offset:(CGFloat)offset {
|
|
672
|
+
const auto eventEmitter = [self pagerEventEmitter];
|
|
673
|
+
if (eventEmitter) {
|
|
674
|
+
eventEmitter->onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{
|
|
675
|
+
.position = static_cast<double>(position),
|
|
676
|
+
.offset = offset
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// This is temporary workaround to allow animations based on onPageScroll event
|
|
681
|
+
// until Fabric implements proper NativeAnimationDriver,
|
|
682
|
+
// see: https://github.com/facebook/react-native/blob/44f431b471c243c92284aa042d3807ba4d04af65/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L59
|
|
683
|
+
RCTOnPageScrollEvent *event = [[RCTOnPageScrollEvent alloc] initWithReactTag:@(self.tag)
|
|
684
|
+
position:@(position)
|
|
685
|
+
offset:@(offset)];
|
|
686
|
+
NSDictionary *userInfo = @{@"event": event};
|
|
687
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
|
|
688
|
+
object:nil
|
|
689
|
+
userInfo:userInfo];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#pragma mark - RCTComponentViewProtocol
|
|
693
|
+
|
|
694
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
695
|
+
{
|
|
696
|
+
return concreteComponentDescriptorProvider<RNCViewPagerComponentDescriptor>();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
@end
|
|
700
|
+
|
|
701
|
+
Class<RCTComponentViewProtocol> RNCViewPagerCls(void)
|
|
702
|
+
{
|
|
703
|
+
return RNCPagerViewComponentView.class;
|
|
704
|
+
}
|