@onekeyfe/react-native-pager-view 6.7.0-alpha.344 → 6.7.0-alpha.359

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.
@@ -239,6 +239,18 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
239
239
  view.applyNativeHeaderStyle()
240
240
  }
241
241
 
242
+ @ReactProp(name = "nativeTabBarVariant")
243
+ override fun setNativeTabBarVariant(view: CollapsiblePagerHost?, value: String?) {
244
+ view ?: return
245
+ view.nativeTabBarVariant = if (value == "segmented") "segmented" else "underline"
246
+ view.applyNativeHeaderStyle()
247
+ }
248
+
249
+ @ReactProp(name = "nativeTabBarTrailingReservedWidth", defaultDouble = 0.0)
250
+ override fun setNativeTabBarTrailingReservedWidth(view: CollapsiblePagerHost?, value: Double) {
251
+ view?.nativeTabBarTrailingReservedWidth = value
252
+ }
253
+
242
254
  @ReactProp(name = "nativeTabBarContentPaddingHorizontal")
243
255
  override fun setNativeTabBarContentPaddingHorizontal(
244
256
  view: CollapsiblePagerHost?,
@@ -249,6 +261,13 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
249
261
  view.applyNativeHeaderStyle()
250
262
  }
251
263
 
264
+ @ReactProp(name = "nativeTabBarContentPaddingVertical")
265
+ override fun setNativeTabBarContentPaddingVertical(view: CollapsiblePagerHost?, value: Double) {
266
+ view ?: return
267
+ view.nativeTabBarContentPaddingVertical = if (value.isFinite()) value.coerceAtLeast(0.0) else 0.0
268
+ view.applyNativeHeaderStyle()
269
+ }
270
+
252
271
  @ReactProp(name = "nativeTabBarItemSpacing")
253
272
  override fun setNativeTabBarItemSpacing(view: CollapsiblePagerHost?, value: Double) {
254
273
  view ?: return
@@ -343,6 +362,14 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
343
362
  view?.scrollToTop(target, animated)
344
363
  }
345
364
 
365
+ override fun scrollHeaderBy(view: CollapsiblePagerHost?, delta: Double) {
366
+ view?.scrollHeaderBy(delta)
367
+ }
368
+
369
+ override fun scrollHeaderToOffset(view: CollapsiblePagerHost?, offset: Double) {
370
+ view?.scrollHeaderToOffset(offset)
371
+ }
372
+
346
373
  override fun setPage(view: CollapsiblePagerHost?, selectedPage: Int) {
347
374
  if (view != null && selectedPage in 0 until view.adapter.itemCount) {
348
375
  view.beginPageTransition(selectedPage)
@@ -37,26 +37,50 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
37
37
  setMeasuredDimension(MeasureSpec.getSize(widthSpec), MeasureSpec.getSize(heightSpec))
38
38
  }
39
39
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) = Unit
40
+
41
+ override fun startViewTransition(view: View) {
42
+ super.startViewTransition(view)
43
+ if (view === reactChild) transitioningReactChild = view
44
+ }
45
+
46
+ override fun endViewTransition(view: View) {
47
+ super.endViewTransition(view)
48
+ if (transitioningReactChild === view) transitioningReactChild = null
49
+ if (retainedReactChild === view) {
50
+ clearRetainedGeometry()
51
+ if (isAttachedToWindow) scheduleLayout()
52
+ }
53
+ }
40
54
  }
41
55
  private val contentFrame = object : FrameLayout(context) {
42
56
  override fun onMeasure(widthSpec: Int, heightSpec: Int) {
43
57
  setMeasuredDimension(
44
- max(MeasureSpec.getSize(widthSpec), contentWidthPx + insetLeft + insetRight),
45
- contentHeightPx + contentTop + insetBottom,
58
+ max(MeasureSpec.getSize(widthSpec), effectiveContentWidthPx + insetLeft + insetRight),
59
+ effectiveContentHeightPx + contentTop + insetBottom,
46
60
  )
47
61
  }
48
62
 
49
63
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
50
64
  // Only the native origin layer moves. Fabric owns its React child's layout/transform.
65
+ val contentWidth = effectiveContentWidthPx
66
+ val contentHeight = effectiveContentHeightPx
51
67
  contentOriginFrame.measure(
52
- MeasureSpec.makeMeasureSpec(contentWidthPx, MeasureSpec.EXACTLY),
53
- MeasureSpec.makeMeasureSpec(contentHeightPx, MeasureSpec.EXACTLY),
68
+ MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY),
69
+ MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY),
54
70
  )
55
- contentOriginFrame.layout(insetLeft, contentTop, insetLeft + contentWidthPx, contentTop + contentHeightPx)
71
+ contentOriginFrame.layout(insetLeft, contentTop, insetLeft + contentWidth, contentTop + contentHeight)
56
72
  }
