@onekeyfe/react-native-pager-view 6.7.0-alpha.375 → 6.7.0-alpha.382

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
@@ -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
 
@@ -382,7 +383,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
382
383
  @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
383
384
  - (void)updateItemsJSON:(NSString *)itemsJSON;
384
385
  - (void)updateStyleWithHeight:(CGFloat)height
386
+ variant:(NSString *)variant
385
387
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
388
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
386
389
  itemSpacing:(CGFloat)itemSpacing
387
390
  fontSize:(CGFloat)fontSize
388
391
  fontFamily:(NSString *)fontFamily
@@ -399,12 +402,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
399
402
 
400
403
  @implementation RNCCollapsiblePagerNativeTabBarView {
401
404
  RNCCollapsiblePagerHorizontalScrollView *_scrollView;
405
+ UIView *_trackView;
402
406
  UIView *_indicatorView;
403
407
  NSArray<NSDictionary *> *_items;
404
408
  NSMutableArray<UIButton *> *_buttons;
405
409
  NSMutableArray<NSValue *> *_indicatorFrames;
406
410
  CGFloat _barHeight;
407
411
  CGFloat _contentPaddingHorizontal;
412
+ CGFloat _contentPaddingVertical;
413
+ BOOL _segmented;
408
414
  CGFloat _itemSpacing;
409
415
  CGFloat _fontSize;
410
416
  NSString *_fontFamily;
@@ -427,6 +433,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
427
433
  _indicatorFrames = [NSMutableArray new];
428
434
  _barHeight = 44;
429
435
  _contentPaddingHorizontal = 20;
436
+ _contentPaddingVertical = 0;
437
+ _segmented = NO;
430
438
  _itemSpacing = 8;
431
439
  _fontSize = 16;
432
440
  _activeTextColor = UIColor.labelColor;
@@ -448,6 +456,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
448
456
  _scrollView.delegate = self;
449
457
  [self addSubview:_scrollView];
450
458
 
459
+ _trackView = [UIView new];
460
+ _trackView.userInteractionEnabled = NO;
461
+ [_scrollView addSubview:_trackView];
462
+
451
463
  _indicatorView = [UIView new];
452
464
  _indicatorView.userInteractionEnabled = NO;
453
465
  [_scrollView addSubview:_indicatorView];
@@ -502,7 +514,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
502
514
  }
503
515
 
504
516
  - (void)updateStyleWithHeight:(CGFloat)height
517
+ variant:(NSString *)variant
505
518
  contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
519
+ contentPaddingVertical:(CGFloat)contentPaddingVertical
506
520
  itemSpacing:(CGFloat)itemSpacing
507
521
  fontSize:(CGFloat)fontSize
508
522
  fontFamily:(NSString *)fontFamily
@@ -514,19 +528,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
514
528
  indicatorBottom:(CGFloat)indicatorBottom
515
529
  {
516
530
  _barHeight = MAX(1, height);
531
+ BOOL wasSegmented = _segmented;
532
+ _segmented = [variant isEqualToString:@"segmented"];
517
533
  _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
534
+ _contentPaddingVertical = MAX(0, contentPaddingVertical);
518
535
  _itemSpacing = MAX(0, itemSpacing);
519
536
  _fontSize = MAX(1, fontSize);
520
537
  _fontFamily = [fontFamily copy];
521
- self.backgroundColor = backgroundColor ?: UIColor.clearColor;
538
+ self.backgroundColor = _segmented ? UIColor.clearColor : (backgroundColor ?: UIColor.clearColor);
522
539
  _scrollView.backgroundColor = self.backgroundColor;
540
+ _scrollView.scrollEnabled = !_segmented;
541
+ if (_segmented && !wasSegmented) [_scrollView setContentOffset:CGPointZero animated:NO];
542
+ _trackView.hidden = !_segmented;
543
+ _trackView.backgroundColor = backgroundColor ?: UIColor.clearColor;
523
544
  _activeTextColor = activeTextColor ?: UIColor.labelColor;
524
545
  _inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
525
546
  _indicatorColor = indicatorColor ?: _activeTextColor;
526
547
  _indicatorHeight = MAX(0, indicatorHeight);
527
548
  _indicatorBottom = MAX(0, indicatorBottom);
528
549
  _indicatorView.backgroundColor = _indicatorColor;
529
- _indicatorView.layer.cornerRadius = _indicatorHeight / 2;
550
+ _indicatorView.layer.cornerRadius = _segmented ? 0 : _indicatorHeight / 2;
530
551
  for (UIButton *button in _buttons) button.titleLabel.font = self.tabFont;
531
552
  [self setNeedsLayout];
532
553
  }
