@onekeyfe/react-native-pager-view 3.0.118 → 3.0.120

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.
@@ -6,11 +6,890 @@
6
6
  #import <react/renderer/components/pagerview/RCTComponentViewHelpers.h>
7
7
 
8
8
  #import "React/RCTConversions.h"
9
+ #import "React/RCTSurfaceTouchHandler.h"
10
+ #import "React/RCTTouchHandler.h"
9
11
 
10
12
  using namespace facebook::react;
11
13
 
12
14
  static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
13
15
 
16
+ typedef void (^RNCCollapsiblePagerNativeTabPressHandler)(NSInteger index, NSString *key);
17
+
18
+ static CGFloat RNCClamp(CGFloat value, CGFloat minimum, CGFloat maximum)
19
+ {
20
+ return MIN(MAX(value, minimum), maximum);
21
+ }
22
+
23
+ static void RNCCollapsiblePagerLog(NSString *message)
24
+ {
25
+ Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
26
+ if (logClass == nil) {
27
+ logClass = NSClassFromString(@"OneKeyLog");
28
+ }
29
+ SEL selector = NSSelectorFromString(@"debug::");
30
+ if (logClass == nil || ![logClass respondsToSelector:selector]) return;
31
+
32
+ typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
33
+ LogFunction logFunction = (LogFunction)[logClass methodForSelector:selector];
34
+ logFunction(logClass, selector, @"CollapsiblePager", message);
35
+ }
36
+
37
+ static BOOL RNCGetColorComponents(UIColor *color, UITraitCollection *traits,
38
+ CGFloat *red, CGFloat *green, CGFloat *blue, CGFloat *alpha)
39
+ {
40
+ UIColor *resolved = [color resolvedColorWithTraitCollection:traits];
41
+ if ([resolved getRed:red green:green blue:blue alpha:alpha]) return YES;
42
+ CGFloat white = 0;
43
+ if ([resolved getWhite:&white alpha:alpha]) {
44
+ *red = white;
45
+ *green = white;
46
+ *blue = white;
47
+ return YES;
48
+ }
49
+ return NO;
50
+ }
51
+
52
+ static UIColor *RNCInterpolateColor(UIColor *from, UIColor *to, CGFloat progress,
53
+ UITraitCollection *traits)
54
+ {
55
+ CGFloat fromRed = 0, fromGreen = 0, fromBlue = 0, fromAlpha = 1;
56
+ CGFloat toRed = 0, toGreen = 0, toBlue = 0, toAlpha = 1;
57
+ if (!RNCGetColorComponents(from, traits, &fromRed, &fromGreen, &fromBlue, &fromAlpha) ||
58
+ !RNCGetColorComponents(to, traits, &toRed, &toGreen, &toBlue, &toAlpha)) {
59
+ return progress >= 0.5 ? to : from;
60
+ }
61
+ CGFloat clamped = RNCClamp(progress, 0, 1);
62
+ return [UIColor colorWithRed:fromRed + (toRed - fromRed) * clamped
63
+ green:fromGreen + (toGreen - fromGreen) * clamped
64
+ blue:fromBlue + (toBlue - fromBlue) * clamped
65
+ alpha:fromAlpha + (toAlpha - fromAlpha) * clamped];
66
+ }
67
+
68
+ @interface RNCCollapsiblePagerHorizontalScrollView : UIScrollView
69
+ @property (nonatomic, copy) NSString *axisOwner;
70
+ @end
71
+
72
+ @implementation RNCCollapsiblePagerHorizontalScrollView
73
+
74
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
75
+ {
76
+ if (gestureRecognizer != self.panGestureRecognizer) {
77
+ return [super gestureRecognizerShouldBegin:gestureRecognizer];
78
+ }
79
+
80
+ CGPoint translation = [self.panGestureRecognizer translationInView:self];
81
+ CGPoint velocity = [self.panGestureRecognizer velocityInView:self];
82
+ CGPoint intent = fabs(translation.x) + fabs(translation.y) >= 1
83
+ ? translation
84
+ : velocity;
85
+ CGFloat maximumOffset = MAX(0, self.contentSize.width - CGRectGetWidth(self.bounds));
86
+ BOOL hasHorizontalRange = maximumOffset > 0;
87
+ BOOL horizontal = MAX(fabs(intent.x), fabs(intent.y)) >= 1 &&
88
+ fabs(intent.x) > fabs(intent.y);
89
+ BOOL shouldBegin = hasHorizontalRange && horizontal &&
90
+ [super gestureRecognizerShouldBegin:gestureRecognizer];
91
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
92
+ @"native-tab-axis owner=%@ dx=%.2f dy=%.2f max=%.2f result=%d",
93
+ _axisOwner ?: @"unknown",
94
+ intent.x,
95
+ intent.y,
96
+ maximumOffset,
97
+ shouldBegin]);
98
+ return shouldBegin;
99
+ }
100
+
101
+ @end
102
+
103
+ static void RNCLogNativeTabScrollBoundary(NSString *owner,
104
+ NSString *phase,
105
+ UIScrollView *scrollView,
106
+ CGFloat startOffsetX)
107
+ {
108
+ NSInteger listState = -1;
109
+ NSInteger pagerState = -1;
110
+ for (UIView *ancestor = scrollView.superview;
111
+ ancestor != nil;
112
+ ancestor = ancestor.superview) {
113
+ if (![ancestor isKindOfClass:UIScrollView.class]) continue;
114
+ UIScrollView *ancestorScrollView = (UIScrollView *)ancestor;
115
+ if (ancestorScrollView.pagingEnabled) {
116
+ pagerState = ancestorScrollView.panGestureRecognizer.state;
117
+ } else if (listState < 0 &&
118
+ (ancestorScrollView.alwaysBounceVertical ||
119
+ ancestorScrollView.contentSize.height >
120
+ CGRectGetHeight(ancestorScrollView.bounds))) {
121
+ listState = ancestorScrollView.panGestureRecognizer.state;
122
+ }
123
+ }
124
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
125
+ @"native-tab-scroll owner=%@ phase=%@ x-start=%.2f x-current=%.2f state=%ld list-state=%ld pager-state=%ld",
126
+ owner,
127
+ phase,
128
+ startOffsetX,
129
+ scrollView.contentOffset.x,
130
+ (long)scrollView.panGestureRecognizer.state,
131
+ (long)listState,
132
+ (long)pagerState]);
133
+ }
134
+
135
+ @interface RNCCollapsiblePagerNativeTabBarView : UIView <UIScrollViewDelegate>
136
+ @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
137
+ - (void)updateItemsJSON:(NSString *)itemsJSON;
138
+ - (void)updateStyleWithHeight:(CGFloat)height
139
+ contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
140
+ itemSpacing:(CGFloat)itemSpacing
141
+ fontSize:(CGFloat)fontSize
142
+ fontFamily:(NSString *)fontFamily
143
+ backgroundColor:(UIColor *)backgroundColor
144
+ activeTextColor:(UIColor *)activeTextColor
145
+ inactiveTextColor:(UIColor *)inactiveTextColor
146
+ indicatorColor:(UIColor *)indicatorColor
147
+ indicatorHeight:(CGFloat)indicatorHeight
148
+ indicatorBottom:(CGFloat)indicatorBottom;
149
+ - (void)setProgress:(CGFloat)progress;
150
+ - (void)setLeftToRight:(BOOL)leftToRight;
151
+ - (void)updateAncestorGesturePrecedence;
152
+ @end
153
+
154
+ @implementation RNCCollapsiblePagerNativeTabBarView {
155
+ RNCCollapsiblePagerHorizontalScrollView *_scrollView;
156
+ UIView *_indicatorView;
157
+ NSArray<NSDictionary *> *_items;
158
+ NSMutableArray<UIButton *> *_buttons;
159
+ NSMutableArray<NSValue *> *_indicatorFrames;
160
+ CGFloat _barHeight;
161
+ CGFloat _contentPaddingHorizontal;
162
+ CGFloat _itemSpacing;
163
+ CGFloat _fontSize;
164
+ NSString *_fontFamily;
165
+ UIColor *_activeTextColor;
166
+ UIColor *_inactiveTextColor;
167
+ UIColor *_indicatorColor;
168
+ CGFloat _indicatorHeight;
169
+ CGFloat _indicatorBottom;
170
+ CGFloat _progress;
171
+ BOOL _leftToRight;
172
+ BOOL _shouldCenterSelectedItem;
173
+ CGFloat _scrollStartOffsetX;
174
+ }
175
+
176
+ - (instancetype)initWithFrame:(CGRect)frame
177
+ {
178
+ if (self = [super initWithFrame:frame]) {
179
+ _items = @[];
180
+ _buttons = [NSMutableArray new];
181
+ _indicatorFrames = [NSMutableArray new];
182
+ _barHeight = 44;
183
+ _contentPaddingHorizontal = 20;
184
+ _itemSpacing = 8;
185
+ _fontSize = 16;
186
+ _activeTextColor = UIColor.labelColor;
187
+ _inactiveTextColor = UIColor.secondaryLabelColor;
188
+ _indicatorColor = UIColor.labelColor;
189
+ _indicatorHeight = 2;
190
+ _indicatorBottom = 0;
191
+ _leftToRight = YES;
192
+ _shouldCenterSelectedItem = YES;
193
+
194
+ _scrollView = [RNCCollapsiblePagerHorizontalScrollView new];
195
+ _scrollView.axisOwner = @"primary";
196
+ _scrollView.showsHorizontalScrollIndicator = NO;
197
+ _scrollView.showsVerticalScrollIndicator = NO;
198
+ _scrollView.alwaysBounceHorizontal = YES;
199
+ _scrollView.directionalLockEnabled = YES;
200
+ _scrollView.delaysContentTouches = NO;
201
+ _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
202
+ _scrollView.delegate = self;
203
+ [self addSubview:_scrollView];
204
+
205
+ _indicatorView = [UIView new];
206
+ _indicatorView.userInteractionEnabled = NO;
207
+ [_scrollView addSubview:_indicatorView];
208
+ }
209
+ return self;
210
+ }
211
+
212
+ - (UIFont *)tabFont
213
+ {
214
+ if (_fontFamily.length > 0) {
215
+ UIFont *font = [UIFont fontWithName:_fontFamily size:_fontSize];
216
+ if (font != nil) return font;
217
+ }
218
+ return [UIFont systemFontOfSize:_fontSize weight:UIFontWeightMedium];
219
+ }
220
+
221
+ - (void)updateItemsJSON:(NSString *)itemsJSON
222
+ {
223
+ NSData *data = [itemsJSON dataUsingEncoding:NSUTF8StringEncoding];
224
+ id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
225
+ NSArray *items = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
226
+ if ([_items isEqualToArray:items]) return;
227
+ _items = items;
228
+
229
+ for (UIButton *button in _buttons) [button removeFromSuperview];
230
+ [_buttons removeAllObjects];
231
+ [_indicatorFrames removeAllObjects];
232
+
233
+ [_items enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger index, BOOL *stop) {
234
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
235
+ button.tag = (NSInteger)index;
236
+ button.titleLabel.font = self.tabFont;
237
+ button.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
238
+ NSString *title = [item[@"title"] isKindOfClass:NSString.class] ? item[@"title"] : @"";
239
+ [button setTitle:title forState:UIControlStateNormal];
240
+ [button addTarget:self action:@selector(handleTabPress:) forControlEvents:UIControlEventTouchUpInside];
241
+ NSString *accessibilityLabel = [item[@"accessibilityLabel"] isKindOfClass:NSString.class]
242
+ ? item[@"accessibilityLabel"]
243
+ : title;
244
+ button.accessibilityLabel = accessibilityLabel;
245
+ NSString *testID = [item[@"testID"] isKindOfClass:NSString.class] ? item[@"testID"] : nil;
246
+ button.accessibilityIdentifier = testID;
247
+ [self->_scrollView addSubview:button];
248
+ [self->_buttons addObject:button];
249
+ }];
250
+
251
+ [_scrollView bringSubviewToFront:_indicatorView];
252
+ for (UIButton *button in _buttons) [_scrollView bringSubviewToFront:button];
253
+ _shouldCenterSelectedItem = YES;
254
+ self.hidden = _items.count == 0;
255
+ [self setNeedsLayout];
256
+ }
257
+
258
+ - (void)updateStyleWithHeight:(CGFloat)height
259
+ contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
260
+ itemSpacing:(CGFloat)itemSpacing
261
+ fontSize:(CGFloat)fontSize
262
+ fontFamily:(NSString *)fontFamily
263
+ backgroundColor:(UIColor *)backgroundColor
264
+ activeTextColor:(UIColor *)activeTextColor
265
+ inactiveTextColor:(UIColor *)inactiveTextColor
266
+ indicatorColor:(UIColor *)indicatorColor
267
+ indicatorHeight:(CGFloat)indicatorHeight
268
+ indicatorBottom:(CGFloat)indicatorBottom
269
+ {
270
+ _barHeight = MAX(1, height);
271
+ _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
272
+ _itemSpacing = MAX(0, itemSpacing);
273
+ _fontSize = MAX(1, fontSize);
274
+ _fontFamily = [fontFamily copy];
275
+ self.backgroundColor = backgroundColor ?: UIColor.clearColor;
276
+ _scrollView.backgroundColor = self.backgroundColor;
277
+ _activeTextColor = activeTextColor ?: UIColor.labelColor;
278
+ _inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
279
+ _indicatorColor = indicatorColor ?: _activeTextColor;
280
+ _indicatorHeight = MAX(0, indicatorHeight);
281
+ _indicatorBottom = MAX(0, indicatorBottom);
282
+ _indicatorView.backgroundColor = _indicatorColor;
283
+ _indicatorView.layer.cornerRadius = _indicatorHeight / 2;
284
+ for (UIButton *button in _buttons) button.titleLabel.font = self.tabFont;
285
+ [self setNeedsLayout];
286
+ }
287
+
288
+ - (void)setLeftToRight:(BOOL)leftToRight
289
+ {
290
+ if (_leftToRight == leftToRight) return;
291
+ _leftToRight = leftToRight;
292
+ _shouldCenterSelectedItem = YES;
293
+ [self setNeedsLayout];
294
+ }
295
+
296
+ - (void)updateAncestorGesturePrecedence
297
+ {
298
+ for (UIView *ancestor = self.superview; ancestor != nil; ancestor = ancestor.superview) {
299
+ if ([ancestor isKindOfClass:UIScrollView.class]) {
300
+ UIScrollView *scrollView = (UIScrollView *)ancestor;
301
+ if (scrollView.pagingEnabled || scrollView.alwaysBounceHorizontal) {
302
+ [scrollView.panGestureRecognizer
303
+ requireGestureRecognizerToFail:_scrollView.panGestureRecognizer];
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ - (void)handleTabPress:(UIButton *)button
310
+ {
311
+ NSInteger index = button.tag;
312
+ if (index < 0 || index >= (NSInteger)_items.count) return;
313
+ NSDictionary *item = _items[index];
314
+ NSString *key = [item[@"key"] isKindOfClass:NSString.class] ? item[@"key"] : @"";
315
+ _shouldCenterSelectedItem = YES;
316
+ if (self.onTabPress) self.onTabPress(index, key);
317
+ }
318
+
319
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
320
+ {
321
+ _shouldCenterSelectedItem = NO;
322
+ _scrollStartOffsetX = scrollView.contentOffset.x;
323
+ RNCLogNativeTabScrollBoundary(@"primary", @"begin", scrollView, _scrollStartOffsetX);
324
+ }
325
+
326
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
327
+ willDecelerate:(BOOL)decelerate
328
+ {
329
+ if (decelerate) return;
330
+ RNCLogNativeTabScrollBoundary(@"primary", @"end", scrollView, _scrollStartOffsetX);
331
+ }
332
+
333
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
334
+ {
335
+ RNCLogNativeTabScrollBoundary(@"primary", @"end", scrollView, _scrollStartOffsetX);
336
+ }
337
+
338
+ - (void)layoutSubviews
339
+ {
340
+ [super layoutSubviews];
341
+ _scrollView.frame = self.bounds;
342
+ UIFont *font = self.tabFont;
343
+ NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
344
+ CGFloat itemsWidth = 0;
345
+ for (UIButton *button in _buttons) {
346
+ NSString *title = [button titleForState:UIControlStateNormal] ?: @"";
347
+ CGFloat textWidth = ceil([title sizeWithAttributes:@{NSFontAttributeName: font}].width);
348
+ CGFloat buttonWidth = MAX(44, textWidth + 16);
349
+ [widths addObject:@(buttonWidth)];
350
+ itemsWidth += buttonWidth;
351
+ }
352
+ if (_buttons.count > 1) itemsWidth += (_buttons.count - 1) * _itemSpacing;
353
+ CGFloat contentWidth = MAX(CGRectGetWidth(self.bounds),
354
+ itemsWidth + _contentPaddingHorizontal * 2);
355
+ _scrollView.contentSize = CGSizeMake(contentWidth, MAX(_barHeight, CGRectGetHeight(self.bounds)));
356
+ [_indicatorFrames removeAllObjects];
357
+
358
+ __block CGFloat x = _leftToRight
359
+ ? _contentPaddingHorizontal
360
+ : contentWidth - _contentPaddingHorizontal;
361
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
362
+ CGFloat buttonWidth = widths[index].doubleValue;
363
+ if (!self->_leftToRight) x -= buttonWidth;
364
+ button.frame = CGRectMake(x, 0, buttonWidth, self->_barHeight);
365
+ NSString *title = [button titleForState:UIControlStateNormal] ?: @"";
366
+ CGFloat textWidth = ceil([title sizeWithAttributes:@{NSFontAttributeName: font}].width);
367
+ CGRect indicatorFrame = CGRectMake(
368
+ CGRectGetMidX(button.frame) - textWidth / 2,
369
+ self->_barHeight - self->_indicatorBottom - self->_indicatorHeight,
370
+ textWidth,
371
+ self->_indicatorHeight
372
+ );
373
+ [self->_indicatorFrames addObject:[NSValue valueWithCGRect:indicatorFrame]];
374
+ if (self->_leftToRight) {
375
+ x += buttonWidth + self->_itemSpacing;
376
+ } else {
377
+ x -= self->_itemSpacing;
378
+ }
379
+ }];
380
+ [self updatePresentation];
381
+ }
382
+
383
+ - (void)setProgress:(CGFloat)progress
384
+ {
385
+ if (_items.count == 0) return;
386
+ if (fabs(_progress - progress) >= 0.001) _shouldCenterSelectedItem = YES;
387
+ _progress = RNCClamp(progress, 0, _items.count - 1);
388
+ if (_indicatorFrames.count == _items.count) [self updatePresentation];
389
+ }
390
+
391
+ - (void)updatePresentation
392
+ {
393
+ NSInteger count = _buttons.count;
394
+ if (count == 0 || _indicatorFrames.count != count) return;
395
+ CGFloat progress = RNCClamp(_progress, 0, count - 1);
396
+ NSInteger lower = (NSInteger)floor(progress);
397
+ NSInteger upper = MIN(lower + 1, count - 1);
398
+ CGFloat fraction = progress - lower;
399
+ CGRect fromFrame = _indicatorFrames[lower].CGRectValue;
400
+ CGRect toFrame = _indicatorFrames[upper].CGRectValue;
401
+ CGRect indicatorFrame = CGRectMake(
402
+ CGRectGetMinX(fromFrame) + (CGRectGetMinX(toFrame) - CGRectGetMinX(fromFrame)) * fraction,
403
+ CGRectGetMinY(fromFrame),
404
+ CGRectGetWidth(fromFrame) + (CGRectGetWidth(toFrame) - CGRectGetWidth(fromFrame)) * fraction,
405
+ _indicatorHeight
406
+ );
407
+ _indicatorView.frame = indicatorFrame;
408
+ _indicatorView.hidden = _indicatorHeight <= 0;
409
+
410
+ NSInteger selectedIndex = (NSInteger)round(progress);
411
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
412
+ CGFloat emphasis = 1 - MIN(1, fabs(progress - index));
413
+ UIColor *color = RNCInterpolateColor(
414
+ self->_inactiveTextColor,
415
+ self->_activeTextColor,
416
+ emphasis,
417
+ self.traitCollection
418
+ );
419
+ [button setTitleColor:color forState:UIControlStateNormal];
420
+ button.accessibilityTraits = index == selectedIndex
421
+ ? UIAccessibilityTraitButton | UIAccessibilityTraitSelected
422
+ : UIAccessibilityTraitButton;
423
+ }];
424
+
425
+ if (_shouldCenterSelectedItem && !_scrollView.tracking && !_scrollView.dragging &&
426
+ !_scrollView.decelerating && CGRectGetWidth(_scrollView.bounds) > 0) {
427
+ CGFloat maximumOffset = MAX(0, _scrollView.contentSize.width - CGRectGetWidth(_scrollView.bounds));
428
+ CGFloat desiredOffset = RNCClamp(
429
+ CGRectGetMidX(indicatorFrame) - CGRectGetWidth(_scrollView.bounds) / 2,
430
+ 0,
431
+ maximumOffset
432
+ );
433
+ [_scrollView setContentOffset:CGPointMake(desiredOffset, 0) animated:NO];
434
+ _shouldCenterSelectedItem = NO;
435
+ }
436
+ }
437
+
438
+ @end
439
+
440
+ @interface RNCCollapsiblePagerNativeSubHeaderView : UIView <UIScrollViewDelegate>
441
+ @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onItemPress;
442
+ @property (nonatomic, readonly) CGFloat preferredHeight;
443
+ - (void)updateConfigJSON:(NSString *)configJSON;
444
+ - (void)updateColorsWithBackgroundColor:(UIColor *)backgroundColor
445
+ activeTextColor:(UIColor *)activeTextColor
446
+ inactiveTextColor:(UIColor *)inactiveTextColor
447
+ selectedBackgroundColor:(UIColor *)selectedBackgroundColor
448
+ fontFamily:(NSString *)fontFamily;
449
+ - (void)setLeftToRight:(BOOL)leftToRight;
450
+ - (void)updateAncestorGesturePrecedence;
451
+ @end
452
+
453
+ @implementation RNCCollapsiblePagerNativeSubHeaderView {
454
+ RNCCollapsiblePagerHorizontalScrollView *_tabsScrollView;
455
+ UIView *_columnsView;
456
+ UILabel *_leadingColumnLabel;
457
+ UILabel *_middleColumnLabel;
458
+ UILabel *_trailingColumnLabel;
459
+ NSArray<NSDictionary *> *_items;
460
+ NSMutableArray<UIButton *> *_buttons;
461
+ NSString *_selectedKey;
462
+ CGFloat _configuredHeight;
463
+ CGFloat _tabsHeight;
464
+ CGFloat _contentPaddingHorizontal;
465
+ CGFloat _itemSpacing;
466
+ CGFloat _fontSize;
467
+ CGFloat _columnFontSize;
468
+ CGFloat _trailingColumnWidth;
469
+ CGFloat _columnGap;
470
+ NSString *_fontFamily;
471
+ UIColor *_activeTextColor;
472
+ UIColor *_inactiveTextColor;
473
+ UIColor *_selectedBackgroundColor;
474
+ BOOL _leftToRight;
475
+ BOOL _shouldCenterSelectedItem;
476
+ CGFloat _scrollStartOffsetX;
477
+ }
478
+
479
+ - (instancetype)initWithFrame:(CGRect)frame
480
+ {
481
+ if (self = [super initWithFrame:frame]) {
482
+ _items = @[];
483
+ _buttons = [NSMutableArray new];
484
+ _selectedKey = @"";
485
+ _configuredHeight = 74;
486
+ _tabsHeight = 42;
487
+ _contentPaddingHorizontal = 20;
488
+ _itemSpacing = 8;
489
+ _fontSize = 14;
490
+ _columnFontSize = 12;
491
+ _trailingColumnWidth = 80;
492
+ _columnGap = 8;
493
+ _activeTextColor = UIColor.labelColor;
494
+ _inactiveTextColor = UIColor.secondaryLabelColor;
495
+ _selectedBackgroundColor = UIColor.secondarySystemFillColor;
496
+ _leftToRight = YES;
497
+ _shouldCenterSelectedItem = YES;
498
+
499
+ _tabsScrollView = [RNCCollapsiblePagerHorizontalScrollView new];
500
+ _tabsScrollView.axisOwner = @"secondary";
501
+ _tabsScrollView.showsHorizontalScrollIndicator = NO;
502
+ _tabsScrollView.showsVerticalScrollIndicator = NO;
503
+ _tabsScrollView.alwaysBounceHorizontal = YES;
504
+ _tabsScrollView.directionalLockEnabled = YES;
505
+ _tabsScrollView.delaysContentTouches = NO;
506
+ _tabsScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
507
+ _tabsScrollView.delegate = self;
508
+ [self addSubview:_tabsScrollView];
509
+
510
+ _columnsView = [UIView new];
511
+ [self addSubview:_columnsView];
512
+ _leadingColumnLabel = [UILabel new];
513
+ _middleColumnLabel = [UILabel new];
514
+ _trailingColumnLabel = [UILabel new];
515
+ for (UILabel *label in @[_leadingColumnLabel, _middleColumnLabel, _trailingColumnLabel]) {
516
+ label.numberOfLines = 1;
517
+ label.lineBreakMode = NSLineBreakByTruncatingTail;
518
+ [_columnsView addSubview:label];
519
+ }
520
+ }
521
+ return self;
522
+ }
523
+
524
+ - (CGFloat)preferredHeight
525
+ {
526
+ return _configuredHeight;
527
+ }
528
+
529
+ - (void)didMoveToWindow
530
+ {
531
+ [super didMoveToWindow];
532
+ if (self.window == nil) return;
533
+
534
+ [self updateAncestorGesturePrecedence];
535
+ }
536
+
537
+ - (void)updateAncestorGesturePrecedence
538
+ {
539
+
540
+ // Keep this nested horizontal scroller ahead of any ancestor pager. Without
541
+ // this precedence, the Discovery pager can consume the category drag.
542
+ for (UIView *ancestor = self.superview; ancestor != nil; ancestor = ancestor.superview) {
543
+ if ([ancestor isKindOfClass:UIScrollView.class]) {
544
+ UIScrollView *scrollView = (UIScrollView *)ancestor;
545
+ if (scrollView.pagingEnabled || scrollView.alwaysBounceHorizontal) {
546
+ [scrollView.panGestureRecognizer
547
+ requireGestureRecognizerToFail:_tabsScrollView.panGestureRecognizer];
548
+ }
549
+ }
550
+ }
551
+ }
552
+
553
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
554
+ {
555
+ _shouldCenterSelectedItem = NO;
556
+ _scrollStartOffsetX = scrollView.contentOffset.x;
557
+ RNCLogNativeTabScrollBoundary(@"secondary", @"begin", scrollView, _scrollStartOffsetX);
558
+ }
559
+
560
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
561
+ willDecelerate:(BOOL)decelerate
562
+ {
563
+ if (decelerate) return;
564
+ RNCLogNativeTabScrollBoundary(@"secondary", @"end", scrollView, _scrollStartOffsetX);
565
+ }
566
+
567
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
568
+ {
569
+ RNCLogNativeTabScrollBoundary(@"secondary", @"end", scrollView, _scrollStartOffsetX);
570
+ }
571
+
572
+ - (UIFont *)fontWithSize:(CGFloat)size
573
+ {
574
+ if (_fontFamily.length > 0) {
575
+ UIFont *font = [UIFont fontWithName:_fontFamily size:size];
576
+ if (font != nil) return font;
577
+ }
578
+ return [UIFont systemFontOfSize:size weight:UIFontWeightMedium];
579
+ }
580
+
581
+ - (void)updateConfigJSON:(NSString *)configJSON
582
+ {
583
+ NSData *data = [configJSON dataUsingEncoding:NSUTF8StringEncoding];
584
+ id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
585
+ NSDictionary *config = [parsed isKindOfClass:NSDictionary.class] ? parsed : @{};
586
+ NSArray *items = [config[@"items"] isKindOfClass:NSArray.class] ? config[@"items"] : @[];
587
+ NSString *selectedKey = [config[@"selectedKey"] isKindOfClass:NSString.class]
588
+ ? config[@"selectedKey"]
589
+ : @"";
590
+ NSDictionary *columns = [config[@"columns"] isKindOfClass:NSDictionary.class]
591
+ ? config[@"columns"]
592
+ : @{};
593
+ NSDictionary *style = [config[@"style"] isKindOfClass:NSDictionary.class]
594
+ ? config[@"style"]
595
+ : @{};
596
+
597
+ BOOL itemsChanged = ![_items isEqualToArray:items];
598
+ BOOL selectedKeyChanged = ![_selectedKey isEqualToString:selectedKey];
599
+ _items = items;
600
+ _selectedKey = [selectedKey copy];
601
+ if (itemsChanged || selectedKeyChanged) _shouldCenterSelectedItem = YES;
602
+ _configuredHeight = MAX(1, [style[@"height"] doubleValue] ?: 74);
603
+ _tabsHeight = MAX(0, [style[@"tabsHeight"] doubleValue] ?: 42);
604
+ _contentPaddingHorizontal = MAX(
605
+ 0,
606
+ [style[@"contentPaddingHorizontal"] doubleValue] ?: 20
607
+ );
608
+ _itemSpacing = MAX(0, [style[@"itemSpacing"] doubleValue] ?: 8);
609
+ _fontSize = MAX(1, [style[@"fontSize"] doubleValue] ?: 14);
610
+ _columnFontSize = MAX(1, [style[@"columnFontSize"] doubleValue] ?: 12);
611
+ _trailingColumnWidth = MAX(0, [style[@"trailingColumnWidth"] doubleValue] ?: 80);
612
+ _columnGap = MAX(0, [style[@"columnGap"] doubleValue] ?: 8);
613
+
614
+ _leadingColumnLabel.text = [columns[@"leading"] isKindOfClass:NSString.class]
615
+ ? columns[@"leading"]
616
+ : @"";
617
+ _middleColumnLabel.text = [columns[@"middle"] isKindOfClass:NSString.class]
618
+ ? columns[@"middle"]
619
+ : @"";
620
+ _trailingColumnLabel.text = [columns[@"trailing"] isKindOfClass:NSString.class]
621
+ ? columns[@"trailing"]
622
+ : @"";
623
+
624
+ if (itemsChanged) {
625
+ for (UIButton *button in _buttons) [button removeFromSuperview];
626
+ [_buttons removeAllObjects];
627
+ [_items enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger index, BOOL *stop) {
628
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
629
+ button.tag = (NSInteger)index;
630
+ button.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
631
+ NSString *title = [item[@"title"] isKindOfClass:NSString.class] ? item[@"title"] : @"";
632
+ [button setTitle:title forState:UIControlStateNormal];
633
+ [button addTarget:self action:@selector(handleItemPress:) forControlEvents:UIControlEventTouchUpInside];
634
+ button.accessibilityLabel = [item[@"accessibilityLabel"] isKindOfClass:NSString.class]
635
+ ? item[@"accessibilityLabel"]
636
+ : title;
637
+ button.accessibilityIdentifier = [item[@"testID"] isKindOfClass:NSString.class]
638
+ ? item[@"testID"]
639
+ : nil;
640
+ [self->_tabsScrollView addSubview:button];
641
+ [self->_buttons addObject:button];
642
+ }];
643
+ }
644
+
645
+ self.hidden = _items.count == 0;
646
+ [self setNeedsLayout];
647
+ }
648
+
649
+ - (void)updateColorsWithBackgroundColor:(UIColor *)backgroundColor
650
+ activeTextColor:(UIColor *)activeTextColor
651
+ inactiveTextColor:(UIColor *)inactiveTextColor
652
+ selectedBackgroundColor:(UIColor *)selectedBackgroundColor
653
+ fontFamily:(NSString *)fontFamily
654
+ {
655
+ self.backgroundColor = backgroundColor ?: UIColor.clearColor;
656
+ _tabsScrollView.backgroundColor = self.backgroundColor;
657
+ _columnsView.backgroundColor = self.backgroundColor;
658
+ _activeTextColor = activeTextColor ?: UIColor.labelColor;
659
+ _inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
660
+ _selectedBackgroundColor = selectedBackgroundColor ?: UIColor.secondarySystemFillColor;
661
+ _fontFamily = [fontFamily copy];
662
+ [self setNeedsLayout];
663
+ }
664
+
665
+ - (void)setLeftToRight:(BOOL)leftToRight
666
+ {
667
+ if (_leftToRight == leftToRight) return;
668
+ _leftToRight = leftToRight;
669
+ [self setNeedsLayout];
670
+ }
671
+
672
+ - (void)handleItemPress:(UIButton *)button
673
+ {
674
+ NSInteger index = button.tag;
675
+ if (index < 0 || index >= (NSInteger)_items.count) return;
676
+ NSDictionary *item = _items[index];
677
+ NSString *key = [item[@"key"] isKindOfClass:NSString.class] ? item[@"key"] : @"";
678
+ if (self.onItemPress) self.onItemPress(index, key);
679
+ }
680
+
681
+ - (void)layoutSubviews
682
+ {
683
+ [super layoutSubviews];
684
+ CGFloat width = CGRectGetWidth(self.bounds);
685
+ CGFloat height = CGRectGetHeight(self.bounds);
686
+ CGFloat tabsHeight = MIN(height, _tabsHeight);
687
+ _tabsScrollView.frame = CGRectMake(0, 0, width, tabsHeight);
688
+ _columnsView.frame = CGRectMake(0, tabsHeight, width, MAX(0, height - tabsHeight));
689
+
690
+ UIFont *itemFont = [self fontWithSize:_fontSize];
691
+ CGFloat itemsWidth = 0;
692
+ NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
693
+ for (UIButton *button in _buttons) {
694
+ button.titleLabel.font = itemFont;
695
+ NSString *title = [button titleForState:UIControlStateNormal] ?: @"";
696
+ CGFloat itemWidth = MAX(44, ceil([title sizeWithAttributes:@{NSFontAttributeName: itemFont}].width) + 20);
697
+ [widths addObject:@(itemWidth)];
698
+ itemsWidth += itemWidth;
699
+ }
700
+ if (_buttons.count > 1) itemsWidth += (_buttons.count - 1) * _itemSpacing;
701
+ CGFloat contentWidth = MAX(width, itemsWidth + _contentPaddingHorizontal * 2);
702
+ _tabsScrollView.contentSize = CGSizeMake(contentWidth, tabsHeight);
703
+ __block CGFloat x = _leftToRight
704
+ ? _contentPaddingHorizontal
705
+ : contentWidth - _contentPaddingHorizontal;
706
+ __block UIButton *selectedButton = nil;
707
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
708
+ CGFloat itemWidth = widths[index].doubleValue;
709
+ if (!self->_leftToRight) x -= itemWidth;
710
+ button.frame = CGRectMake(x, MAX(0, (tabsHeight - 32) / 2), itemWidth, MIN(32, tabsHeight));
711
+ NSDictionary *item = self->_items[index];
712
+ NSString *key = [item[@"key"] isKindOfClass:NSString.class] ? item[@"key"] : @"";
713
+ BOOL selected = [key isEqualToString:self->_selectedKey];
714
+ button.backgroundColor = selected ? self->_selectedBackgroundColor : UIColor.clearColor;
715
+ button.layer.cornerRadius = 10;
716
+ [button setTitleColor:selected ? self->_activeTextColor : self->_inactiveTextColor
717
+ forState:UIControlStateNormal];
718
+ button.accessibilityTraits = selected
719
+ ? UIAccessibilityTraitButton | UIAccessibilityTraitSelected
720
+ : UIAccessibilityTraitButton;
721
+ if (selected) selectedButton = button;
722
+ if (self->_leftToRight) {
723
+ x += itemWidth + self->_itemSpacing;
724
+ } else {
725
+ x -= self->_itemSpacing;
726
+ }
727
+ }];
728
+
729
+ UIFont *columnFont = [self fontWithSize:_columnFontSize];
730
+ for (UILabel *label in @[_leadingColumnLabel, _middleColumnLabel, _trailingColumnLabel]) {
731
+ label.font = columnFont;
732
+ label.textColor = _inactiveTextColor;
733
+ }
734
+ CGFloat columnHeight = CGRectGetHeight(_columnsView.bounds);
735
+ CGFloat half = width / 2;
736
+ CGFloat trailingX = width - _contentPaddingHorizontal - _trailingColumnWidth;
737
+ if (_leftToRight) {
738
+ _leadingColumnLabel.textAlignment = NSTextAlignmentLeft;
739
+ _middleColumnLabel.textAlignment = NSTextAlignmentRight;
740
+ _trailingColumnLabel.textAlignment = NSTextAlignmentRight;
741
+ _leadingColumnLabel.frame = CGRectMake(
742
+ _contentPaddingHorizontal,
743
+ 0,
744
+ MAX(0, half - _contentPaddingHorizontal),
745
+ columnHeight
746
+ );
747
+ _middleColumnLabel.frame = CGRectMake(
748
+ half,
749
+ 0,
750
+ MAX(0, trailingX - _columnGap - half),
751
+ columnHeight
752
+ );
753
+ _trailingColumnLabel.frame = CGRectMake(
754
+ trailingX,
755
+ 0,
756
+ _trailingColumnWidth,
757
+ columnHeight
758
+ );
759
+ } else {
760
+ _leadingColumnLabel.textAlignment = NSTextAlignmentRight;
761
+ _middleColumnLabel.textAlignment = NSTextAlignmentLeft;
762
+ _trailingColumnLabel.textAlignment = NSTextAlignmentLeft;
763
+ _leadingColumnLabel.frame = CGRectMake(
764
+ half,
765
+ 0,
766
+ MAX(0, width - _contentPaddingHorizontal - half),
767
+ columnHeight
768
+ );
769
+ _middleColumnLabel.frame = CGRectMake(
770
+ _contentPaddingHorizontal + _trailingColumnWidth + _columnGap,
771
+ 0,
772
+ MAX(0, half - _contentPaddingHorizontal - _trailingColumnWidth - _columnGap),
773
+ columnHeight
774
+ );
775
+ _trailingColumnLabel.frame = CGRectMake(
776
+ _contentPaddingHorizontal,
777
+ 0,
778
+ _trailingColumnWidth,
779
+ columnHeight
780
+ );
781
+ }
782
+
783
+ if (_shouldCenterSelectedItem && selectedButton != nil &&
784
+ !_tabsScrollView.tracking && !_tabsScrollView.dragging &&
785
+ !_tabsScrollView.decelerating) {
786
+ CGFloat maximumOffset = MAX(0, contentWidth - width);
787
+ CGFloat desiredOffset = RNCClamp(
788
+ CGRectGetMidX(selectedButton.frame) - width / 2,
789
+ 0,
790
+ maximumOffset
791
+ );
792
+ [_tabsScrollView setContentOffset:CGPointMake(desiredOffset, 0) animated:NO];
793
+ _shouldCenterSelectedItem = NO;
794
+ }
795
+ }
796
+
797
+ @end
798
+
799
+ @interface RNCCollapsiblePagerSharedHeaderView : UIView
800
+ @end
801
+
802
+ @implementation RNCCollapsiblePagerSharedHeaderView
803
+
804
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
805
+ {
806
+ if (self.hidden || !self.userInteractionEnabled || self.alpha <= 0.01) return nil;
807
+ // A pinned sticky child can be translated below this host's original bounds.
808
+ // Keep it interactive while the host itself scrolls above the viewport.
809
+ for (UIView *subview in self.subviews.reverseObjectEnumerator) {
810
+ if (subview.hidden || subview.alpha <= 0.01 || !subview.userInteractionEnabled) continue;
811
+ CGPoint childPoint = [subview convertPoint:point fromView:self];
812
+ UIView *hitView = [subview hitTest:childPoint withEvent:event];
813
+ if (hitView != nil) return hitView;
814
+ }
815
+ return nil;
816
+ }
817
+
818
+ @end
819
+
820
+ @interface RNCCollapsiblePagerDirectScrollView : UIScrollView
821
+ @property (nonatomic, weak) UIView *excludedHeaderView;
822
+ @end
823
+
824
+ @implementation RNCCollapsiblePagerDirectScrollView
825
+
826
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
827
+ {
828
+ if (gestureRecognizer == self.panGestureRecognizer && self.excludedHeaderView != nil) {
829
+ CGPoint point = [gestureRecognizer locationInView:self.excludedHeaderView];
830
+ if ([self.excludedHeaderView hitTest:point withEvent:nil] != nil) {
831
+ RNCCollapsiblePagerLog(@"pager-header-excluded result=1");
832
+ return NO;
833
+ }
834
+ }
835
+ return [super gestureRecognizerShouldBegin:gestureRecognizer];
836
+ }
837
+
838
+ @end
839
+
840
+ @interface RNCCollapsiblePagerPressCancellationPanGestureRecognizer : UIPanGestureRecognizer
841
+ @end
842
+
843
+ @implementation RNCCollapsiblePagerPressCancellationPanGestureRecognizer
844
+
845
+ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
846
+ {
847
+ return [preventedGestureRecognizer isKindOfClass:RCTSurfaceTouchHandler.class] ||
848
+ [preventedGestureRecognizer isKindOfClass:RCTTouchHandler.class];
849
+ }
850
+
851
+ @end
852
+
853
+ @interface RNCCollapsiblePagerOuterPagerPanGestureRecognizer : UIPanGestureRecognizer
854
+ @property (nonatomic, strong) NSHashTable<UIGestureRecognizer *> *blockedPagerGestures;
855
+ @end
856
+
857
+ @implementation RNCCollapsiblePagerOuterPagerPanGestureRecognizer
858
+
859
+ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
860
+ {
861
+ return [_blockedPagerGestures containsObject:preventedGestureRecognizer];
862
+ }
863
+
864
+ - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
865
+ {
866
+ UIView *view = preventingGestureRecognizer.view;
867
+ if ([view isKindOfClass:UIScrollView.class] &&
868
+ [view isDescendantOfView:self.view]) {
869
+ UIScrollView *scrollView = (UIScrollView *)view;
870
+ BOOL horizontal = scrollView.alwaysBounceHorizontal ||
871
+ scrollView.contentSize.width > CGRectGetWidth(scrollView.bounds);
872
+ if (horizontal && preventingGestureRecognizer == scrollView.panGestureRecognizer) {
873
+ return YES;
874
+ }
875
+ }
876
+ return NO;
877
+ }
878
+
879
+ @end
880
+
881
+ @interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer : UIPanGestureRecognizer
882
+ @end
883
+
884
+ @implementation RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer
885
+
886
+ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
887
+ {
888
+ return NO;
889
+ }
890
+
891
+ @end
892
+
14
893
  @interface RNCCollapsiblePagerViewComponentView () <