57
73
  }
58
74
  private val pagerInsets = WeakHashMap<View, Int>()
59
75
  private var reactChild: View? = null
76
+ private var transitioningReactChild: View? = null
77
+ private var retainedReactChild: View? = null
78
+ private var retainedContentWidthPx = 0
79
+ private var retainedContentHeightPx = 0
80
+ private val effectiveContentWidthPx: Int
81
+ get() = if (reactChild == null && retainedReactChild != null) retainedContentWidthPx else contentWidthPx
82
+ private val effectiveContentHeightPx: Int
83
+ get() = if (reactChild == null && retainedReactChild != null) retainedContentHeightPx else contentHeightPx
60
84
  private var fabricState: StateWrapper? = null
61
85
  private var lastOriginX: Int? = null
62
86
  private var lastOriginY: Int? = null
@@ -172,6 +196,8 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
172
196
  internal fun addReactChild(child: View, index: Int) {
173
197
  require(index == 0 && reactChild == null) { "NativeScroller requires one React content view" }
174
198
  viewport.stopMotion()
199
+ clearRetainedGeometry()
200
+ transitioningReactChild = null
175
201
  reactChild = child
176
202
  contentOriginFrame.addView(child)
177
203
  scheduleLayout()
@@ -180,13 +206,29 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
180
206
  internal fun removeReactChild() {
181
207
  viewport.stopMotion()
182
208
  pendingRestore.cancel()
183
- reactChild?.let(contentOriginFrame::removeView)
209
+ reactChild?.let { child ->
210
+ clearRetainedGeometry()
211
+ // Fabric removes the child before its disappearing view finishes drawing.
212
+ if (transitioningReactChild === child &&
213
+ contentOriginFrame.width > 0 && contentOriginFrame.height > 0) {
214
+ retainedReactChild = child
215
+ retainedContentWidthPx = contentOriginFrame.width
216
+ retainedContentHeightPx = contentOriginFrame.height
217
+ }
218
+ contentOriginFrame.removeView(child)
219
+ }
184
220
  reactChild = null
185
221
  contentWidthPx = 0
186
222
  contentHeightPx = 0
187
223
  scheduleLayout()
188
224
  }
189
225
 
226
+ private fun clearRetainedGeometry() {
227
+ retainedReactChild = null
228
+ retainedContentWidthPx = 0
229
+ retainedContentHeightPx = 0
230
+ }
231
+
190
232
  internal fun reactChildCount() = if (reactChild == null) 0 else 1
191
233
  internal fun reactChildAt(index: Int): View {
192
234
  require(index == 0)
@@ -381,6 +423,8 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
381
423
  lastContentViewportHeight = null
382
424
  viewport.stopMotion()
383
425
  viewport.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, null)
426
+ transitioningReactChild = null
427
+ clearRetainedGeometry()
384
428
  super.onDetachedFromWindow()
385
429
  }
386
430
 
package/docs/SPEC.md CHANGED
@@ -74,7 +74,7 @@ the shared host before their old scroll-edge interaction is released.
74
74
  The measured overlay height is excluded from the collapsible header distance:
75
75
  `header` remains collapsible and `stickyHeader` pins immediately below the
76
76
  overlay. Selection, cancellation, reuse and disposal rebind or release the
77
- interaction and restore any prior scroll-edge style. Native scroll/refresh
77
+ interaction and restore any prior scroll-edge style. The existing owner-scoped native sticky-height callback also reaches NativeList hosts, so their React container slots clip below the same fixed viewport region and release it when the owner detaches. Native scroll/refresh
78
78
  coordination continues on the main thread without per-frame JavaScript work.
79
79
  Refresh retains the pager-owned `overlay + header + sticky` top inset but offsets
80
80
  the indicator by `header + sticky`, so its spinner begins below the fixed overlay.
@@ -171,6 +171,24 @@ intent. Real vertical consumption, a newer valid command, or owner detachment
171
171
  releases the target. Existing NativeList and NativeScroller public coordinates
172
172
  are unchanged. The same page-top/sticky-top destinations are available without smooth-header mode.
173
173
 
