@onekeyfe/react-native-pager-view 6.7.0-15 → 6.7.0-17

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.
Files changed (24) hide show
  1. package/README.md +5 -0
  2. package/android/src/androidTest/AndroidManifest.xml +7 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +247 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +76 -21
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
  8. package/docs/SPEC.md +146 -1
  9. package/ios/RNCCollapsiblePagerViewComponentView.mm +355 -40
  10. package/ios/RNCNativeScrollerComponentView.h +2 -0
  11. package/ios/RNCNativeScrollerComponentView.mm +18 -0
  12. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +186 -0
  13. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +178 -0
  14. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
  15. package/lib/module/CollapsiblePagerView.js +20 -4
  16. package/lib/module/CollapsiblePagerView.web.js +11 -2
  17. package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
  18. package/lib/typescript/src/CollapsiblePagerView.d.ts +6 -1
  19. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +2 -0
  20. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
  21. package/package.json +2 -2
  22. package/src/CollapsiblePagerView.tsx +68 -4
  23. package/src/CollapsiblePagerView.web.tsx +19 -2
  24. package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
@@ -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.
@@ -817,6 +917,51 @@ fallback debounce, including replacement-state restoration. Other collection
817
917
  fallbacks keep that safety delay. Caller insets and retained logical offsets
818
918
  remain governed by the existing restoration policy.
819
919
 
920
+ When the active scroll owner is removed before its replacement becomes usable,
921
+ the pager MUST retain bounded recovery (at most 12 retries per recovery round,
922
+ spaced 100 ms apart). Clearing the completed mount's reattachment flag MUST NOT
923
+ cancel that recovery. A replacement that becomes usable during that round,
924
+ without another Fabric insertion, MUST receive the observer, shared headers,
925
+ insets and retained collapse state. Page selection changes and recycling
926
+ invalidate stale retry callbacks. Repeated mount attempts MUST NOT reset the
927
+ retry budget. Once all 12 retries have been scheduled, an Insert whose parent is
928
+ in the current page subtree MUST reopen recovery if that page retains a released
929
+ scroll owner, including when the final retry callback is still pending. Reopening
930
+ invalidates the old callback before clearing its scheduled state. The Insert
931
+ MUST be recorded during a horizontal drag or transition, while observer and shared
932
+ header attachment remain deferred until selection settles. Cancelling the drag
933
+ on the same page MUST retain the reopened recovery round. Empty transactions,
934
+ removals and inserts outside the current page MUST NOT reopen the budget. Owned
935
+ `NativeScroller` and `NativeList` hosts additionally emit push readiness
936
+ notifications when their scroll views attach and obtain positive bounds so late
937
+ replacements recover immediately even without another Fabric insertion or after
938
+ the retry budget expires; static pages with neither an unready scroll host nor a
939
+ released scroll owner do not schedule retry timers.
940
+
941
+ Status of transition-time and pending-final-retry recovery: implemented and
942
+ UIKit regression verified on iOS 26.5 (2026-10-27). Both new regressions fail
943
+ before the fix and pass afterward. Cancellation settles on the source page
944
+ before the replacement becomes usable; the pending-final-retry case makes the
945
+ replacement usable after the old timer's deadline. Observer and shared header
946
+ ownership stay detached during the drag, then recover with the retained collapse.
947
+ The full Pager suite executes 85 tests, with one iOS-27-only skip and no failures.
948
+
949
+ Status of exhausted-budget recovery: implemented and UIKit regression verified
950
+ on iOS 26.5 (2026-10-27). The regression exhausts the initial round, checks that
951
+ empty transactions, removals and offscreen inserts do not reopen it, then inserts
952
+ a current-page replacement whose scroller appears after did-mount. It fails
953
+ before the fix and passes afterward, including preservation of an active round
954
+ and restoration of the observer, shared headers and retained collapse. The full
955
+ Pager suite executes 83 tests, with one iOS-27-only skip and no failures.
956
+
957
+ Status of replacement recovery: implemented and runtime verified on iOS 26.5
958
+ (2026-10-23). The late-replacement UIKit regression fails before the fix and
959
+ passes afterward; the full Pager suite executes 82 tests, with one iOS-27-only
960
+ skip and no failures. In the consuming app based on x at b0789571d8 with SDK
961
+ 6.7.0-9 and this local native change, two empty Favorites-to-Wallet-add-to-Market
962
+ cycles and a Trending/Favorites round trip keep the first row aligned with the
963
+ sticky header. The previously observed 212-point gap is absent.
964
+
820
965
  This is a UI-thread, main-runtime native coordination change with no new props,
821
966
  events, background-runtime work, persistence or retained strong view cache.
822
967
  Android already prepares discovered scroll children before draw and is unchanged;