15
894
  RCTRNCCollapsiblePagerViewViewProtocol,
16
895
  UIPageViewControllerDataSource,
@@ -19,32 +898,59 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
19
898
  UIGestureRecognizerDelegate
20
899
  >
21
900
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
901
+ - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
902
+ transition:(NSUInteger)transition;
903
+ - (void)setDirectNativePagerEnabled:(BOOL)enabled;
904
+ - (void)attachSharedHeadersToScrollView:(UIScrollView *)scrollView;
905
+ - (void)liftSharedHeadersForPagerTransition:(NSString *)reason;
906
+ - (void)restoreSharedHeadersToContainer;
907
+ - (void)layoutSharedHeaders;
908
+ - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
909
+ - (void)updateSharedHeaderPressCancellationGesture;
910
+ - (void)detachSharedHeaderPressCancellationGesture;
911
+ - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
912
+ - (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer;
913
+ - (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer;
914
+ - (void)connectVerticalPagerGuard;
915
+ - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer;
22
916
  @end
23
917
 
24
918
  @implementation RNCCollapsiblePagerViewComponentView {
25
919
  UIView *_containerView;
920
+ RNCCollapsiblePagerSharedHeaderView *_sharedHeaderHostView;
26
921
  UIPageViewController *_pageViewController;
922
+ UIScrollView *_pageViewControllerScrollView;
27
923
  UIScrollView *_pagerScrollView;
28
924
  NSMutableArray<UIView<RCTComponentViewProtocol> *> *_logicalChildren;
29
925
  NSMutableArray<UIViewController *> *_pageControllers;
30
926
  UIView *_headerView;
31
927
  UIView *_stickyHeaderView;
928
+ RNCCollapsiblePagerNativeTabBarView *_nativeTabBarView;
929
+ RNCCollapsiblePagerNativeSubHeaderView *_nativeSubHeaderView;
32
930
  NSInteger _currentIndex;
33
931
  NSInteger _destinationIndex;
34
932
  NSInteger _pendingInitialPage;
35
933
  NSInteger _headerHeight;
36
934
  NSInteger _stickyHeaderHeight;
935
+ CGFloat _nativeTabBarHeight;
936
+ CGFloat _nativeSubHeaderHeight;
37
937
  CGFloat _headerOffset;
38
938
  BOOL _scrollEnabled;
39
939
  // OneKey patch: Coordinate this inner pager with an outer horizontal pager.
40
940
  BOOL _nestedScrollEnabled;
941
+ BOOL _nativeSmoothHeaderScrollEnabled;
41
942
  UIPanGestureRecognizer *_blockerGesture;
42
943
  BOOL _transitioning;
43
944
  BOOL _isPagerDragging;
945
+ BOOL _directNativePagerEnabled;
946
+ BOOL _pendingDirectNativePagerEnabled;
947
+ BOOL _hasPendingDirectNativePagerChange;
44
948
  BOOL _hasReceivedPageCommand;
45
949
  NSUInteger _transitionId;
46
950
  // OneKey patch: Retain the latest tab tap while an animation is in flight.
47
951
  NSInteger _pendingGoToIndex;
952
+ BOOL _pendingGoToAnimated;
953
+ BOOL _needsSlotRebuild;
48
954
  BOOL _hasAppliedInitialPage;
49
955
  BOOL _needsPropsReapply;
50
956
  NSString *_layoutDirection;
@@ -57,7 +963,16 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
57
963
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
58
964
  NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
59
965
  __weak UIScrollView *_observedScrollView;
966
+ __weak UIScrollView *_sharedHeaderScrollView;
967
+ __weak UIView *_sharedHeaderPressCancellationGestureHost;
968
+ __weak UIGestureRecognizer *_reactTouchHandler;
969
+ UIPanGestureRecognizer *_sharedHeaderPressCancellationGesture;
970
+ RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
971
+ UIPanGestureRecognizer *_verticalPagerGesture;
60
972
  BOOL _observingContentOffset;
973
+ CGFloat _currentLogicalOffset;
974
+ CGFloat _observedListPanStartLogicalOffset;
975
+ BOOL _sharedHeadersLiftedForPagerTransition;
61
976
  BOOL _isBeingRecycled;
62
977
  NSUInteger _generation;
63
978
  }
@@ -86,8 +1001,15 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
86
1001
  _destinationIndex = 0;
87
1002
  _pendingInitialPage = 0;
88
1003
  _scrollEnabled = YES;
1004
+ _nativeTabBarHeight = 44;
1005
+ _nativeSubHeaderHeight = 74;
89
1006
  _nestedScrollEnabled = NO;
1007
+ _nativeSmoothHeaderScrollEnabled = NO;
1008
+ _directNativePagerEnabled = NO;
1009
+ _pendingDirectNativePagerEnabled = NO;
1010
+ _hasPendingDirectNativePagerChange = NO;
90
1011
  _pendingGoToIndex = -1;
1012
+ _pendingGoToAnimated = YES;
91
1013
  _hasAppliedInitialPage = NO;
92
1014
  _needsPropsReapply = YES;
93
1015
  _layoutDirection = @"ltr";
@@ -96,6 +1018,50 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
96
1018
  _containerView.clipsToBounds = YES;
97
1019
  self.contentView = _containerView;
98
1020
 
1021
+ _sharedHeaderHostView = [RNCCollapsiblePagerSharedHeaderView new];
1022
+ _sharedHeaderHostView.clipsToBounds = NO;
1023
+ [_containerView addSubview:_sharedHeaderHostView];
1024
+
1025
+ _verticalPagerGesture =
1026
+ [[RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer alloc]
1027
+ initWithTarget:self
1028
+ action:@selector(verticalPagerGuardGestureChanged:)];
1029
+ _verticalPagerGesture.delegate = self;
1030
+ _verticalPagerGesture.cancelsTouchesInView = NO;
1031
+ _verticalPagerGesture.enabled = NO;
1032
+ [self addGestureRecognizer:_verticalPagerGesture];
1033
+
1034
+ _nativeTabBarView = [RNCCollapsiblePagerNativeTabBarView new];
1035
+ _nativeTabBarView.hidden = YES;
1036
+ __weak __typeof__(self) weakSelf = self;
1037
+ _nativeTabBarView.onTabPress = ^(NSInteger index, NSString *key) {
1038
+ __strong __typeof__(weakSelf) self = weakSelf;
1039
+ if (self == nil || self->_isBeingRecycled) return;
1040
+ const auto emitter = [self eventEmitter];
1041
+ if (emitter) {
1042
+ emitter->onNativeTabPress({
1043
+ .position = (int)index,
1044
+ .key = std::string(key.UTF8String ?: "")
1045
+ });
1046
+ }
1047
+ };
1048
+ [_sharedHeaderHostView addSubview:_nativeTabBarView];
1049
+
1050
+ _nativeSubHeaderView = [RNCCollapsiblePagerNativeSubHeaderView new];
1051
+ _nativeSubHeaderView.hidden = YES;
1052
+ _nativeSubHeaderView.onItemPress = ^(NSInteger index, NSString *key) {
1053
+ __strong __typeof__(weakSelf) self = weakSelf;
1054
+ if (self == nil || self->_isBeingRecycled) return;
1055
+ const auto emitter = [self eventEmitter];
1056
+ if (emitter) {
1057
+ emitter->onNativeSubHeaderPress({
1058
+ .position = (int)index,
1059
+ .key = std::string(key.UTF8String ?: "")
1060
+ });
1061
+ }
1062
+ };
1063
+ [_sharedHeaderHostView addSubview:_nativeSubHeaderView];
1064
+
99
1065
  [self initializePageViewController];
100
1066
  }
101
1067
  return self;
@@ -104,7 +1070,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
104
1070
  - (void)willMoveToSuperview:(UIView *)newSuperview
105
1071
  {
106
1072
  [super willMoveToSuperview:newSuperview];
107
- if (newSuperview != nil && _pageViewController == nil) {
1073
+ if (newSuperview != nil && !_directNativePagerEnabled && _pageViewController == nil) {
108
1074
  self.contentView = _containerView;
109
1075
  [self initializePageViewController];
110
1076
  }
@@ -113,6 +1079,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
113
1079
  - (void)didMoveToWindow
114
1080
  {
115
1081
  [super didMoveToWindow];
1082
+ [self updateSharedHeaderPressCancellationGesture];
116
1083
  // OneKey patch: removing a decelerating page from its window can suppress
117
1084
  // UIKit's final scroll callback. Restore the last acknowledged page on reattach.
118
1085
  if (self.window != nil && _isPagerDragging &&
@@ -121,8 +1088,14 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
121
1088
  }
122
1089
  }
123
1090
 
1091
+ - (void)dealloc
1092
+ {
1093
+ [self detachSharedHeaderPressCancellationGesture];
1094
+ }
1095
+
124
1096
  - (void)initializePageViewController
125
1097
  {
1098
+ if (_directNativePagerEnabled) return;
126
1099
  _pageViewController = [[UIPageViewController alloc]
127
1100
  initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
128
1101
  navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
@@ -136,6 +1109,7 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
136
1109
  for (UIView *subview in _pageViewController.view.subviews) {
137
1110
  if ([subview isKindOfClass:UIScrollView.class]) {
138
1111
  _pagerScrollView = (UIScrollView *)subview;
1112
+ _pageViewControllerScrollView = _pagerScrollView;
139
1113
  _pagerScrollView.delegate = self;
140
1114
  _pagerScrollView.delaysContentTouches = NO;
141
1115
  _pagerScrollView.scrollEnabled = _scrollEnabled;
@@ -143,6 +1117,200 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
143
1117
  }
144
1118
  }
145
1119
  [self applyNestedScrollBlocker];
1120
+ [self connectVerticalPagerGuard];
1121
+ }
1122
+
1123
+ - (void)setDirectNativePagerEnabled:(BOOL)enabled
1124
+ {
1125
+ if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
1126
+ ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView =
1127
+ _nativeSmoothHeaderScrollEnabled ? _sharedHeaderHostView : nil;
1128
+ }
1129
+ if (_transitioning || _isPagerDragging) {
1130
+ _pendingDirectNativePagerEnabled = enabled;
1131
+ _hasPendingDirectNativePagerChange = YES;
1132
+ return;
1133
+ }
1134
+ _hasPendingDirectNativePagerChange = NO;
1135
+ if (_directNativePagerEnabled == enabled) return;
1136
+
1137
+ [self detachScrollObserver];
1138
+ [self restoreSharedHeadersToContainer];
1139
+ if (_blockerGesture != nil) {
1140
+ [self removeGestureRecognizer:_blockerGesture];
1141
+ _blockerGesture = nil;
1142
+ }
1143
+ for (UIViewController *controller in _pageControllers) {
1144
+ [controller.view removeFromSuperview];
1145
+ }
1146
+
1147
+ if (enabled) {
1148
+ _pageViewController.dataSource = nil;
1149
+ _pageViewController.delegate = nil;
1150
+ _pageViewControllerScrollView.delegate = nil;
1151
+ [_pageViewController.view removeFromSuperview];
1152
+ _pageViewController = nil;
1153
+ _pageViewControllerScrollView = nil;
1154
+
1155
+ RNCCollapsiblePagerDirectScrollView *directPager =
1156
+ [RNCCollapsiblePagerDirectScrollView new];
1157
+ directPager.excludedHeaderView = _nativeSmoothHeaderScrollEnabled
1158
+ ? _sharedHeaderHostView
1159
+ : nil;
1160
+ directPager.pagingEnabled = YES;
1161
+ directPager.directionalLockEnabled = YES;
1162
+ directPager.delaysContentTouches = NO;
1163
+ directPager.showsHorizontalScrollIndicator = NO;
1164
+ directPager.showsVerticalScrollIndicator = NO;
1165
+ directPager.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
1166
+ directPager.scrollEnabled = _scrollEnabled;
1167
+ directPager.delegate = self;
1168
+ [_containerView insertSubview:directPager atIndex:0];
1169
+ _pagerScrollView = directPager;
1170
+ _directNativePagerEnabled = YES;
1171
+ } else {
1172
+ _pagerScrollView.delegate = nil;
1173
+ [_pagerScrollView removeFromSuperview];
1174
+ _pagerScrollView = nil;
1175
+ _directNativePagerEnabled = NO;
1176
+ [self initializePageViewController];
1177
+ }
1178
+
1179
+ [self applyNestedScrollBlocker];
1180
+ [self connectVerticalPagerGuard];
1181
+ [self rebuildSlots];
1182
+ [self updateSharedHeaderPressCancellationGesture];
1183
+ [self setNeedsLayout];
1184
+ }
1185
+
1186
+ - (CGFloat)directPagerOffsetForIndex:(NSInteger)index
1187
+ {
1188
+ CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
1189
+ if (width <= 0 || _pageControllers.count == 0) return 0;
1190
+ NSInteger physicalIndex = self.isLtrLayout
1191
+ ? index
1192
+ : (NSInteger)_pageControllers.count - 1 - index;
1193
+ return width * MAX(0, physicalIndex);
1194
+ }
1195
+
1196
+ - (CGFloat)directPagerProgress
1197
+ {
1198
+ CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
1199
+ if (width <= 0 || _pageControllers.count == 0) return _currentIndex;
1200
+ CGFloat physicalProgress = RNCClamp(
1201
+ _pagerScrollView.contentOffset.x / width,
1202
+ 0,
1203
+ _pageControllers.count - 1
1204
+ );
1205
+ return self.isLtrLayout
1206
+ ? physicalProgress
1207
+ : _pageControllers.count - 1 - physicalProgress;
1208
+ }
1209
+
1210
+ #pragma mark - Smooth shared header ownership
1211
+
1212
+ - (void)restoreSharedHeadersToContainer
1213
+ {
1214
+ _sharedHeaderScrollView = nil;
1215
+ _sharedHeadersLiftedForPagerTransition = NO;
1216
+ _sharedHeaderHostView.layer.zPosition = 0;
1217
+ if (_sharedHeaderHostView.superview != _containerView) {
1218
+ [_containerView addSubview:_sharedHeaderHostView];
1219
+ }
1220
+ [self layoutSharedHeaders];
1221
+ }
1222
+
1223
+ - (void)attachSharedHeadersToScrollView:(UIScrollView *)scrollView
1224
+ {
1225
+ if (!_nativeSmoothHeaderScrollEnabled || _transitioning || _isPagerDragging ||
1226
+ scrollView == nil || _isBeingRecycled) return;
1227
+ if (_sharedHeaderScrollView == scrollView &&
1228
+ _sharedHeaderHostView.superview == scrollView) {
1229
+ [self layoutSharedHeaders];
1230
+ return;
1231
+ }
1232
+
1233
+ _sharedHeaderScrollView = scrollView;
1234
+ _sharedHeadersLiftedForPagerTransition = NO;
1235
+ _sharedHeaderHostView.layer.zPosition = 1000;
1236
+ if (_sharedHeaderHostView.superview != scrollView) {
1237
+ [scrollView addSubview:_sharedHeaderHostView];
1238
+ }
1239
+ [_nativeTabBarView updateAncestorGesturePrecedence];
1240
+ [_nativeSubHeaderView updateAncestorGesturePrecedence];
1241
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
1242
+ [self layoutSharedHeaders];
1243
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1244
+ @"header-owner=list page=%ld offset=%.2f",
1245
+ (long)_currentIndex,
1246
+ _currentLogicalOffset]);
1247
+ }
1248
+
1249
+ - (void)liftSharedHeadersForPagerTransition:(NSString *)reason
1250
+ {
1251
+ if (!_nativeSmoothHeaderScrollEnabled || _sharedHeadersLiftedForPagerTransition ||
1252
+ _isBeingRecycled) return;
1253
+ [self restoreSharedHeadersToContainer];
1254
+ _sharedHeadersLiftedForPagerTransition = YES;
1255
+ [self layoutSharedHeaders];
1256
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1257
+ @"header-owner=pager page=%ld destination=%ld reason=%@",
1258
+ (long)_currentIndex,
1259
+ (long)_destinationIndex,
1260
+ reason ?: @"unknown"]);
1261
+ }
1262
+
1263
+ - (void)layoutSharedHeaders
1264
+ {
1265
+ CGFloat width = CGRectGetWidth(self.bounds);
1266
+ CGFloat totalHeight = _headerHeight + _stickyHeaderHeight;
1267
+ BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
1268
+ _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
1269
+
1270
+ _sharedHeaderHostView.transform = CGAffineTransformIdentity;
1271
+ _headerView.transform = CGAffineTransformIdentity;
1272
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
1273
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
1274
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
1275
+
1276
+ _sharedHeaderHostView.frame = CGRectMake(
1277
+ 0,
1278
+ attachedToList ? -totalHeight : 0,
1279
+ width,
1280
+ totalHeight
1281
+ );
1282
+ CGFloat stickyY = _headerHeight;
1283
+ _headerView.frame = CGRectMake(0, 0, width, _headerHeight);
1284
+ _stickyHeaderView.frame = CGRectMake(0, stickyY, width, _stickyHeaderHeight);
1285
+ _nativeTabBarView.frame = CGRectMake(
1286
+ 0,
1287
+ stickyY,
1288
+ width,
1289
+ MIN(_stickyHeaderHeight, _nativeTabBarHeight)
1290
+ );
1291
+ CGFloat nativeTabBarVisibleHeight = CGRectGetHeight(_nativeTabBarView.frame);
1292
+ _nativeSubHeaderView.frame = CGRectMake(
1293
+ 0,
1294
+ stickyY + nativeTabBarVisibleHeight,
1295
+ width,
1296
+ MIN(
1297
+ MAX(0, _stickyHeaderHeight - nativeTabBarVisibleHeight),
1298
+ _nativeSubHeaderHeight
1299
+ )
1300
+ );
1301
+ [self applyHeaderOffset];
1302
+ }
1303
+
1304
+ - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
1305
+ {
1306
+ if (!_nativeSmoothHeaderScrollEnabled || _headerOffset >= _headerHeight ||
1307
+ pageIndex < 0 || pageIndex >= _pageControllers.count) return;
1308
+
1309
+ _pageOffsets[[self pageKeyForIndex:pageIndex]] = @(_headerOffset);
1310
+ UIScrollView *scrollView = [self findVerticalScrollViewInView:_pageControllers[pageIndex].view];
1311
+ if (scrollView != nil && scrollView != _observedScrollView) {
1312
+ [self applyInsetsToScrollView:scrollView pageIndex:pageIndex restore:YES];
1313
+ }
146
1314
  }