174
+ ## Current-page partial header command
175
+
176
+ `scrollHeaderBy(delta: number)` moves the current page's shared collapsible
177
+ header from its displayed offset C to `clamp(C + delta, 0, D)`.
178
+ `scrollHeaderToOffset(offset: number)` moves it to `clamp(offset, 0, D)`
179
+ regardless of intervening native keyboard or user scrolling. D is the latest
180
+ measured collapsible height. Inputs use logical points on iOS and Android and
181
+ CSS pixels on Web. Non-finite values are ignored. The header and current body move immediately
182
+ so a focused input becomes visible before the next keyboard frame. A valid command
183
+ clears only the current page's body reading position B; it never changes
184
+ another page's memory or adds permanent content/insets. It requires the same
185
+ stable attached current owner as the page-top commands and is a safe no-op
186
+ during horizontal transitions, before mount, or after disposal. A newer
187
+ command or real user vertical consumption takes ownership from an older one.
188
+ Dynamic header measurements clamp any in-flight target to the latest D.
189
+ This command allows a focused input inside the shared header to move above a
190
+ keyboard without replacing the pager's sole vertical scroll owner.
191
+
174
192
  ## Independent NativeScroller migration
175
193
 
176
194
  Status: implemented in source. Both native builds pass. Android focused emulator
@@ -333,6 +351,32 @@ warnings were observed. Keyboard, multi-touch, physical-device performance and
333
351
  the full lifecycle/interaction matrix remain unverified. These are local source
334
352
  builds; the currently installed npm version does not yet contain this migration.
335
353
 
354
+ ## Android removal-transition geometry retention
355
+
356
+ Status: implemented locally; unreleased. On the Android UI thread, during a
357
+ screen removal transition,
358
+ NativeList and CollapsiblePager layout entry points respect the RecyclerView
359
+ suppression state established by the screen-transition owner. The owner restores
360
+ the original state when its last transition ownership ends. This adds no public
361
+ API and does not change Web or iOS behavior.
362
+
363
+ If Fabric removes the current NativeScroller React child while its origin-frame
364
+ transition remains active, Android retains the already displayed native extent
365
+ for drawing. The current child and raw content dimensions still clear normally;
366
+ only native content measurement and layout use the retained effective extent.
367
+ The matching transition end or detach clears the private child and extent state
368
+ idempotently. Adding a new child cancels prior retention, and a late end for the
369
+ old child cannot alter the new content. Ordinary removal, size events and scroll
370
+ commands keep their existing semantics.
371
+
372
+ Local arm64 DevShell acceptance covered Base, CXO and Overview exit content,
373
+ normal scrolling, RecyclerView ownership and suppression cleanup for the
374
+ observed CXO exit, and NativeScroller raw-zero content dimensions with a
375
+ nonzero displayed origin followed by detach cleanup. Nested-owner and cancel
376
+ paths, Overview rebound/cancel, natural resize and new-child runtime cases,
377
+ and the full performance matrix remain unverified. SDK publication and
378
+ consumer version alignment are separate steps.
379
+
336
380
  ## iOS vertical-scroll press cancellation
337
381
 
338
382
  Status: implemented in source. A source-identical `node_modules` build was
@@ -378,7 +422,7 @@ content, the shared header, and touches beginning on an inner horizontal banner.
378
422
  Normal taps MUST remain functional. Press-retention behavior in application
379
423
  controls belongs to the consuming responder implementation; the Pager package
380
424
  does not contain or publish Tamagui helpers. The native application tab bar has
381
- a separate contract in react-native-tab-view/docs/SPEC.md.
425
+ a separate contract in react-native-bottom-tabs/docs/SPEC.md.
382
426
 
383
427
  The current native implementation confirms horizontal intent after at least
384
428
  32 points on iOS or 32 dp on Android, with horizontal displacement exceeding
@@ -452,6 +496,40 @@ The attached UIKit regression intentionally constrains the real pager pan to two
452
496
  required touches to prove the no-pan path; it is distinct from natural Home OS
453
497
  input, coalesced-event delivery, and physical-device acceptance.
454
498
 
499
+ ## Native segmented tab bar
500
+
501
+ Status: implemented locally. Focused production-class iOS XCTest and Android
502
+ instrumentation checks pass; consuming App interaction acceptance is separate.
503
+ `nativeTabBar.style.variant` accepts `"underline"` (the default) or
504
+ `"segmented"` on iOS and Android. The optional
505
+ `nativeTabBar.style.contentPaddingVertical` is a nonnegative logical inset,
506
+ defaulting to zero. Omitting both fields preserves the existing content-width,
507
+ horizontally scrollable underline bar. Web ignores `nativeTabBar` and retains
508
+ its caller-rendered sticky header.
509
+
510
+ In segmented mode, the existing `height` is the entire tab slot. The track
511
+ fills the space inside `contentPaddingHorizontal` and
512
+ `contentPaddingVertical`, uses `backgroundColor`, and has a fully rounded
513
+ outline. Its visible items divide the track into equal-width segments in their
514
+ current order, including after item insertion, removal, relabeling, or resize.
515
+ The thumb uses `indicatorColor`, is inset two logical points from its segment,
516
+ and is fully rounded. Segment titles use the existing font and active/inactive
517
+ text colors, remain single-line with truncation, and retain their existing
518
+ accessibility labels, test IDs, and native press event keys. The segmented bar
519
+ does not scroll horizontally; `itemSpacing`, `indicatorHeight`, and
520
+ `indicatorBottom` apply only to underline mode. The pager's native fractional
521
+ page progress moves the thumb continuously between equal-width segments and
522
+ interpolates text emphasis without per-frame JavaScript updates. It settles at
523
+ the actual selected page after a completed or cancelled gesture. RTL reverses
524
+ physical segment order without changing logical page keys or indices.
525
+
526
+ Switching variants or removing the native tab bar clears the previous visual
527
+ state. The existing native press command, page callbacks, retention, vertical
528
+ header geometry, and Web behavior remain unchanged. Acceptance requires
529
+ focused native geometry/progress/press checks on iOS and Android, including
530
+ two or three segments, dynamic labels/items, RTL, narrow widths, and variant
531
+ switching. Type forwarding alone does not prove rendered behavior.
532
+
455
533
  ## Native tab-press animation default