@@ -585,6 +606,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
585
606
  {
586
607
  [super layoutSubviews];
587
608
  _scrollView.frame = self.bounds;
609
+ if (_segmented) {
610
+ CGFloat width = CGRectGetWidth(self.bounds);
611
+ CGFloat height = CGRectGetHeight(self.bounds);
612
+ CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
613
+ CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
614
+ CGRect trackFrame = CGRectMake(insetX, insetY,
615
+ MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
616
+ _trackView.frame = trackFrame;
617
+ _trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
618
+ _scrollView.contentSize = self.bounds.size;
619
+ [_indicatorFrames removeAllObjects];
620
+ NSUInteger count = _buttons.count;
621
+ [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
622
+ CGFloat first = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * index / count;
623
+ CGFloat last = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * (index + 1) / count;
624
+ CGFloat x = self->_leftToRight ? first : CGRectGetMaxX(trackFrame) - (last - CGRectGetMinX(trackFrame));
625
+ CGFloat end = self->_leftToRight ? last : CGRectGetMaxX(trackFrame) - (first - CGRectGetMinX(trackFrame));
626
+ button.frame = CGRectMake(x, CGRectGetMinY(trackFrame), MAX(0, end - x), CGRectGetHeight(trackFrame));
627
+ CGRect thumbFrame = CGRectMake(x + 2, CGRectGetMinY(trackFrame) + 2,
628
+ MAX(0, CGRectGetWidth(button.frame) - 4), MAX(0, CGRectGetHeight(trackFrame) - 4));
629
+ [self->_indicatorFrames addObject:[NSValue valueWithCGRect:thumbFrame]];
630
+ }];
631
+ _indicatorView.layer.cornerRadius = MAX(0, CGRectGetHeight(trackFrame) - 4) / 2;
632
+ [self updatePresentation];
633
+ return;
634
+ }
635
+ _trackView.frame = CGRectZero;
588
636
  UIFont *font = self.tabFont;
589
637
  NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
590
638
  CGFloat itemsWidth = 0;
@@ -648,10 +696,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
648
696
  CGRectGetMinX(fromFrame) + (CGRectGetMinX(toFrame) - CGRectGetMinX(fromFrame)) * fraction,
649
697
  CGRectGetMinY(fromFrame),
650
698
  CGRectGetWidth(fromFrame) + (CGRectGetWidth(toFrame) - CGRectGetWidth(fromFrame)) * fraction,
651
- _indicatorHeight
699
+ _segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
652
700
  );
653
701
  _indicatorView.frame = indicatorFrame;
654
- _indicatorView.hidden = _indicatorHeight <= 0;
702
+ _indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
655
703
 
656
704
  NSInteger selectedIndex = (NSInteger)round(progress);
657
705
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
@@ -668,7 +716,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
668
716
  : UIAccessibilityTraitButton;
669
717
  }];
670
718
 