147
1315
 
148
1316
  #pragma mark - React mounting
@@ -169,24 +1337,36 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
169
1337
 
170
1338
  - (void)rebuildSlots
171
1339
  {
1340
+ if (_transitioning || _isPagerDragging) {
1341
+ _needsSlotRebuild = YES;
1342
+ return;
1343
+ }
1344
+ _needsSlotRebuild = NO;
172
1345
  [self detachScrollObserver];
1346
+ [self restoreSharedHeadersToContainer];
173
1347
  // OneKey patch: Fabric reuses these views without resetting transforms
174
1348
  // written by a native parent. Release our collapse translation with the slot.
175
1349
  _headerView.transform = CGAffineTransformIdentity;
176
1350
  _stickyHeaderView.transform = CGAffineTransformIdentity;
1351
+ _sharedHeaderHostView.transform = CGAffineTransformIdentity;
1352
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
1353
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
177
1354
  [_headerView removeFromSuperview];
178
1355
  [_stickyHeaderView removeFromSuperview];
1356
+ for (UIViewController *controller in _pageControllers) {
1357
+ [controller.view removeFromSuperview];
1358
+ }
179
1359
  _headerView = nil;
180
1360
  _stickyHeaderView = nil;
181
1361
  [_pageControllers removeAllObjects];
182
1362
 
183
1363
  if (_logicalChildren.count > 0) {
184
1364
  _headerView = _logicalChildren[0];
185
- [_containerView addSubview:_headerView];
1365
+ [_sharedHeaderHostView addSubview:_headerView];
186
1366
  }
187
1367
  if (_logicalChildren.count > 1) {
188
1368
  _stickyHeaderView = _logicalChildren[1];
189
- [_containerView addSubview:_stickyHeaderView];
1369
+ [_sharedHeaderHostView addSubview:_stickyHeaderView];
190
1370
  }
191
1371
  for (NSInteger index = 2; index < _logicalChildren.count; index++) {
192
1372
  UIView *page = _logicalChildren[index];
@@ -201,18 +1381,32 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
201
1381
  _pendingInitialPage < _pageControllers.count) {
202
1382
  _currentIndex = _pendingInitialPage;
203
1383
  // A recycled host may receive slots before its page controller exists.
204
- _hasAppliedInitialPage = _pageViewController != nil;
1384
+ _hasAppliedInitialPage = _directNativePagerEnabled || _pageViewController != nil;
205
1385
  } else {
206
1386
  _currentIndex = MIN(MAX(_currentIndex, 0), _pageControllers.count - 1);
207
1387
  }
208
- UIViewController *controller = _pageControllers[_currentIndex];
209
- [_pageViewController setViewControllers:@[controller]
210
- direction:UIPageViewControllerNavigationDirectionForward
211
- animated:NO
212
- completion:nil];
1388
+ _destinationIndex = _currentIndex;
1389
+ if (_directNativePagerEnabled) {
1390
+ for (UIViewController *controller in _pageControllers) {
1391
+ [_pagerScrollView addSubview:controller.view];
1392
+ }
1393
+ [_pagerScrollView setContentOffset:CGPointMake(
1394
+ [self directPagerOffsetForIndex:_currentIndex],
1395
+ 0
1396
+ ) animated:NO];
1397
+ } else {
1398
+ UIViewController *controller = _pageControllers[_currentIndex];
1399
+ [_pageViewController setViewControllers:@[controller]
1400
+ direction:UIPageViewControllerNavigationDirectionForward
1401
+ animated:NO
1402
+ completion:nil];
1403
+ }
1404
+ [_nativeTabBarView setProgress:_currentIndex];
213
1405
  }