456
534
 
457
535
  Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
@@ -472,6 +550,28 @@ Status: implemented; focused native-wrapper tests cover both directions,
472
550
  explicit overrides and in-flight/superseded command targets. Home applies this policy to its JS tab bar, with iOS and
473
551
  Android simulator recordings verifying distant jumps and adjacent transitions.
474
552
 
553
+ ## Native tab trailing control reservation
554
+
555
+ Status: implemented locally, unreleased. The focused Android host test passed
556
+ (1/1); iOS hit tests had 8 passes and 1 existing skip. iOS companion-app UI
557
+ acceptance remains unverified.
558
+ `nativeTabBar.style.trailingReservedWidth` is an optional logical trailing
559
+ reservation for a sibling React control in the full-width sticky header. It
560
+ defaults to zero. JavaScript normalizes non-finite or negative input to zero;
561
+ native also clamps the reservation to the actual tab viewport width. On Android
562
+ and iOS, only the native tab viewport shrinks from `W` to `W - R`: its frame
563
+ begins at zero in LTR and at `R` in RTL. The sticky React header, its height,
564
+ and page content keep their original full-width geometry. Touches in the
565
+ reserved region reach the sibling control, while native tab hit testing and
566
+ last-tab visibility use the reduced viewport. Changing the reservation updates
567
+ that viewport; removing it or recycling the pager restores the default zero.
568
+ Web does not render the native tab and ignores this style field. For a positive
569
+ reservation, the box-none sticky React control draws above the native tab so
570
+ its inward fade and toolbar hit area remain visible; zero preserves the old
571
+ native-tab foreground order. Acceptance requires focused native layout and hit
572
+ testing for zero, nonzero, reset, and RTL reservations; type forwarding alone
573
+ is insufficient.
574
+
475
575
  ## Android refresh foreground coordination
476
576
 
477
577
  Status: implemented; Android Home simulator placement verified on 2026-09-28.
@@ -18,6 +18,7 @@
18
18
 
19
19
  // OneKey patch: deduplicate structural mutation parents before scanning the page.
20
20
  #include <unordered_set>
21
+ #include <cmath>
21
22
 
22
23
  using namespace facebook::react;
23
24
 
@@ -368,7 +369,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
368
369
  @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
369
370
  - (void)updateItemsJSON:(NSString *)itemsJSON;
370
371
  - (void)updateStyleWithHeight:(CGFloat)height
372
+ variant:(NSString *)variant
371
373
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
374
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
372
375
  itemSpacing:(CGFloat)itemSpacing
373
376
  fontSize:(CGFloat)fontSize
374
377
  fontFamily:(NSString *)fontFamily
@@ -385,12 +388,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
385
388
 
386
389
  @implementation RNCCollapsiblePagerNativeTabBarView {
387
390
  RNCCollapsiblePagerHorizontalScrollView *_scrollView;
391
+ UIView *_trackView;
388
392
  UIView *_indicatorView;
389
393
  NSArray<NSDictionary *> *_items;
390
394
  NSMutableArray<UIButton *> *_buttons;
391
395
  NSMutableArray<NSValue *> *_indicatorFrames;
392
396
  CGFloat _barHeight;
393
397
  CGFloat _contentPaddingHorizontal;
398
+ CGFloat _contentPaddingVertical;
399
+ BOOL _segmented;
394
400
  CGFloat _itemSpacing;
395
401
  CGFloat _fontSize;
396
402
  NSString *_fontFamily;
@@ -413,6 +419,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
413
419
  _indicatorFrames = [NSMutableArray new];
414
420
  _barHeight = 44;
415
421
  _contentPaddingHorizontal = 20;
422
+ _contentPaddingVertical = 0;
423
+ _segmented = NO;
416
424
  _itemSpacing = 8;
417
425
  _fontSize = 16;
418
426
  _activeTextColor = UIColor.labelColor;
@@ -434,6 +442,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
434
442
  _scrollView.delegate = self;
435
443
  [self addSubview:_scrollView];
436
444
 
445
+ _trackView = [UIView new];
446
+ _trackView.userInteractionEnabled = NO;
447
+ [_scrollView addSubview:_trackView];
448
+
437
449
  _indicatorView = [UIView new];
438
450
  _indicatorView.userInteractionEnabled = NO;
439
451
  [_scrollView addSubview:_indicatorView];
@@ -488,7 +500,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
488
500
  }