671
- if (_shouldCenterSelectedItem && !_scrollView.tracking && !_scrollView.dragging &&
719
+ if (_shouldCenterSelectedItem && !_segmented && !_scrollView.tracking && !_scrollView.dragging &&
672
720
  !_scrollView.decelerating && CGRectGetWidth(_scrollView.bounds) > 0) {
673
721
  CGFloat maximumOffset = MAX(0, _scrollView.contentSize.width - CGRectGetWidth(_scrollView.bounds));
674
722
  CGFloat desiredOffset = RNCClamp(
@@ -1485,6 +1533,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1485
1533
  - (void)liftSharedHeadersForPagerTransition:(NSString *)reason;
1486
1534
  - (void)restoreSharedHeadersToContainer;
1487
1535
  - (void)layoutSharedHeaders;
1536
+ - (void)bringReservedStickyControlToFront;
1488
1537
  - (void)restorePinnedStickyHeader;
1489
1538
  - (void)updatePinnedStickyHeader;
1490
1539
  - (UIScrollView *)currentVerticalScrollViewForParentPager;
@@ -1575,6 +1624,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1575
1624
  NSUInteger _pendingTopInsetReboundGeneration;
1576
1625
  NSTimer *_topInsetReboundTimer;
1577
1626
  CGFloat _nativeTabBarHeight;
1627
+ CGFloat _nativeTabBarTrailingReservedWidth;
1578
1628
  CGFloat _nativeSubHeaderHeight;
1579
1629
  CGFloat _headerOffset;
1580
1630
  BOOL _scrollEnabled;
@@ -2501,6 +2551,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2501
2551
  }
2502
2552
  [_nativeTabBarView updateAncestorGesturePrecedence];
2503
2553
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2554
+ [self bringReservedStickyControlToFront];
2504
2555
  }
2505
2556
 
2506
2557
  - (void)restoreSharedHeadersToContainer
@@ -2606,10 +2657,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2606
2657
  CGFloat nativeTabBarVisibleHeight = _nativeTabBarView.hidden
2607
2658
  ? 0
2608
2659
  : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2660
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2609
2661
  _nativeTabBarView.frame = CGRectMake(
2610
- 0,
2662
+ self.isLtrLayout ? 0 : tabReservedWidth,
2611
2663
  stickyChildY,
2612
- width,
2664
+ width - tabReservedWidth,
2613
2665
  nativeTabBarVisibleHeight
2614
2666
  );
2615
2667
  _nativeSubHeaderView.frame = CGRectMake(
@@ -2623,9 +2675,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2623
2675
  );
2624
2676
  _topOverlayHostView.frame = CGRectMake(0, 0, width, self.effectiveTopOverlayHeight);
2625
2677
  _topOverlayView.frame = _topOverlayHostView.bounds;
2678
+ [self bringReservedStickyControlToFront];
2626
2679
  [self applyHeaderOffset];
2627
2680
  }
2628
2681
 
2682
+ - (void)bringReservedStickyControlToFront
2683
+ {
2684
+ if (_stickyHeaderView != nil &&
2685
+ _stickyHeaderView.superview == _nativeTabBarView.superview) {
2686
+ if (_nativeTabBarTrailingReservedWidth > 0) {
2687
+ [_stickyHeaderView.superview bringSubviewToFront:_stickyHeaderView];
2688
+ } else {
2689
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2690
+ if (!_nativeSubHeaderView.hidden &&
2691
+ _nativeSubHeaderView.superview == _nativeTabBarView.superview) {
2692
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2693
+ }
2694
+ }
2695
+ }
2696
+ }
2697
+
2629
2698
  - (void)updatePinnedStickyHeader
2630
2699
  {
2631
2700
  BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
@@ -2672,8 +2741,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2672
2741
  CGFloat childY = shouldPin ? 0 : _headerHeight;
2673
2742
  CGFloat width = CGRectGetWidth(self.bounds);
2674
2743
  CGFloat tabHeight = _nativeTabBarView.hidden ? 0 : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2744
+ CGFloat tabReservedWidth = MIN(width, MAX(0, _nativeTabBarTrailingReservedWidth));
2675
2745
  _stickyHeaderView.frame = CGRectMake(0, childY, width, _stickyHeaderHeight);
2676
- _nativeTabBarView.frame = CGRectMake(0, childY, width, tabHeight);
2746
+ _nativeTabBarView.frame = CGRectMake(self.isLtrLayout ? 0 : tabReservedWidth,
2747
+ childY, width - tabReservedWidth, tabHeight);
2677
2748
  _nativeSubHeaderView.frame = CGRectMake(0, childY + tabHeight, width,
2678
2749
  MIN(MAX(0, _stickyHeaderHeight - tabHeight), _nativeSubHeaderHeight));
2679
2750
  _pinnedStickyHostView.hidden = !shouldPin;
@@ -2693,6 +2764,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2693
2764
  }
2694
2765
  [_nativeTabBarView updateAncestorGesturePrecedence];