214
- if (_headerView != nil) [_containerView bringSubviewToFront:_headerView];
215
- if (_stickyHeaderView != nil) [_containerView bringSubviewToFront:_stickyHeaderView];
1406
+ if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
1407
+ if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
1408
+ if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
1409
+ [_containerView bringSubviewToFront:_sharedHeaderHostView];
216
1410
  [self setNeedsLayout];
217
1411
  dispatch_async(dispatch_get_main_queue(), ^{
218
1412
  if (!self->_isBeingRecycled) {
@@ -226,38 +1420,68 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
226
1420
  {
227
1421
  [super layoutSubviews];
228
1422
  _containerView.frame = self.bounds;
229
- _pageViewController.view.frame = _containerView.bounds;
1423
+ if (_directNativePagerEnabled) {
1424
+ _pagerScrollView.frame = _containerView.bounds;
1425
+ } else {
1426
+ _pageViewController.view.frame = _containerView.bounds;
1427
+ }
230
1428
  // OneKey patch: Fabric may mount page slots before applying the host props.
231
1429
  // Commit the pending initial page once both props and page controllers exist.
232
- if (!_hasAppliedInitialPage && _pageViewController != nil &&
1430
+ if (!_transitioning && !_isPagerDragging && !_hasAppliedInitialPage &&
1431
+ (_directNativePagerEnabled ? _pagerScrollView != nil : _pageViewController != nil) &&
233
1432
  _pendingInitialPage >= 0 && _pendingInitialPage < _pageControllers.count) {
234
1433
  [self detachScrollObserver];
235
1434
  _currentIndex = _pendingInitialPage;
236
1435
  _destinationIndex = _currentIndex;
237
1436
  _hasAppliedInitialPage = YES;
238
- [_pageViewController setViewControllers:@[_pageControllers[_currentIndex]]
239
- direction:UIPageViewControllerNavigationDirectionForward
240
- animated:NO
241
- completion:nil];
1437
+ if (_directNativePagerEnabled) {
1438
+ [_pagerScrollView setContentOffset:CGPointMake(
1439
+ [self directPagerOffsetForIndex:_currentIndex],
1440
+ 0
1441
+ ) animated:NO];
1442
+ } else {
1443
+ [_pageViewController setViewControllers:@[_pageControllers[_currentIndex]]
1444
+ direction:UIPageViewControllerNavigationDirectionForward
1445
+ animated:NO
1446
+ completion:nil];
1447
+ }
1448
+ [_nativeTabBarView setProgress:_currentIndex];
242
1449
  }
243
1450
  // 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.
1451
+ // Restore transforms before settling page and shared-header frames.
245
1452
  _headerView.transform = CGAffineTransformIdentity;
246
1453
  _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;
1454
+ _sharedHeaderHostView.transform = CGAffineTransformIdentity;
1455
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
1456
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
1457
+ // Settle native page containers before sizing their React child views.
1458
+ if (_directNativePagerEnabled) {
1459
+ CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
1460
+ CGFloat height = CGRectGetHeight(_pagerScrollView.bounds);
1461
+ _pagerScrollView.contentSize = CGSizeMake(width * _pageControllers.count, height);
1462
+ [_pageControllers enumerateObjectsUsingBlock:^(UIViewController *controller,
1463
+ NSUInteger index,
1464
+ BOOL *stop) {
1465
+ NSInteger physicalIndex = self.isLtrLayout
1466
+ ? (NSInteger)index
1467
+ : (NSInteger)self->_pageControllers.count - 1 - (NSInteger)index;
1468
+ controller.view.frame = CGRectMake(width * physicalIndex, 0, width, height);
1469
+ controller.view.subviews.firstObject.frame = controller.view.bounds;
1470
+ }];
1471
+ if (!_transitioning && !_isPagerDragging) {
1472
+ [_pagerScrollView setContentOffset:CGPointMake(
1473
+ [self directPagerOffsetForIndex:_currentIndex],
1474
+ 0
1475
+ ) animated:NO];
1476
+ }
1477
+ } else {
1478
+ [_pageViewController.view layoutIfNeeded];
1479
+ for (UIViewController *controller in _pageControllers) {
1480
+ controller.view.frame = _pageViewController.view.bounds;
1481
+ controller.view.subviews.firstObject.frame = controller.view.bounds;
1482
+ }
259
1483
  }
260
- [self applyHeaderOffset];
1484
+ [self layoutSharedHeaders];
261
1485
  dispatch_async(dispatch_get_main_queue(), ^{
262
1486
  if (!self->_isBeingRecycled) {
263
1487
  [self prepareAdjacentPageInsets];
@@ -270,7 +1494,9 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
270
1494
  {
271
1495
  _isBeingRecycled = YES;
272
1496
  _generation++;
1497
+ [self detachSharedHeaderPressCancellationGesture];
273
1498
  [self detachScrollObserver];
1499
+ [self restoreSharedHeadersToContainer];
274
1500
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
275
1501
  NSValue *value = [_originalInsets objectForKey:scrollView];
276
1502
  if (value != nil) {
@@ -301,8 +1527,13 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
301
1527
  // written by a native parent. Release our collapse translation with the slot.
302
1528
  _headerView.transform = CGAffineTransformIdentity;
303
1529
  _stickyHeaderView.transform = CGAffineTransformIdentity;
1530
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
1531
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
304
1532
  [_headerView removeFromSuperview];
305
1533
  [_stickyHeaderView removeFromSuperview];
1534
+ for (UIViewController *controller in _pageControllers) {
1535
+ [controller.view removeFromSuperview];
1536
+ }
306
1537
  [_logicalChildren removeAllObjects];
307
1538
  [_pageControllers removeAllObjects];
308
1539
  _headerView = nil;
@@ -315,7 +1546,11 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
315
1546
  _blockerGesture = nil;
316
1547
  }
317
1548
  [_pageViewController.view removeFromSuperview];
1549
+ if (_directNativePagerEnabled) {
1550
+ [_pagerScrollView removeFromSuperview];
1551
+ }
318
1552
  _pageViewController = nil;
1553
+ _pageViewControllerScrollView = nil;
319
1554
  _pagerScrollView = nil;
320
1555
  [super prepareForRecycle];
321
1556
  [_pageOffsets removeAllObjects];
@@ -326,17 +1561,36 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
326
1561
  _pendingInitialPage = 0;
327
1562
  _headerHeight = 0;
328
1563
  _stickyHeaderHeight = 0;
1564
+ _nativeTabBarHeight = 44;
1565
+ _nativeSubHeaderHeight = 74;
329
1566
  _headerOffset = 0;
1567
+ _currentLogicalOffset = 0;
1568
+ _observedListPanStartLogicalOffset = 0;
1569
+ _sharedHeaderHostView.transform = CGAffineTransformIdentity;
1570
+ _sharedHeaderHostView.frame = CGRectZero;
1571
+ _sharedHeaderHostView.layer.zPosition = 0;
330
1572
  _scrollEnabled = YES;
331
1573
  _nestedScrollEnabled = NO;
1574
+ _nativeSmoothHeaderScrollEnabled = NO;
1575
+ _directNativePagerEnabled = NO;
1576
+ _pendingDirectNativePagerEnabled = NO;
1577
+ _hasPendingDirectNativePagerChange = NO;
332
1578
  _transitioning = NO;
333
1579
  _isPagerDragging = NO;
1580
+ _sharedHeaderScrollView = nil;
1581
+ _sharedHeadersLiftedForPagerTransition = NO;
1582
+ _verticalPagerGesture.enabled = NO;
334
1583
  _hasReceivedPageCommand = NO;
335
1584
  _pendingGoToIndex = -1;
1585
+ _pendingGoToAnimated = YES;
1586
+ _needsSlotRebuild = NO;
336
1587
  _hasAppliedInitialPage = NO;
337
1588
  // OneKey patch: Fabric retains old props when recycling this view.
338
1589
  _needsPropsReapply = YES;
339
1590
  _layoutDirection = @"ltr";
1591
+ [_nativeTabBarView updateItemsJSON:@"[]"];
1592
+ [_nativeTabBarView setProgress:0];
1593
+ [_nativeSubHeaderView updateConfigJSON:@"{}"];
340
1594
  _isBeingRecycled = NO;
341
1595
  }
342
1596
 
@@ -362,8 +1616,28 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
362
1616
  _nestedScrollEnabled = newViewProps.nestedScrollEnabled;
363
1617
  [self applyNestedScrollBlocker];
364
1618
  }
1619
+ if (_needsPropsReapply ||
1620
+ oldViewProps.nativeSmoothHeaderScrollEnabled !=
1621
+ newViewProps.nativeSmoothHeaderScrollEnabled) {
1622
+ _nativeSmoothHeaderScrollEnabled = newViewProps.nativeSmoothHeaderScrollEnabled;
1623
+ _verticalPagerGesture.enabled = _nativeSmoothHeaderScrollEnabled;
1624
+ [self connectVerticalPagerGuard];
1625
+ [self setDirectNativePagerEnabled:
1626
+ _nativeSmoothHeaderScrollEnabled || !_nativeTabBarView.hidden];
1627
+ if (_nativeSmoothHeaderScrollEnabled) {
1628
+ [self attachSharedHeadersToScrollView:_observedScrollView];
1629
+ } else {
1630
+ [self restoreSharedHeadersToContainer];
1631
+ }
1632
+ [self updateSharedHeaderPressCancellationGesture];
1633
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1634
+ @"smooth-header enabled=%d",
1635
+ _nativeSmoothHeaderScrollEnabled]);
1636
+ }
365
1637
  if (_needsPropsReapply || oldViewProps.layoutDirection != newViewProps.layoutDirection) {
366
1638
  _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
1639
+ [_nativeTabBarView setLeftToRight:self.isLtrLayout];
1640
+ [_nativeSubHeaderView setLeftToRight:self.isLtrLayout];
367
1641
  }
368
1642
  BOOL insetsChanged = NO;
369
1643
  if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
@@ -385,6 +1659,56 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
385
1659
  _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
386
1660
  [self setNeedsLayout];
387
1661
  }
1662
+ if (_needsPropsReapply || oldViewProps.nativeTabBarItems != newViewProps.nativeTabBarItems) {
1663
+ NSString *itemsJSON = RCTNSStringFromString(newViewProps.nativeTabBarItems);
1664
+ [_nativeTabBarView updateItemsJSON:itemsJSON];
1665
+ [self setDirectNativePagerEnabled:
1666
+ _nativeSmoothHeaderScrollEnabled || !_nativeTabBarView.hidden];
1667
+ [self setNeedsLayout];
1668
+ }
1669
+ if (_needsPropsReapply || oldViewProps.nativeSubHeaderConfig != newViewProps.nativeSubHeaderConfig) {
1670
+ NSString *configJSON = RCTNSStringFromString(newViewProps.nativeSubHeaderConfig);
1671
+ [_nativeSubHeaderView updateConfigJSON:configJSON];
1672
+ _nativeSubHeaderHeight = _nativeSubHeaderView.preferredHeight;
1673
+ [self setNeedsLayout];
1674
+ }
1675
+ if (_needsPropsReapply ||
1676
+ oldViewProps.nativeTabBarHeight != newViewProps.nativeTabBarHeight ||
1677
+ oldViewProps.nativeTabBarContentPaddingHorizontal != newViewProps.nativeTabBarContentPaddingHorizontal ||
1678
+ oldViewProps.nativeTabBarItemSpacing != newViewProps.nativeTabBarItemSpacing ||
1679
+ oldViewProps.nativeTabBarFontSize != newViewProps.nativeTabBarFontSize ||
1680
+ oldViewProps.nativeTabBarFontFamily != newViewProps.nativeTabBarFontFamily ||
1681
+ oldViewProps.nativeTabBarBackgroundColor != newViewProps.nativeTabBarBackgroundColor ||
1682
+ oldViewProps.nativeTabBarActiveTextColor != newViewProps.nativeTabBarActiveTextColor ||
1683
+ oldViewProps.nativeTabBarInactiveTextColor != newViewProps.nativeTabBarInactiveTextColor ||
1684
+ oldViewProps.nativeTabBarIndicatorColor != newViewProps.nativeTabBarIndicatorColor ||
1685
+ oldViewProps.nativeTabBarIndicatorHeight != newViewProps.nativeTabBarIndicatorHeight ||
1686
+ oldViewProps.nativeTabBarIndicatorBottom != newViewProps.nativeTabBarIndicatorBottom ||
1687
+ oldViewProps.nativeSubHeaderSelectedBackgroundColor != newViewProps.nativeSubHeaderSelectedBackgroundColor) {
1688
+ _nativeTabBarHeight = newViewProps.nativeTabBarHeight > 0
1689
+ ? newViewProps.nativeTabBarHeight
1690
+ : 44;
1691
+ NSString *fontFamily = RCTNSStringFromString(newViewProps.nativeTabBarFontFamily);
1692
+ [_nativeTabBarView
1693
+ updateStyleWithHeight:_nativeTabBarHeight
1694
+ contentPaddingHorizontal:MAX(0, newViewProps.nativeTabBarContentPaddingHorizontal)
1695
+ itemSpacing:MAX(0, newViewProps.nativeTabBarItemSpacing)
1696
+ fontSize:newViewProps.nativeTabBarFontSize > 0 ? newViewProps.nativeTabBarFontSize : 16
1697
+ fontFamily:fontFamily
1698
+ backgroundColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarBackgroundColor)
1699
+ activeTextColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarActiveTextColor)
1700
+ inactiveTextColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarInactiveTextColor)
1701
+ indicatorColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarIndicatorColor)
1702
+ indicatorHeight:MAX(0, newViewProps.nativeTabBarIndicatorHeight)
1703
+ indicatorBottom:MAX(0, newViewProps.nativeTabBarIndicatorBottom)];
1704
+ [_nativeSubHeaderView
1705
+ updateColorsWithBackgroundColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarBackgroundColor)
1706
+ activeTextColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarActiveTextColor)
1707
+ inactiveTextColor:RCTUIColorFromSharedColor(newViewProps.nativeTabBarInactiveTextColor)
1708
+ selectedBackgroundColor:RCTUIColorFromSharedColor(newViewProps.nativeSubHeaderSelectedBackgroundColor)
1709
+ fontFamily:fontFamily];
1710
+ [self setNeedsLayout];
1711
+ }
388
1712
 