489
501
 
490
502
  - (void)updateStyleWithHeight:(CGFloat)height
503
+ variant:(NSString *)variant
491
504
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
505
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
492
506
  itemSpacing:(CGFloat)itemSpacing
493
507
  fontSize:(CGFloat)fontSize
494
508
  fontFamily:(NSString *)fontFamily
@@ -500,19 +514,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
500
514
  indicatorBottom:(CGFloat)indicatorBottom
501
515
  {
502
516
  _barHeight = MAX(1, height);
517
+ BOOL wasSegmented = _segmented;
518
+ _segmented = [variant isEqualToString:@"segmented"];
503
519
  _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
520
+ _contentPaddingVertical = MAX(0, contentPaddingVertical);
504
521
  _itemSpacing = MAX(0, itemSpacing);
505
522
  _fontSize = MAX(1, fontSize);
506
523
  _fontFamily = [fontFamily copy];
507
- self.backgroundColor = backgroundColor ?: UIColor.clearColor;
524
+ self.backgroundColor = _segmented ? UIColor.clearColor : (backgroundColor ?: UIColor.clearColor);
508
525
  _scrollView.backgroundColor = self.backgroundColor;
526
+ _scrollView.scrollEnabled = !_segmented;
527
+ if (_segmented && !wasSegmented) [_scrollView setContentOffset:CGPointZero animated:NO];
528
+ _trackView.hidden = !_segmented;
529
+ _trackView.backgroundColor = backgroundColor ?: UIColor.clearColor;
509
530
  _activeTextColor = activeTextColor ?: UIColor.labelColor;
510
531
  _inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
511
532
  _indicatorColor = indicatorColor ?: _activeTextColor;
512
533
  _indicatorHeight = MAX(0, indicatorHeight);
513
534
  _indicatorBottom = MAX(0, indicatorBottom);
514
535
  _indicatorView.backgroundColor = _indicatorColor;
515
- _indicatorView.layer.cornerRadius = _indicatorHeight / 2;
536
+ _indicatorView.layer.cornerRadius = _segmented ? 0 : _indicatorHeight / 2;
516
537
  for (UIButton *button in _buttons) button.titleLabel.font = self.tabFont;
517
538
  [self setNeedsLayout];
518
539
  }
@@ -571,6 +592,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
571
592
  {
572
593
  [super layoutSubviews];
573
594
  _scrollView.frame = self.bounds;
595
+ if (_segmented) {
596
+ CGFloat width = CGRectGetWidth(self.bounds);
597
+ CGFloat height = CGRectGetHeight(self.bounds);
598
+ CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
599
+ CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
600
+ CGRect trackFrame = CGRectMake(insetX, insetY,
601
+ MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
602
+ _trackView.frame = trackFrame;
603
+ _trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
604
+ _scrollView.contentSize = self.bounds.size;
605
+ [_indicatorFrames removeAllObjects];
606
+ NSUInteger count = _buttons.count;
607
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
608
+ CGFloat first = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * index / count;
609
+ CGFloat last = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * (index + 1) / count;
610
+ CGFloat x = self->_leftToRight ? first : CGRectGetMaxX(trackFrame) - (last - CGRectGetMinX(trackFrame));
611
+ CGFloat end = self->_leftToRight ? last : CGRectGetMaxX(trackFrame) - (first - CGRectGetMinX(trackFrame));
612
+ button.frame = CGRectMake(x, CGRectGetMinY(trackFrame), MAX(0, end - x), CGRectGetHeight(trackFrame));
613
+ CGRect thumbFrame = CGRectMake(x + 2, CGRectGetMinY(trackFrame) + 2,
614
+ MAX(0, CGRectGetWidth(button.frame) - 4), MAX(0, CGRectGetHeight(trackFrame) - 4));
615
+ [self->_indicatorFrames addObject:[NSValue valueWithCGRect:thumbFrame]];
616
+ }];
617
+ _indicatorView.layer.cornerRadius = MAX(0, CGRectGetHeight(trackFrame) - 4) / 2;
618
+ [self updatePresentation];
619
+ return;
620
+ }
621
+ _trackView.frame = CGRectZero;
574
622
  UIFont *font = self.tabFont;