2695
2766
  [_nativeSubHeaderView updateAncestorGesturePrecedence];
2767
+ [self bringReservedStickyControlToFront];
2696
2768
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2697
2769
  [self logHomeVisualGeometry:shouldPin ? @"pin" : @"unpin"
2698
2770
  scrollView:_observedScrollView];
@@ -3088,6 +3160,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3088
3160
  if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
3089
3161
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
3090
3162
  if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3163
+ [self bringReservedStickyControlToFront];
3091
3164
  [_containerView bringSubviewToFront:_sharedHeaderHostView];
3092
3165
  if (_stickyHeaderIsPinned) {
3093
3166
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
@@ -3292,6 +3365,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3292
3365
  _homeVisualLastLayoutLog = 0;
3293
3366
  _homeVisualLastGeometrySignature = nil;
3294
3367
  _nativeTabBarHeight = 44;
3368
+ _nativeTabBarTrailingReservedWidth = 0;
3295
3369
  _nativeSubHeaderHeight = 74;
3296
3370
  _headerOffset = 0;
3297
3371
  _currentLogicalOffset = 0;
@@ -3373,6 +3447,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3373
3447
  _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
3374
3448
  [_nativeTabBarView setLeftToRight:self.isLtrLayout];
3375
3449
  [_nativeSubHeaderView setLeftToRight:self.isLtrLayout];
3450
+ [self setNeedsLayout];
3451
+ }
3452
+ if (_needsPropsReapply ||
3453
+ oldViewProps.nativeTabBarTrailingReservedWidth != newViewProps.nativeTabBarTrailingReservedWidth) {
3454
+ double reserved = newViewProps.nativeTabBarTrailingReservedWidth;
3455
+ _nativeTabBarTrailingReservedWidth = std::isfinite(reserved) && reserved >= 0 ? reserved : 0;
3456
+ [self setNeedsLayout];
3376
3457
  }
3377
3458
  BOOL insetsChanged = NO;
3378
3459
  if (_needsPropsReapply || oldViewProps.headerHeight != newViewProps.headerHeight) {
@@ -3453,7 +3534,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3453
3534
  }