389
1713
  _needsPropsReapply = NO;
390
1714
  [super updateProps:props oldProps:oldProps];
@@ -392,6 +1716,13 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
392
1716
  [self reapplyInsetsToObservedScrollView];
393
1717
  [self setNeedsLayout];
394
1718
  }
1719
+ if (!_nativeTabBarView.hidden) {
1720
+ [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
1721
+ }
1722
+ if (!_nativeSubHeaderView.hidden) {
1723
+ [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
1724
+ }
1725
+ [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
395
1726
  }
396
1727
 
397
1728
  #pragma mark - Collapsible scroll coordination
@@ -439,11 +1770,16 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
439
1770
  if (_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
440
1771
  UIScrollView *candidate = [self findVerticalScrollViewInView:_pageControllers[_currentIndex].view];
441
1772
  if (candidate == nil) return;
442
- // A decelerating list can otherwise claim a new horizontal touch immediately.
443
- if (_pagerScrollView != nil) {
1773
+ // Legacy mode keeps the historical pager-first dependency. Smooth mode
1774
+ // arbitrates header touches by axis so the vertical list can begin directly.
1775
+ if (_pagerScrollView != nil && !_nativeSmoothHeaderScrollEnabled && !_needsPropsReapply) {
444
1776
  [candidate.panGestureRecognizer requireGestureRecognizerToFail:_pagerScrollView.panGestureRecognizer];
445
1777
  }
446
- if (candidate == _observedScrollView) return;
1778
+ if (candidate == _observedScrollView) {
1779
+ [self attachSharedHeadersToScrollView:candidate];
1780
+ [self updateSharedHeaderPressCancellationGesture];
1781
+ return;
1782
+ }
447
1783
 
448
1784
  BOOL replacingCurrentScrollView = _observingContentOffset;
449
1785
  [self detachScrollObserver];
@@ -461,13 +1797,18 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
461
1797
  forKeyPath:@"contentSize"
462
1798
  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
463
1799
  context:RNCCollapsiblePagerContentOffsetContext];
1800
+ [candidate.panGestureRecognizer addTarget:self action:@selector(observedListPanChanged:)];
464
1801
  _observingContentOffset = YES;
1802
+ _currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
1803
+ [self attachSharedHeadersToScrollView:candidate];
1804
+ [self updateSharedHeaderPressCancellationGesture];
465
1805
  }
466
1806
 
467
1807
  - (void)detachScrollObserver
468
1808
  {
469
1809
  UIScrollView *scrollView = _observedScrollView;
470
1810
  if (scrollView != nil) {
1811
+ [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
471
1812
  _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
472
1813
  if (_observingContentOffset) {
473
1814
  [scrollView removeObserver:self
@@ -482,6 +1823,211 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
482
1823
  _observedScrollView = nil;
483
1824
  }
484
1825
 
1826
+ - (void)detachSharedHeaderPressCancellationGesture
1827
+ {
1828
+ if (_sharedHeaderPressCancellationGesture.view != nil) {
1829
+ [_sharedHeaderPressCancellationGesture.view
1830
+ removeGestureRecognizer:_sharedHeaderPressCancellationGesture];
1831
+ }
1832
+ if (_sharedHeaderOuterPagerGesture.view != nil) {
1833
+ [_sharedHeaderOuterPagerGesture.view
1834
+ removeGestureRecognizer:_sharedHeaderOuterPagerGesture];
1835
+ }
1836
+ _sharedHeaderOuterPagerGesture.blockedPagerGestures = nil;
1837
+ _sharedHeaderPressCancellationGestureHost = nil;
1838
+ _reactTouchHandler = nil;
1839
+ }
1840
+
1841
+ - (void)connectVerticalPagerGuard
1842
+ {
1843
+ if (_pagerScrollView != nil && _verticalPagerGesture != nil) {
1844
+ [_pagerScrollView.panGestureRecognizer
1845
+ requireGestureRecognizerToFail:_verticalPagerGesture];
1846
+ }
1847
+ }
1848
+
1849
+ - (void)updateSharedHeaderPressCancellationGesture
1850
+ {
1851
+ if (!_nativeSmoothHeaderScrollEnabled || self.window == nil || _isBeingRecycled) {
1852
+ [self detachSharedHeaderPressCancellationGesture];
1853
+ return;
1854
+ }
1855
+
1856
+ UIGestureRecognizer *reactTouchHandler = nil;
1857
+ for (UIView *ancestor = self; ancestor != nil; ancestor = ancestor.superview) {
1858
+ for (UIGestureRecognizer *gestureRecognizer in ancestor.gestureRecognizers) {
1859
+ if ([gestureRecognizer isKindOfClass:RCTSurfaceTouchHandler.class] ||
1860
+ [gestureRecognizer isKindOfClass:RCTTouchHandler.class]) {
1861
+ reactTouchHandler = gestureRecognizer;
1862
+ break;
1863
+ }
1864
+ }
1865
+ if (reactTouchHandler != nil) break;
1866
+ }
1867
+
1868
+ UIView *gestureHost = reactTouchHandler.view.superview;
1869
+ if (reactTouchHandler == nil || gestureHost == nil) {
1870
+ [self detachSharedHeaderPressCancellationGesture];
1871
+ RNCCollapsiblePagerLog(@"press-cancel-bridge attached=0 reason=touch-handler-missing");
1872
+ return;
1873
+ }
1874
+
1875
+ NSHashTable<UIGestureRecognizer *> *outerPagerGestures = [NSHashTable weakObjectsHashTable];
1876
+ for (UIView *ancestor = self.superview;
1877
+ ancestor != nil;
1878
+ ancestor = ancestor.superview) {
1879
+ if (![ancestor isKindOfClass:UIScrollView.class]) continue;
1880
+ UIScrollView *scrollView = (UIScrollView *)ancestor;
1881
+ if (scrollView.pagingEnabled) {
1882
+ [outerPagerGestures addObject:scrollView.panGestureRecognizer];
1883
+ }
1884
+ }
1885
+ if (_sharedHeaderPressCancellationGesture.view == gestureHost &&
1886
+ _sharedHeaderOuterPagerGesture.view == _sharedHeaderHostView &&
1887
+ _reactTouchHandler == reactTouchHandler) {
1888
+ _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
1889
+ return;
1890
+ }
1891
+
1892
+ [self detachSharedHeaderPressCancellationGesture];
1893
+ if (_sharedHeaderPressCancellationGesture == nil) {
1894
+ _sharedHeaderPressCancellationGesture =
1895
+ [[RNCCollapsiblePagerPressCancellationPanGestureRecognizer alloc]
1896
+ initWithTarget:self
1897
+ action:@selector(sharedHeaderPressCancellationGestureChanged:)];
1898
+ _sharedHeaderPressCancellationGesture.delegate = self;
1899
+ _sharedHeaderPressCancellationGesture.cancelsTouchesInView = NO;
1900
+ _sharedHeaderPressCancellationGesture.maximumNumberOfTouches = 1;
1901
+ }
1902
+ if (_sharedHeaderOuterPagerGesture == nil) {
1903
+ _sharedHeaderOuterPagerGesture =
1904
+ [[RNCCollapsiblePagerOuterPagerPanGestureRecognizer alloc]
1905
+ initWithTarget:self
1906
+ action:@selector(outerPagerGestureChanged:)];
1907
+ _sharedHeaderOuterPagerGesture.delegate = self;
1908
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = YES;
1909
+ _sharedHeaderOuterPagerGesture.maximumNumberOfTouches = 1;
1910
+ }
1911
+ _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
1912
+ _reactTouchHandler = reactTouchHandler;
1913
+ _sharedHeaderPressCancellationGestureHost = gestureHost;
1914
+ [gestureHost addGestureRecognizer:_sharedHeaderPressCancellationGesture];
1915
+ [_sharedHeaderHostView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
1916
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1917
+ @"press-cancel-bridge attached=1 outer-pagers=%lu",
1918
+ (unsigned long)outerPagerGestures.count]);
1919
+ }
1920
+
1921
+ - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer
1922
+ {
1923
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
1924
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1925
+ @"press-cancel-owner=header page=%ld touch-handler-state=%ld",
1926
+ (long)_currentIndex,
1927
+ (long)_reactTouchHandler.state]);
1928
+ } else if (recognizer.state == UIGestureRecognizerStateEnded ||
1929
+ recognizer.state == UIGestureRecognizerStateCancelled ||
1930
+ recognizer.state == UIGestureRecognizerStateFailed) {
1931
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1932
+ @"press-cancel-end page=%ld state=%ld touch-handler-state=%ld",
1933
+ (long)_currentIndex,
1934
+ (long)recognizer.state,
1935
+ (long)_reactTouchHandler.state]);
1936
+ }
1937
+ }
1938
+
1939
+ - (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer
1940
+ {
1941
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
1942
+ NSMutableArray<NSString *> *blockedStates = [NSMutableArray new];
1943
+ for (UIGestureRecognizer *pagerGesture in
1944
+ _sharedHeaderOuterPagerGesture.blockedPagerGestures) {
1945
+ [blockedStates addObject:[NSString stringWithFormat:
1946
+ @"%@:%ld",
1947
+ NSStringFromClass(pagerGesture.view.class),
1948
+ (long)pagerGesture.state]];
1949
+ }
1950
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1951
+ @"gesture-owner=header-outer-pager-block page=%ld blocked=[%@] rct=%ld list=%ld inner-pager=%ld",
1952
+ (long)_currentIndex,
1953
+ [blockedStates componentsJoinedByString:@","],
1954
+ (long)_reactTouchHandler.state,
1955
+ (long)_observedScrollView.panGestureRecognizer.state,
1956
+ (long)_pagerScrollView.panGestureRecognizer.state]);
1957
+ } else if (recognizer.state == UIGestureRecognizerStateEnded ||
1958
+ recognizer.state == UIGestureRecognizerStateCancelled ||
1959
+ recognizer.state == UIGestureRecognizerStateFailed) {
1960
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1961
+ @"gesture-end=header-outer-pager-block page=%ld state=%ld",
1962
+ (long)_currentIndex,
1963
+ (long)recognizer.state]);
1964
+ }
1965
+ }
1966
+
1967
+ - (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer
1968
+ {
1969
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
1970
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1971
+ @"gesture-owner=vertical-pager-guard page=%ld list=%ld pager=%ld",
1972
+ (long)_currentIndex,
1973
+ (long)_observedScrollView.panGestureRecognizer.state,
1974
+ (long)_pagerScrollView.panGestureRecognizer.state]);
1975
+ } else if (recognizer.state == UIGestureRecognizerStateEnded ||
1976
+ recognizer.state == UIGestureRecognizerStateCancelled ||
1977
+ recognizer.state == UIGestureRecognizerStateFailed) {
1978
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1979
+ @"gesture-end=vertical-pager-guard page=%ld state=%ld",
1980
+ (long)_currentIndex,
1981
+ (long)recognizer.state]);
1982
+ }
1983
+ }
1984
+
1985
+ - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer
1986
+ {
1987
+ if (!_nativeSmoothHeaderScrollEnabled || recognizer != _observedScrollView.panGestureRecognizer) {
1988
+ return;
1989
+ }
1990
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
1991
+ _observedListPanStartLogicalOffset =
1992
+ _observedScrollView.contentOffset.y + _observedScrollView.contentInset.top;
1993
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1994
+ @"gesture-owner=list page=%ld logical=%.2f content-height=%.2f "
1995
+ "bounds-height=%.2f inset=(%.2f,%.2f) enabled=%d dragging=%d",
1996
+ (long)_currentIndex,
1997
+ _observedListPanStartLogicalOffset,
1998
+ _observedScrollView.contentSize.height,
1999
+ _observedScrollView.bounds.size.height,
2000
+ _observedScrollView.contentInset.top,
2001
+ _observedScrollView.contentInset.bottom,
2002
+ _observedScrollView.isScrollEnabled,
2003
+ _observedScrollView.isDragging]);
2004
+ } else if (recognizer.state == UIGestureRecognizerStateEnded ||
2005
+ recognizer.state == UIGestureRecognizerStateCancelled ||
2006
+ recognizer.state == UIGestureRecognizerStateFailed) {
2007
+ CGFloat minimumOffset = -_observedScrollView.contentInset.top;
2008
+ CGFloat maximumOffset = MAX(
2009
+ minimumOffset,
2010
+ _observedScrollView.contentSize.height - _observedScrollView.bounds.size.height +
2011
+ _observedScrollView.contentInset.bottom
2012
+ );
2013
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2014
+ @"gesture-end=list page=%ld state=%ld logical-start=%.2f "
2015
+ "logical-end=%.2f header=%.2f range=(%.2f,%.2f) "
2016
+ "content-height=%.2f bounds-height=%.2f inset=(%.2f,%.2f)",
2017
+ (long)_currentIndex,
2018
+ (long)recognizer.state,
2019
+ _observedListPanStartLogicalOffset,
2020
+ _observedScrollView.contentOffset.y + _observedScrollView.contentInset.top,
2021
+ _headerOffset,
2022
+ minimumOffset,
2023
+ maximumOffset,
2024
+ _observedScrollView.contentSize.height,
2025
+ _observedScrollView.bounds.size.height,
2026
+ _observedScrollView.contentInset.top,
2027
+ _observedScrollView.contentInset.bottom]);
2028
+ }
2029
+ }
2030
+
485
2031
  - (void)restoreDetachedScrollInsets