575
623
  NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
576
624
  CGFloat itemsWidth = 0;
@@ -634,10 +682,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
634
682
  CGRectGetMinX(fromFrame) + (CGRectGetMinX(toFrame) - CGRectGetMinX(fromFrame)) * fraction,
635
683
  CGRectGetMinY(fromFrame),
636
684
  CGRectGetWidth(fromFrame) + (CGRectGetWidth(toFrame) - CGRectGetWidth(fromFrame)) * fraction,
637
- _indicatorHeight
685
+ _segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
638
686
  );
639
687
  _indicatorView.frame = indicatorFrame;
640
- _indicatorView.hidden = _indicatorHeight <= 0;
688
+ _indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
641
689
 
642
690
  NSInteger selectedIndex = (NSInteger)round(progress);
643
691
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
@@ -654,7 +702,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
654
702
  : UIAccessibilityTraitButton;
655
703
  }];
656
704
 
657
- if (_shouldCenterSelectedItem && !_scrollView.tracking && !_scrollView.dragging &&
705
+ if (_shouldCenterSelectedItem && !_segmented && !_scrollView.tracking && !_scrollView.dragging &&
658
706
  !_scrollView.decelerating && CGRectGetWidth(_scrollView.bounds) > 0) {
659
707
  CGFloat maximumOffset = MAX(0, _scrollView.contentSize.width - CGRectGetWidth(_scrollView.bounds));
660
708
  CGFloat desiredOffset = RNCClamp(
@@ -1471,6 +1519,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1471
1519
  - (void)liftSharedHeadersForPagerTransition:(NSString *)reason;
1472
1520
  - (void)restoreSharedHeadersToContainer;
1473
1521
  - (void)layoutSharedHeaders;
1522
+ - (void)bringReservedStickyControlToFront;
1474
1523
  - (void)restorePinnedStickyHeader;
1475
1524
  - (void)updatePinnedStickyHeader;
1476
1525
  - (UIScrollView *)currentVerticalScrollViewForParentPager;
@@ -1558,6 +1607,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1558
1607
  NSUInteger _pendingTopInsetReboundGeneration;
1559
1608
  NSTimer *_topInsetReboundTimer;
1560
1609
  CGFloat _nativeTabBarHeight;
1610
+ CGFloat _nativeTabBarTrailingReservedWidth;
1561
1611
  CGFloat _nativeSubHeaderHeight;
1562
1612
  CGFloat _headerOffset;
1563
1613
  BOOL _scrollEnabled;
@@ -2479,6 +2529,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2479
2529
  }
2480
2530
  [_nativeTabBarView updateAncestorGesturePrecedence];
2481
2531
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2532
+ [self bringReservedStickyControlToFront];
2482
2533
  }
2483
2534
 
2484
2535
  - (void)restoreSharedHeadersToContainer
@@ -2584,10 +2635,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2584
2635
  CGFloat nativeTabBarVisibleHeight = _nativeTabBarView.hidden
2585
2636
  ? 0
2586
2637
  : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2638
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2587
2639
  _nativeTabBarView.frame = CGRectMake(
2588
- 0,
2640
+ self.isLtrLayout ? 0 : tabReservedWidth,
2589
2641
  stickyChildY,
2590
- width,
2642
+ width - tabReservedWidth,
2591
2643
  nativeTabBarVisibleHeight
2592
2644
  );
2593
2645
  _nativeSubHeaderView.frame = CGRectMake(
@@ -2601,9 +2653,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2601
2653
  );
2602
2654
  _topOverlayHostView.frame = CGRectMake(0, 0, width, self.effectiveTopOverlayHeight);
2603
2655
  _topOverlayView.frame = _topOverlayHostView.bounds;
2656
+ [self bringReservedStickyControlToFront];
2604
2657
  [self applyHeaderOffset];
2605
2658
  }
2606
2659
 
2660
+ - (void)bringReservedStickyControlToFront
2661
+ {
2662
+ if (_stickyHeaderView != nil &&
2663
+ _stickyHeaderView.superview == _nativeTabBarView.superview) {
2664
+ if (_nativeTabBarTrailingReservedWidth > 0) {
2665
+ [_stickyHeaderView.superview bringSubviewToFront:_stickyHeaderView];
2666
+ } else {
2667
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2668
+ if (!_nativeSubHeaderView.hidden &&
2669
+ _nativeSubHeaderView.superview == _nativeTabBarView.superview) {
2670
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2671
+ }
2672
+ }
2673
+ }
2674
+ }
2675
+
2607
2676
  - (void)updatePinnedStickyHeader