3454
3535
  if (_needsPropsReapply ||
3455
3536
  oldViewProps.nativeTabBarHeight != newViewProps.nativeTabBarHeight ||
3537
+ oldViewProps.nativeTabBarVariant != newViewProps.nativeTabBarVariant ||
3456
3538
  oldViewProps.nativeTabBarContentPaddingHorizontal != newViewProps.nativeTabBarContentPaddingHorizontal ||
3539
+ oldViewProps.nativeTabBarContentPaddingVertical != newViewProps.nativeTabBarContentPaddingVertical ||
3457
3540
  oldViewProps.nativeTabBarItemSpacing != newViewProps.nativeTabBarItemSpacing ||
3458
3541
  oldViewProps.nativeTabBarFontSize != newViewProps.nativeTabBarFontSize ||
3459
3542
  oldViewProps.nativeTabBarFontFamily != newViewProps.nativeTabBarFontFamily ||
@@ -3470,7 +3553,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3470
3553
  NSString *fontFamily = RCTNSStringFromString(newViewProps.nativeTabBarFontFamily);
3471
3554
  [_nativeTabBarView
3472
3555
  updateStyleWithHeight:_nativeTabBarHeight
3556
+ variant:RCTNSStringFromString(newViewProps.nativeTabBarVariant)
3473
3557
  contentPaddingHorizontal:MAX(0, newViewProps.nativeTabBarContentPaddingHorizontal)
3558
+ contentPaddingVertical:MAX(0, newViewProps.nativeTabBarContentPaddingVertical)
3474
3559
  itemSpacing:MAX(0, newViewProps.nativeTabBarItemSpacing)
3475
3560
  fontSize:newViewProps.nativeTabBarFontSize > 0 ? newViewProps.nativeTabBarFontSize : 16
3476
3561
  fontFamily:fontFamily
@@ -3504,6 +3589,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3504
3589
  if (!_nativeSubHeaderView.hidden) {
3505
3590
  [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
3506
3591
  }
3592
+ [self bringReservedStickyControlToFront];
3507
3593
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3508
3594
  if (_stickyHeaderIsPinned) {
3509
3595
  [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
@@ -4581,8 +4667,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4581
4667
 
4582
4668
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
4583
4669
  {
4584
- if ([scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class]) {
4585
- [(RNCNativeScrollerComponentView *)scrollView.superview setPagerStickyHeight:height forOwner:self];
4670
+ id host = scrollView.superview;
4671
+ if ([host respondsToSelector:@selector(setPagerStickyHeight:forOwner:)]) {
4672
+ [host setPagerStickyHeight:height forOwner:self];
4586
4673
  }
4587
4674
  }
4588
4675
 
@@ -5436,6 +5523,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5436
5523
  {
5437
5524
  UIScrollView *scrollView = _pagerTopScrollView;
5438
5525
  if (_dispatchingPagerTop || ![self matchesPagerTop:scrollView]) return;
5526
+ CGFloat clampedTarget = MIN(MAX(_pagerTopLogicalTarget, 0), _headerHeight);
5527
+ if (clampedTarget != _pagerTopLogicalTarget) {
5528
+ _pagerTopLogicalTarget = clampedTarget;
5529
+ CGFloat physicalTarget = RNCLimitRestoredScrollOffset(clampedTarget - scrollView.contentInset.top,
5530
+ scrollView.contentSize.height, scrollView.bounds.size.height, scrollView.contentInset.top, scrollView.contentInset.bottom);
5531
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:NO];
5532
+ }
5439
5533
  CGFloat logical = scrollView.contentOffset.y + scrollView.contentInset.top;
5440
5534
  if (fabs(logical - _pagerTopLogicalTarget) > 0.5) return;
5441
5535
  _pageOffsets[_pagerTopPageKey] = @(_nativeSmoothHeaderScrollEnabled ? 0 : _pagerTopLogicalTarget);
@@ -5447,11 +5541,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5447
5541
  }
5448
5542
 
5449
5543
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
5544
+ {
5545
+ if (target != 0 && target != 1) return;
5546
+ [self scrollHeaderToOffset:(target == 1 ? _headerHeight : 0) animated:animated];
5547
+ }
5548
+
5549
+ - (void)scrollHeaderBy:(double)delta
5550
+ {
5551
+ if (!std::isfinite(delta)) return;
5552
+ [self scrollHeaderToOffset:MIN(MAX(_headerOffset + delta, 0), _headerHeight) animated:NO];
5553
+ }
5554
+
5555
+ - (void)scrollHeaderToOffset:(double)offset
5556
+ {
5557
+ if (!std::isfinite(offset)) return;
5558
+ [self scrollHeaderToOffset:MIN(MAX(offset, 0), _headerHeight) animated:NO];
5559
+ }
5560
+
5561
+ - (void)scrollHeaderToOffset:(CGFloat)logicalTarget animated:(BOOL)animated
5450
5562
  {
5451
5563
  [self cancelTopInsetReboundCheck];
5452
5564
  UIScrollView *scrollView = _observedScrollView;
5453
5565
  NSString *key = _observedPageKey;
5454
- if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
5566
+ if (_isBeingRecycled || _transitioning || _isPagerDragging ||
5455
5567
  _transitionPageOffsets != nil || _syntheticPageRestore || scrollView == nil || scrollView.window == nil ||
5456
5568
  key == nil || ![key isEqualToString:self.currentPageKey] || _observedPageIndex != _currentIndex ||
5457
5569
  _observedPageGeneration != _generation || self.window == nil) return;
@@ -5467,7 +5579,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5467
5579
  [_alignedBodyKeys removeObject:key];
5468
5580
  [_suspendedBodyKeys removeObject:key];
5469
5581
  [_displayBodyOverrides removeObjectForKey:key];
5470
- CGFloat logicalTarget = target == 1 ? _headerHeight : 0;
5582
+ logicalTarget = MIN(MAX(logicalTarget, 0), _headerHeight);
5471
5583
  _pageOffsets[key] = @(_nativeSmoothHeaderScrollEnabled ? 0 : logicalTarget);
5472
5584
  _pagerTopScrollView = scrollView;
5473
5585
  _pagerTopPageKey = [key copy];