486
2032
  {
487
2033
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
@@ -574,7 +2120,13 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
574
2120
  NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
575
2121
  // The sticky slot can change height between pages. Cache offsets relative
576
2122
  // to content start so a round trip does not add that height difference.
577
- targetOffset = (saved == nil ? _headerOffset : saved.doubleValue) - next.top;
2123
+ CGFloat restoredLogicalOffset = saved == nil ? _headerOffset : saved.doubleValue;
2124
+ // While the shared header is visible, retained pages must agree on its
2125
+ // collapse position. Deep offsets remain independent after full collapse.
2126
+ if (_nativeSmoothHeaderScrollEnabled && _headerOffset < _headerHeight) {
2127
+ restoredLogicalOffset = _headerOffset;
2128
+ }
2129
+ targetOffset = restoredLogicalOffset - next.top;
578
2130
  targetOffset = MAX(targetOffset, -next.top + _headerOffset);
579
2131
  }
580
2132
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
@@ -623,7 +2175,8 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
623
2175
  return;
624
2176
  }
625
2177
  UIScrollView *scrollView = (UIScrollView *)object;
626
- _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2178
+ _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
2179
+ _pageOffsets[self.currentPageKey] = @(_currentLogicalOffset);
627
2180
  [self updateHeaderForScrollView:scrollView];
