@onekeyfe/react-native-pager-view 3.0.114 → 3.0.116
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/README.md +40 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +630 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +222 -0
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +3 -1
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +39 -0
- package/ios/RNCCollapsiblePagerViewComponentView.h +9 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +991 -0
- package/ios/RNCPagerViewComponentView.mm +4 -2
- package/lib/module/CollapsiblePagerView.js +167 -0
- package/lib/module/CollapsiblePagerView.web.js +478 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +87 -0
- package/lib/module/PagerView.web.js +177 -0
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +6 -0
- package/lib/module/usePagerView.js +1 -1
- package/lib/typescript/src/CollapsiblePagerView.d.ts +58 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +105 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +51 -0
- package/lib/typescript/src/PagerView.web.d.ts +55 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.web.d.ts +5 -0
- package/package.json +5 -2
- package/src/CollapsiblePagerView.tsx +293 -0
- package/src/CollapsiblePagerView.web.tsx +710 -0
- package/src/CollapsiblePagerViewNativeComponent.ts +87 -0
- package/src/PagerView.web.tsx +280 -0
- package/src/index.tsx +1 -0
- 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
|
+
}
|