2608
2677
  {
2609
2678
  BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
@@ -2650,8 +2719,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2650
2719
  CGFloat childY = shouldPin ? 0 : _headerHeight;
2651
2720
  CGFloat width = CGRectGetWidth(self.bounds);
2652
2721
  CGFloat tabHeight = _nativeTabBarView.hidden ? 0 : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2722
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2653
2723
  _stickyHeaderView.frame = CGRectMake(0, childY, width, _stickyHeaderHeight);
2654
- _nativeTabBarView.frame = CGRectMake(0, childY, width, tabHeight);
2724
+ _nativeTabBarView.frame = CGRectMake(self.isLtrLayout ? 0 : tabReservedWidth,
2725
+ childY, width - tabReservedWidth, tabHeight);
2655
2726
  _nativeSubHeaderView.frame = CGRectMake(0, childY + tabHeight, width,
2656
2727
  MIN(MAX(0, _stickyHeaderHeight - tabHeight), _nativeSubHeaderHeight));
2657
2728
  _pinnedStickyHostView.hidden = !shouldPin;
@@ -2671,6 +2742,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2671
2742
  }
2672
2743
  [_nativeTabBarView updateAncestorGesturePrecedence];
2673
2744
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2745
+ [self bringReservedStickyControlToFront];
2674
2746
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2675
2747
  [self logHomeVisualGeometry:shouldPin ? @"pin" : @"unpin"
2676
2748
  scrollView:_observedScrollView];
@@ -3050,6 +3122,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3050
3122
  if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
3051
3123
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
3052
3124
  if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3125
+ [self bringReservedStickyControlToFront];
3053
3126
  [_containerView bringSubviewToFront:_sharedHeaderHostView];
3054
3127
  if (_stickyHeaderIsPinned) {
3055
3128
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
@@ -3262,6 +3335,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3262
3335
  _homeVisualLastLayoutLog = 0;
3263
3336
  _homeVisualLastGeometrySignature = nil;
3264
3337
  _nativeTabBarHeight = 44;
3338
+ _nativeTabBarTrailingReservedWidth = 0;
3265
3339
  _nativeSubHeaderHeight = 74;
3266
3340
  _headerOffset = 0;
3267
3341
  _currentLogicalOffset = 0;
@@ -3343,6 +3417,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3343
3417
  _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
3344
3418
  [_nativeTabBarView setLeftToRight:self.isLtrLayout];
3345
3419
  [_nativeSubHeaderView setLeftToRight:self.isLtrLayout];
3420
+ [self setNeedsLayout];
3421
+ }
3422
+ if (_needsPropsReapply ||
3423
+ oldViewProps.nativeTabBarTrailingReservedWidth != newViewProps.nativeTabBarTrailingReservedWidth) {
3424
+ double reserved = newViewProps.nativeTabBarTrailingReservedWidth;
3425
+ _nativeTabBarTrailingReservedWidth = std::isfinite(reserved) && reserved >= 0 ? reserved : 0;
3426
+ [self setNeedsLayout];
3346
3427
  }
3347
3428
  BOOL insetsChanged = NO;
3348
3429
  if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
@@ -3423,7 +3504,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3423
3504
  }