628
2181
  return;
629
2182
  }
@@ -633,15 +2186,31 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
633
2186
  - (void)updateHeaderForScrollView:(UIScrollView *)scrollView
634
2187
  {
635
2188
  CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
2189
+ _currentLogicalOffset = logicalOffset;
636
2190
  _headerOffset = MIN(MAX(logicalOffset, 0), _headerHeight);
637
2191
  [self applyHeaderOffset];
638
2192
  }
639
2193
 
640
2194
  - (void)applyHeaderOffset
641
2195
  {
642
- CGAffineTransform transform = CGAffineTransformMakeTranslation(0, -_headerOffset);
643
- _headerView.transform = transform;
644
- _stickyHeaderView.transform = transform;
2196
+ BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
2197
+ _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
2198
+ if (attachedToList) {
2199
+ _sharedHeaderHostView.transform = CGAffineTransformIdentity;
2200
+ _headerView.transform = CGAffineTransformIdentity;
2201
+ CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight);
2202
+ CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
2203
+ _stickyHeaderView.transform = stickyTransform;
2204
+ _nativeTabBarView.transform = stickyTransform;
2205
+ _nativeSubHeaderView.transform = stickyTransform;
2206
+ return;
2207
+ }
2208
+ CGAffineTransform collapseTransform = CGAffineTransformMakeTranslation(0, -_headerOffset);
2209
+ _sharedHeaderHostView.transform = collapseTransform;
2210
+ _headerView.transform = CGAffineTransformIdentity;
2211
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
2212
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
2213
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
645
2214
  }
646
2215
 
647
2216
  #pragma mark - Pager navigation
@@ -656,25 +2225,32 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
656
2225
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
657
2226
  _hasReceivedPageCommand = YES;
658
2227
  _hasAppliedInitialPage = YES;
659
- if (_transitioning && animated) {
2228
+ if (_transitioning || _isPagerDragging) {
660
2229
  _pendingGoToIndex = index;
2230
+ _pendingGoToAnimated = animated;
661
2231
  return;
662
2232
  }
663
2233
 
664
2234
  _pendingGoToIndex = -1;
2235
+ _pendingGoToAnimated = YES;
665
2236
 
666
2237
  // Re-selecting the displayed controller removes its view briefly and cancels
667
2238
  // an in-flight list touch. Focus synchronization must be idempotent.
2239
+ BOOL currentControllerDisplayed = _directNativePagerEnabled
2240
+ ? fabs(_pagerScrollView.contentOffset.x - [self directPagerOffsetForIndex:index]) < 0.5
2241
+ : _pageViewController.viewControllers.firstObject == _pageControllers[index];
668
2242
  if (!_transitioning && index == _currentIndex &&
669
- _pageViewController.viewControllers.firstObject == _pageControllers[index]) {
2243
+ currentControllerDisplayed) {
670
2244
  [self attachScrollObserverForCurrentPage];
671
2245
  [self emitPageSelected:index];
672
2246
  [self emitDiagnostics:@"page-selected"];
673
2247
  return;
674
2248
  }
675
2249
 
676
- [self detachScrollObserver];
2250
+ [self preparePageForHorizontalTransitionAtIndex:index];
677
2251
  _destinationIndex = index;
2252
+ [self liftSharedHeadersForPagerTransition:@"programmatic"];
2253
+ [self detachScrollObserver];
678
2254
  BOOL forward = (index > _currentIndex && self.isLtrLayout) ||
679
2255
  (index < _currentIndex && !self.isLtrLayout);
680
2256
  UIPageViewControllerNavigationDirection direction = forward
@@ -685,6 +2261,47 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
685
2261
  NSUInteger capturedTransition = ++_transitionId;
686
2262
  NSUInteger capturedGeneration = _generation;
687
2263
  __weak __typeof__(self) weakSelf = self;
2264
+ if (_directNativePagerEnabled) {
2265
+ CGFloat targetOffset = [self directPagerOffsetForIndex:index];
2266
+ void (^animations)(void) = ^{
2267
+ [self->_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0)];
2268
+ [self->_nativeTabBarView setProgress:index];
2269
+ };
2270
+ void (^completion)(BOOL) = ^(BOOL finished) {
2271
+ __strong __typeof__(weakSelf) self = weakSelf;
2272
+ if (self == nil || self->_generation != capturedGeneration ||
2273
+ self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
2274
+ self->_pagerScrollView.scrollEnabled = self->_scrollEnabled;
2275
+ if (finished) {
2276
+ self->_currentIndex = index;
2277
+ self->_destinationIndex = index;
2278
+ [self->_nativeTabBarView setProgress:index];
2279
+ [self attachScrollObserverForCurrentPage];
2280
+ [self emitPageSelected:index];
2281
+ [self emitDiagnostics:@"page-selected"];
2282
+ } else if (self->_pendingGoToIndex < 0) {
2283
+ self->_pendingGoToIndex = index;
2284
+ self->_pendingGoToAnimated = animated;
2285
+ }
2286
+ [self completeTransitionOnNextRunLoopForGeneration:capturedGeneration
2287
+ transition:capturedTransition];
2288
+ };
2289
+
2290
+ if (animated && index != _currentIndex) {
2291
+ _pagerScrollView.scrollEnabled = NO;
2292
+ [UIView animateWithDuration:0.28
2293
+ delay:0
2294
+ options:UIViewAnimationOptionCurveEaseInOut |
2295
+ UIViewAnimationOptionAllowUserInteraction
2296
+ animations:animations
2297
+ completion:completion];
2298
+ } else {
2299
+ [UIView performWithoutAnimation:animations];
2300
+ completion(YES);
2301
+ }
2302
+ return;
2303
+ }
2304
+
688
2305
  [_pageViewController setViewControllers:@[_pageControllers[index]]
689
2306
  direction:direction
690
2307
  animated:animated && index != _currentIndex
@@ -694,21 +2311,45 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
694
2311
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
695
2312
  if (finished) {
696
2313
  self->_currentIndex = index;
2314
+ self->_destinationIndex = index;
2315
+ [self->_nativeTabBarView setProgress:index];
697
2316
  [self attachScrollObserverForCurrentPage];
698
2317
  [self emitPageSelected:index];
699
2318
  [self emitDiagnostics:@"page-selected"];
2319
+ } else {
2320
+ [self->_nativeTabBarView setProgress:self->_currentIndex];
2321
+ if (self->_pendingGoToIndex < 0) {
2322
+ self->_pendingGoToIndex = self->_currentIndex;
2323
+ self->_pendingGoToAnimated = NO;
2324
+ }
700
2325
  }
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
- });
2326
+ [self completeTransitionOnNextRunLoopForGeneration:capturedGeneration
2327
+ transition:capturedTransition];
709
2328
  }];
710
2329
  }
711
2330
 
2331
+ - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
2332
+ transition:(NSUInteger)transition
2333
+ {
2334
+ dispatch_async(dispatch_get_main_queue(), ^{
2335
+ if (self->_generation != generation || self->_transitionId != transition ||
2336
+ self->_isBeingRecycled) return;
2337
+ self->_transitioning = NO;
2338
+ if (self->_hasPendingDirectNativePagerChange) {
2339
+ BOOL enabled = self->_pendingDirectNativePagerEnabled;
2340
+ self->_hasPendingDirectNativePagerChange = NO;
2341
+ [self setDirectNativePagerEnabled:enabled];
2342
+ } else if (self->_needsSlotRebuild) {
2343
+ [self rebuildSlots];
2344
+ }
2345
+ if (!self->_hasAppliedInitialPage) [self setNeedsLayout];
2346
+ [self drainPendingGoTo];
2347
+ if (!self->_transitioning && !self->_needsSlotRebuild) {
2348
+ [self attachScrollObserverForCurrentPage];
2349
+ }
2350
+ });
2351
+ }
2352
+
712
2353
  - (UIViewController *)adjacentController:(UIViewController *)controller delta:(NSInteger)delta
713
2354
  {
714
2355
  NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
@@ -737,9 +2378,18 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
737
2378
  - (void)drainPendingGoTo
738
2379
  {
739
2380
  NSInteger pending = _pendingGoToIndex;
2381
+ BOOL animated = _pendingGoToAnimated;
740
2382
  _pendingGoToIndex = -1;
741
- if (pending >= 0 && pending != _currentIndex) {
742
- [self goTo:pending animated:YES];
2383
+ _pendingGoToAnimated = YES;
2384
+ BOOL currentControllerNeedsRestore = NO;
2385
+ if (_currentIndex >= 0 && _currentIndex < _pageControllers.count) {
2386
+ currentControllerNeedsRestore = _directNativePagerEnabled
2387
+ ? fabs(_pagerScrollView.contentOffset.x -
2388
+ [self directPagerOffsetForIndex:_currentIndex]) >= 0.5
2389
+ : _pageViewController.viewControllers.firstObject != _pageControllers[_currentIndex];
2390
+ }
2391
+ if (pending >= 0 && (pending != _currentIndex || currentControllerNeedsRestore)) {
2392
+ [self goTo:pending animated:animated];
743
2393
  }
744
2394
  }
745
2395
 
@@ -747,10 +2397,21 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
747
2397
 
748
2398
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
749
2399
  {
2400
+ if (_directNativePagerEnabled) {
2401
+ [self preparePageForHorizontalTransitionAtIndex:_currentIndex - 1];
2402
+ [self preparePageForHorizontalTransitionAtIndex:_currentIndex + 1];
2403
+ }
750
2404
  // A user drag owns the result even if UIKit later completes the old animation.
751
2405
  ++_transitionId;
2406
+ [self liftSharedHeadersForPagerTransition:@"interactive"];
2407
+ [self detachScrollObserver];
752
2408
  _isPagerDragging = YES;
753
2409
  _transitioning = YES;
2410
+ if (_nativeSmoothHeaderScrollEnabled) {
2411
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2412
+ @"gesture-owner=pager page=%ld",
2413
+ (long)_currentIndex]);
2414
+ }
754
2415
  const auto emitter = [self eventEmitter];
755
2416
  if (emitter) {
756
2417
  emitter->onPageScrollStateChanged({
@@ -779,6 +2440,43 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
779
2440
 
780
2441
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection
781
2442
  {
2443
+ if (_directNativePagerEnabled) {
2444
+ NSInteger index = emitSelection
2445
+ ? (NSInteger)llround([self directPagerProgress])
2446
+ : _currentIndex;
2447
+ index = MAX(0, MIN(index, (NSInteger)_pageControllers.count - 1));
2448
+ [self detachScrollObserver];
2449
+ _currentIndex = index;
2450
+ _destinationIndex = index;
2451
+ [_pagerScrollView setContentOffset:CGPointMake(
2452
+ [self directPagerOffsetForIndex:index],
2453
+ 0
2454
+ ) animated:NO];
2455
+ [_nativeTabBarView setProgress:index];
2456
+ [self attachScrollObserverForCurrentPage];
2457
+ if (emitSelection &&
2458
+ (_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
2459
+ [self emitPageSelected:index];
2460
+ [self emitDiagnostics:@"page-selected"];
2461
+ }
2462
+ _isPagerDragging = NO;
2463
+ if (_nativeSmoothHeaderScrollEnabled) {
2464
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2465
+ @"gesture-end=pager page=%ld",
2466
+ (long)_currentIndex]);
2467
+ }
2468
+ const auto emitter = [self eventEmitter];
2469
+ if (emitter) {
2470
+ emitter->onPageScrollStateChanged({
2471
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Idle
2472
+ });
2473
+ }
2474
+ [self emitDiagnostics:@"pager-idle"];
2475
+ [self completeTransitionOnNextRunLoopForGeneration:_generation
2476
+ transition:_transitionId];
2477
+ return;
2478
+ }
2479
+
782
2480
  if (_isPagerDragging) {
783
2481
  UIViewController *controller = nil;
784
2482
  if (!emitSelection && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
@@ -801,16 +2499,11 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
801
2499
  }
802
2500
  NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
803
2501
  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
- }
2502
+ BOOL controllerNeedsRestore = _pageViewController.viewControllers.firstObject != controller;
811
2503
  [self detachScrollObserver];
812
2504
  _currentIndex = index;
813
2505
  _destinationIndex = index;
2506
+ [_nativeTabBarView setProgress:index];
814
2507
  [self attachScrollObserverForCurrentPage];
815
2508
  // A newer tab tap remains authoritative while its queued command runs.
816
2509
  if (emitSelection &&
@@ -818,9 +2511,17 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
818
2511
  [self emitPageSelected:index];
819
2512
  [self emitDiagnostics:@"page-selected"];
820
2513
  }
2514
+ if (controllerNeedsRestore && _pendingGoToIndex < 0) {
2515
+ _pendingGoToIndex = index;
2516
+ _pendingGoToAnimated = NO;
2517
+ }
821
2518
  }
822
2519
  _isPagerDragging = NO;
823
- _transitioning = NO;
2520
+ }
2521
+ if (_nativeSmoothHeaderScrollEnabled) {
2522
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2523
+ @"gesture-end=pager page=%ld",
2524
+ (long)_currentIndex]);
824
2525
  }
825
2526
  const auto emitter = [self eventEmitter];
826
2527
  if (emitter) {
@@ -829,7 +2530,8 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
829
2530
  });
830
2531
  }
831
2532
  [self emitDiagnostics:@"pager-idle"];
832
- [self drainPendingGoTo];
2533
+ [self completeTransitionOnNextRunLoopForGeneration:_generation
2534
+ transition:_transitionId];
833
2535
  }
834
2536
 
835
2537
  - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
@@ -841,11 +2543,29 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
841
2543
  {
842
2544
  CGFloat width = scrollView.bounds.size.width;
843
2545
  if (width <= 0) return;
2546
+ if (_directNativePagerEnabled && scrollView == _pagerScrollView) {
2547
+ CGFloat progress = [self directPagerProgress];
2548
+ NSInteger position = (NSInteger)floor(progress);
2549
+ CGFloat offset = progress - position;
2550
+ position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
2551
+ [_nativeTabBarView setProgress:progress];
2552
+ const auto emitter = [self eventEmitter];
2553
+ if (emitter) {
2554
+ emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
2555
+ }
2556
+ return;
2557
+ }
844
2558
  CGFloat rawOffset = (scrollView.contentOffset.x - width) / width;
845
2559
  BOOL backwards = self.isLtrLayout ? rawOffset < 0 : rawOffset > 0;
846
2560
  NSInteger position = backwards ? _currentIndex - 1 : _currentIndex;
847
2561
  CGFloat offset = backwards ? 1 - fabs(rawOffset) : fabs(rawOffset);
848
2562
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
2563
+ CGFloat tabProgress = position + offset;
2564
+ if (_transitioning && !_isPagerDragging && _destinationIndex != _currentIndex) {
2565
+ CGFloat transitionProgress = RNCClamp(fabs(rawOffset), 0, 1);
2566
+ tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
2567
+ }
2568
+ [_nativeTabBarView setProgress:tabProgress];
849
2569
  const auto emitter = [self eventEmitter];
850
2570
  if (emitter) {
851
2571
  emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
@@ -939,6 +2659,52 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
939
2659
 
940
2660
  - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
941
2661
  {
2662
+ if (gestureRecognizer == _sharedHeaderOuterPagerGesture) {
2663
+ UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
2664
+ CGPoint translation = [pan translationInView:pan.view];
2665
+ CGPoint velocity = [pan velocityInView:pan.view];
2666
+ CGPoint intent = fabs(translation.x) + fabs(translation.y) >= 1
2667
+ ? translation
2668
+ : velocity;
2669
+ BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
2670
+ BOOL horizontal = hasMotion && fabs(intent.x) > fabs(intent.y);
2671
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2672
+ @"header-axis role=outer-pager-block dx=%.2f dy=%.2f velocity=(%.2f,%.2f) result=%d outer-pagers=%lu",
2673
+ intent.x,
2674
+ intent.y,
2675
+ velocity.x,
2676
+ velocity.y,
2677
+ horizontal,
2678
+ (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
2679
+ return horizontal;
2680
+ }
2681
+ if (gestureRecognizer == _sharedHeaderPressCancellationGesture ||
2682
+ gestureRecognizer == _verticalPagerGesture) {
2683
+ UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
2684
+ CGPoint translation = [pan translationInView:pan.view];
2685
+ CGPoint velocity = [pan velocityInView:pan.view];
2686
+ CGPoint intent = fabs(translation.x) + fabs(translation.y) >= 1
2687
+ ? translation
2688
+ : velocity;
2689
+ BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
2690
+ BOOL vertical = hasMotion && fabs(intent.y) > fabs(intent.x);
2691
+ NSString *role = gestureRecognizer == _sharedHeaderPressCancellationGesture
2692
+ ? @"vertical-cancel"
2693
+ : @"vertical-pager-guard";
2694
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2695
+ @"header-axis role=%@ dx=%.2f dy=%.2f velocity=(%.2f,%.2f) "
2696
+ "result=%d rct=%ld list=%ld pager=%ld",
2697
+ role,
2698
+ intent.x,
2699
+ intent.y,
2700
+ velocity.x,
2701
+ velocity.y,
2702
+ vertical,
2703
+ (long)_reactTouchHandler.state,
2704
+ (long)_observedScrollView.panGestureRecognizer.state,
2705
+ (long)_pagerScrollView.panGestureRecognizer.state]);
2706
+ return vertical;
2707
+ }
942
2708
  if (gestureRecognizer != _blockerGesture) return YES;
943
2709
  if (!_nestedScrollEnabled) return NO;
944
2710
  if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) return NO;
@@ -962,9 +2728,41 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
962
2728
 
963
2729
  - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
964
2730
  {
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.
2731
+ if (gestureRecognizer == _sharedHeaderOuterPagerGesture) {
2732
+ if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
2733
+ _sharedHeaderPressCancellationGestureHost == nil) return NO;
2734
+ UIView *touchView = touch.view;
2735
+ BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
2736
+ [touchView isDescendantOfView:_sharedHeaderHostView];
2737
+ if (!beginsInSharedHeader) return NO;
2738
+
2739
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2740
+ @"outer-pager-touch target=%@ outer-pagers=%lu",
2741
+ touchView == nil ? @"none" : NSStringFromClass(touchView.class),
2742
+ (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
2743
+ return YES;
2744
+ }
2745
+ if (gestureRecognizer == _sharedHeaderPressCancellationGesture) {
2746
+ if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
2747
+ _sharedHeaderPressCancellationGestureHost == nil) return NO;
2748
+ UIView *touchView = touch.view;
2749
+ BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
2750
+ [touchView isDescendantOfView:_sharedHeaderHostView];
2751
+ if (!beginsInSharedHeader) return NO;
2752
+
2753
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
2754
+ @"header-touch target=%@ shared=1",
2755
+ touchView == nil ? @"none" : NSStringFromClass(touchView.class)]);
2756
+ return YES;
2757
+ }
2758
+ if (gestureRecognizer == _verticalPagerGesture) {
2759
+ UIView *touchView = touch.view;
2760
+ return touchView != _sharedHeaderHostView &&
2761
+ ![touchView isDescendantOfView:_sharedHeaderHostView];
2762
+ }
2763
+ // A new touch can cancel UIKit's animation before its first frame. Queue a
2764
+ // non-animated settle for the next safe transition boundary instead of
2765
+ // re-entering setViewControllers while UIKit is flushing the current view.
968
2766
  if (gestureRecognizer == _blockerGesture && _transitioning && !_isPagerDragging &&
969
2767
  [touch.view isDescendantOfView:_pagerScrollView]) {
970
2768
  [self goTo:_destinationIndex animated:NO];
@@ -975,6 +2773,30 @@ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerConte
975
2773
  - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
976
2774
  shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
977
2775
  {
2776
+ if (gestureRecognizer == _sharedHeaderOuterPagerGesture ||
2777
+ otherGestureRecognizer == _sharedHeaderOuterPagerGesture) {
2778
+ UIGestureRecognizer *other = gestureRecognizer == _sharedHeaderOuterPagerGesture
2779
+ ? otherGestureRecognizer
2780
+ : gestureRecognizer;
2781
+ return ![other isKindOfClass:RCTSurfaceTouchHandler.class] &&
2782
+ ![other isKindOfClass:RCTTouchHandler.class] &&
2783
+ ![_sharedHeaderOuterPagerGesture.blockedPagerGestures containsObject:other];
2784
+ }
2785
+ if (gestureRecognizer == _sharedHeaderPressCancellationGesture ||
2786
+ otherGestureRecognizer == _sharedHeaderPressCancellationGesture) {
2787
+ UIGestureRecognizer *other = gestureRecognizer == _sharedHeaderPressCancellationGesture
2788
+ ? otherGestureRecognizer
2789
+ : gestureRecognizer;
2790
+ return ![other isKindOfClass:RCTSurfaceTouchHandler.class] &&
2791
+ ![other isKindOfClass:RCTTouchHandler.class];
2792
+ }
2793
+ if (gestureRecognizer == _verticalPagerGesture ||
2794
+ otherGestureRecognizer == _verticalPagerGesture) {
2795
+ UIGestureRecognizer *other = gestureRecognizer == _verticalPagerGesture
2796
+ ? otherGestureRecognizer
2797
+ : gestureRecognizer;
2798
+ return other != _pagerScrollView.panGestureRecognizer;
2799
+ }
978
2800
  return gestureRecognizer == _blockerGesture;
979
2801
  }
980
2802