3424
3505
  if (_needsPropsReapply ||
3425
3506
  oldViewProps.nativeTabBarHeight != newViewProps.nativeTabBarHeight ||
3507
+ oldViewProps.nativeTabBarVariant != newViewProps.nativeTabBarVariant ||
3426
3508
  oldViewProps.nativeTabBarContentPaddingHorizontal != newViewProps.nativeTabBarContentPaddingHorizontal ||
3509
+ oldViewProps.nativeTabBarContentPaddingVertical != newViewProps.nativeTabBarContentPaddingVertical ||
3427
3510
  oldViewProps.nativeTabBarItemSpacing != newViewProps.nativeTabBarItemSpacing ||
3428
3511
  oldViewProps.nativeTabBarFontSize != newViewProps.nativeTabBarFontSize ||
3429
3512
  oldViewProps.nativeTabBarFontFamily != newViewProps.nativeTabBarFontFamily ||
@@ -3440,7 +3523,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3440
3523
  NSString *fontFamily = RCTNSStringFromString(newViewProps.nativeTabBarFontFamily);
3441
3524
  [_nativeTabBarView
3442
3525
  updateStyleWithHeight:_nativeTabBarHeight
3526
+ variant:RCTNSStringFromString(newViewProps.nativeTabBarVariant)
3443
3527
  contentPaddingHorizontal:MAX(0, newViewProps.nativeTabBarContentPaddingHorizontal)
3528
+ contentPaddingVertical:MAX(0, newViewProps.nativeTabBarContentPaddingVertical)
3444
3529
  itemSpacing:MAX(0, newViewProps.nativeTabBarItemSpacing)
3445
3530
  fontSize:newViewProps.nativeTabBarFontSize > 0 ? newViewProps.nativeTabBarFontSize : 16
3446
3531
  fontFamily:fontFamily
@@ -3474,6 +3559,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3474
3559
  if (!_nativeSubHeaderView.hidden) {
3475
3560
  [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
3476
3561
  }
3562
+ [self bringReservedStickyControlToFront];
3477
3563
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3478
3564
  if (_stickyHeaderIsPinned) {
3479
3565
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
@@ -4438,8 +4524,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4438
4524
 
4439
4525
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
4440
4526
  {
4441
- if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
4442
- [(RNCNativeScrollerComponentView *)scrollView.superview setPagerStickyHeight:height forOwner:self];
4527
+ id host = scrollView.superview;
4528
+ if ([host respondsToSelector:@selector(setPagerStickyHeight:forOwner:)]) {
4529
+ [host setPagerStickyHeight:height forOwner:self];
4443
4530
  }
4444
4531
  }
4445
4532
 
@@ -5233,6 +5320,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5233
5320
  {
5234
5321
  UIScrollView *scrollView = _pagerTopScrollView;
5235
5322
  if (_dispatchingPagerTop || ![self matchesPagerTop:scrollView]) return;
5323
+ CGFloat clampedTarget = MIN(MAX(_pagerTopLogicalTarget, 0), _headerHeight);
5324
+ if (clampedTarget != _pagerTopLogicalTarget) {
5325
+ _pagerTopLogicalTarget = clampedTarget;
5326
+ CGFloat physicalTarget = RNCLimitRestoredScrollOffset(clampedTarget - scrollView.contentInset.top,
5327
+ scrollView.contentSize.height, scrollView.bounds.size.height, scrollView.contentInset.top, scrollView.contentInset.bottom);
5328
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:NO];
5329
+ }
5236
5330
  CGFloat logical = scrollView.contentOffset.y + scrollView.contentInset.top;
5237
5331
  if (fabs(logical - _pagerTopLogicalTarget) > 0.5) return;
5238
5332
  _pageOffsets[_pagerTopPageKey] = @(_nativeSmoothHeaderScrollEnabled ? 0 : _pagerTopLogicalTarget);
@@ -5244,11 +5338,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5244
5338
  }
5245
5339
 
5246
5340
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
5341
+ {
5342
+ if (target != 0 && target != 1) return;
5343
+ [self scrollHeaderToOffset:(target == 1 ? _headerHeight : 0) animated:animated];
5344
+ }
5345
+
5346
+ - (void)scrollHeaderBy:(double)delta
5347
+ {
5348
+ if (!std::isfinite(delta)) return;
5349
+ [self scrollHeaderToOffset:MIN(MAX(_headerOffset + delta, 0), _headerHeight) animated:NO];
5350
+ }
5351
+
5352
+ - (void)scrollHeaderToOffset:(double)offset
5353
+ {
5354
+ if (!std::isfinite(offset)) return;
5355
+ [self scrollHeaderToOffset:MIN(MAX(offset, 0), _headerHeight) animated:NO];
5356
+ }
5357
+
5358
+ - (void)scrollHeaderToOffset:(CGFloat)logicalTarget animated:(BOOL)animated
5247
5359
  {
5248
5360
  [self cancelTopInsetReboundCheck];
5249
5361
  UIScrollView *scrollView = _observedScrollView;
5250
5362
  NSString *key = _observedPageKey;
5251
- if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
5363
+ if (_isBeingRecycled || _transitioning || _isPagerDragging ||
5252
5364
  _transitionPageOffsets != nil || _syntheticPageRestore || scrollView == nil || scrollView.window == nil ||
5253
5365
  key == nil || ![key isEqualToString:self.currentPageKey] || _observedPageIndex != _currentIndex ||
5254
5366
  _observedPageGeneration != _generation || self.window == nil) return;
@@ -5264,7 +5376,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5264
5376
  [_alignedBodyKeys removeObject:key];
5265
5377
  [_suspendedBodyKeys removeObject:key];
5266
5378
  [_displayBodyOverrides removeObjectForKey:key];
5267
- CGFloat logicalTarget = target == 1 ? _headerHeight : 0;
5379
+ logicalTarget = MIN(MAX(logicalTarget, 0), _headerHeight);
5268
5380
  _pageOffsets[key] = @(_nativeSmoothHeaderScrollEnabled ? 0 : logicalTarget);
5269
5381
  _pagerTopScrollView = scrollView;
5270
5382
  _pagerTopPageKey = [key copy];