@onekeyfe/react-native-pager-view 3.0.157 → 3.0.159

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 (42) hide show
  1. package/README.md +57 -4
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +379 -31
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
  4. package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
  5. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +41 -0
  6. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
  8. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  9. package/android/src/main/jni/CMakeLists.txt +19 -0
  10. package/android/src/main/jni/pagerview.h +11 -0
  11. package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
  12. package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
  13. package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
  14. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +75 -0
  15. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
  16. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
  17. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
  18. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
  19. package/docs/SPEC.md +451 -0
  20. package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
  21. package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
  22. package/ios/RNCCollapsiblePagerViewComponentView.mm +420 -2
  23. package/ios/RNCNativeScrollerComponentView.h +17 -0
  24. package/ios/RNCNativeScrollerComponentView.mm +479 -0
  25. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
  26. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +788 -0
  27. package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
  28. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +214 -0
  29. package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
  30. package/ios/tests/RNCNativeScrollerTests.mm +252 -0
  31. package/lib/module/CollapsiblePagerView.js +10 -1
  32. package/lib/module/NativeScroller.js +171 -11
  33. package/lib/module/NativeScrollerNativeComponent.ts +101 -0
  34. package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
  35. package/lib/typescript/src/NativeScroller.d.ts +15 -7
  36. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
  37. package/package.json +8 -4
  38. package/react-native-pager-view.podspec +2 -1
  39. package/react-native.config.js +15 -0
  40. package/src/CollapsiblePagerView.tsx +14 -4
  41. package/src/NativeScroller.tsx +291 -14
  42. package/src/NativeScrollerNativeComponent.ts +101 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,451 @@
1
+ # PagerView native scrolling contract
2
+
3
+ Existing PagerView and CollapsiblePagerView APIs are documented in README.md.
4
+ The sections below define native coordination boundaries without new React props.
5
+
6
+ ## Independent NativeScroller migration
7
+
8
+ Status: implemented in source. Both native builds pass. Android focused emulator
9
+ cases below and the iOS same-source UIKit harness pass; the full acceptance
10
+ matrix remains incomplete. Device evidence is scoped below.
11
+ NativeScroller keeps React children and their Fabric/Yoga layout, but owns its
12
+ vertical native scrolling implementation: AndroidX NestedScrollView on Android
13
+ and UIScrollView on iOS. It MUST NOT inherit ReactScrollView/RCTScrollView or
14
+ require rebuilding ReactAndroid. Web retains its existing implementation.
15
+
16
+ The initial compatibility contract covers existing Home callers: viewport style,
17
+ contentContainerStyle including padding and flexGrow, dynamically mounted React
18
+ children, vertical scroll enablement/indicator, stable pagerScrollKey, scroll
19
+ throttling, onScroll/onScrollBeginDrag/onScrollEndDrag and momentum callbacks,
20
+ content-size notification, and scrollTo/scrollToEnd commands. Standard RN
21
+ RefreshControl JSX remains a supported input; its props and onRefresh callback
22
+ are adapted to owned native refresh controls rather than mounting an RN-private
23
+ scroll-view implementation. Supported properties and intentional platform limits
24
+ MUST be explicit in the exported types. Unknown RN ScrollView features MUST NOT
25
+ be silently advertised as supported. No business data or Home-specific geometry
26
+ belongs in the native component.
27
+
28
+ This is a vertical, non-virtualized subset, not a drop-in implementation of every
29
+ ScrollView prop. Horizontal scrolling, sticky rows, snapping, zoom and automatic
30
+ keyboard insets are outside this contract. iOS owns bounces and decelerationRate;
31
+ Android retains AndroidX fling physics without a public friction override.
32
+ Keyboard persist-tap behavior is coordinated by the JS responder wrapper using
33
+ RN 0.86's TextInput registry, while native owns drag-to-dismiss. The RN-private
34
+ registry dependency and keyboard behavior need validation on RN upgrades.
35
+ Arbitrary custom refresh components are not supported.
36
+
37
+ Native views, gestures, insets and refresh controllers belong to the UI thread.
38
+ React content layout remains Fabric-owned. Layout-size updates may cross the
39
+ bridge when dimensions change; native scrolling and header coordination MUST NOT
40
+ depend on per-frame JS updates. This is a main/UI component, with no background
41
+ runtime, persistent storage or cross-runtime heap state.
42
+
43
+ The wrapper mounts one non-collapsible React content View. Native MUST preserve
44
+ its Fabric-assigned frame and transform. A shared Fabric ShadowNode reads native
45
+ state describing local content displacement and separate ancestor displacement;
46
+ it MUST apply the ancestor displacement outside the content transform. UIKit
47
+ updates this state from contentOffset; Android updates it from content origin,
48
+ scroll position and pager consumption. Equal values are deduplicated, and a
49
+ replacement state wrapper is synchronized with the current native position.
50
+ These native state updates do not require per-frame JavaScript callbacks.
51
+ Fabric hit testing and descendant measureInWindow MUST include the displacement,
52
+ so measurement follows visible content after scroll, inset changes and page
53
+ return. Geometry acceptance MUST compare a measured React child with its visible
54
+ position, including the last child after scrollToEnd.
55
+
56
+ The automatic flexGrow minimum MUST use the native usable content viewport,
57
+ not the raw host height. An internal onContentViewportChange({height}) event
58
+ reports logical units only when static geometry changes; it is not a public
59
+ NativeScroller prop. Android reports viewport height minus caller/pager top
60
+ reservation and caller bottom inset. Its pager viewport already includes the
61
+ extra collapsible-header height. iOS reports bounds height minus caller top and
62
+ bottom insets and the sum of pager-owned sticky reservations. Both describe the
63
+ React area available with the shared header collapsed. The height MUST NOT
64
+ depend on contentSize, current header offset, refresh temporary insets or iOS
65
+ synthetic bottom fill, which would create a layout feedback loop. Owners MUST
66
+ remove their reservation on release. The current value MUST be delivered when
67
+ the view first becomes able to dispatch its event and after reattachment. An
68
+ event queued before its initial emitter is ready MUST be delivered when that
69
+ emitter becomes available. Replacing the emitter on the same attached view does
70
+ not require another event when the usable height is unchanged.
71
+
72
+ Before the first native viewport event, the wrapper contributes no positive
73
+ automatic minimum. It combines valid native height with an explicit numeric
74
+ caller minHeight using the greater value; nonnumeric explicit minima remain
75
+ untouched. Content padding remains part of Yoga layout. Long-to-short replacement
76
+ MUST clamp the local offset without leaving a pager-inset-sized artificial scroll
77
+ range or hiding the first React child. Tests and device acceptance MUST cover
78
+ long-to-empty/restore, page return, header/sticky resizing, refresh completion
79
+ without minimum-height changes, and an intentionally larger caller minimum.
80
+
81
+ Android MUST dispatch touch and non-touch nested scrolling continuously across
82
+ the content/header boundary. Content returns to its start before downward travel
83
+ expands the header; upward travel collapses the header before scrolling content.
84
+ Consumed and remaining distance MUST be accounted for once. Only touch pull can
85
+ trigger refresh; inertial movement MUST NOT trigger refresh. iOS MUST preserve a
86
+ single UIScrollView trajectory through the inset/header range. Existing Pager
87
+ refresh foreground placement and shared-header press cancellation remain in
88
+ force. NativeList and unrelated RN ScrollViews retain their implementations.
89
+
90
+ New touch, page deactivation, replacement of the mounted React content root and
91
+ detach MUST stop stale inertia. Every started drag/momentum sequence has one
92
+ terminal notification. An animated scrollTo/scrollToEnd command with a different
93
+ clamped target emits one momentum begin/end pair, including AndroidX's immediate
94
+ execution branch for closely spaced commands. A target equal to the current
95
+ position emits no new pair. A new command ends any previous active sequence
96
+ before starting its own, and non-animated movement does not start momentum;
97
+ recycled views MUST NOT emit into an old owner. Content shrink clamps unreachable
98
+ offsets without a blank tail; resize/inset changes MUST NOT accumulate offsets.
99
+ On Android, a pager offset restoration that is clamped because React content has
100
+ not reached native yet is reapplied on content layout until reached; a new touch,
101
+ drag, scroll command, content-root replacement or pager release cancels it.
102
+ Stopping an idle scroller MUST NOT start or end a non-touch nested scroll.
103
+ Refresh is controlled by the caller and emits once per qualifying gesture.
104
+ Android applies a refreshing value when it changes (or at first layout); later
105
+ layout passes do not reassert it over a gesture-started indicator.
106
+
107
+ Acceptance requires both native builds, focused contract checks and iOS/Android
108
+ recordings: expanded/collapsed Home Spot/Perps/DeFi, short/empty and dynamically
109
+ resized content, fast return through the header boundary, new-touch interruption,
110
+ adjacent/distant page changes, horizontal gestures versus row presses, pull from
111
+ header/content, refresh completion, retained-page return, imperative
112
+ animation/no-op/interruption, and React child measureInWindow versus visible
113
+ geometry. History/NFT and Market/Discover are regression controls. Source/type/build success alone is not
114
+ device acceptance. Implementation findings and remaining limits are recorded
115
+ after validation, before this section is promoted to runtime verified.
116
+
117
+ Android acceptance checkpoint (2026-09-28): the official local-shell/local-vendor
118
+ DevSession build passed, with custom Fabric descriptor registration verified.
119
+ On the dedicated emulator, native field inspection confirmed NativeScrollerView
120
+ and its AndroidX viewport. Home Spot/Perps/DeFi return gestures crossed the
121
+ content/header boundary. In the final short-fill binary, Perps content offset
122
+ 1389 reached zero and the same non-touch momentum continued header offset
123
+ 755 -> 648 -> 441 -> 217 -> 56 before natural exhaustion. This proves continuity,
124
+ not that every gesture must fully expand the header.
125
+
126
+ The first valid binary exposed an artificial 284 px short-content range. After the
127
+ usable-viewport correction, Gallery long-to-empty replacement had local offset 0
128
+ with short text visible; restoring rows and scrollToEnd reached the final child.
129
+ measureInWindow y746.7/h72.0 matched native visible bounds y1960/h189px at
130
+ pixel density2.625. Short-content refresh advanced the fixture counter once and
131
+ controlled completion removed the indicator. Home three-page refresh visibility,
132
+ History/NFT vertical scrolling and Market/Browser horizontal controls passed on
133
+ the preceding valid binary; these control implementations were unchanged by the
134
+ short-fill correction. Evidence is recorded in the app task's ignored
135
+ `ignore/independent-native-scroller/android-acceptance.md`, alongside screenshots,
136
+ recording chunks and native field samples.
137
+
138
+ This checkpoint does not cover a physical handset, keyboard/multi-touch/accessory
139
+ interactions, exhaustive event counts and command interruption, explicit larger
140
+ caller minHeight, header/sticky resizing or the full refresh matrix. Existing
141
+ development warnings were observed. Android evidence does not establish iOS
142
+ acceptance, and source/build success is not a substitute for its runtime cases.
143
+
144
+ iOS acceptance checkpoint (2026-09-28): the final official local-shell/local-vendor
145
+ build passed and launched on the dedicated external-image simulator. Nine
146
+ same-source UIKit harness methods and 36 assertions pass, including negative-pull
147
+ preservation during shrink, caller inset ownership, static usable height and
148
+ refresh/recycle interruption. This is UIKit execution, not a formal XCTest run.
149
+
150
+ On the final binary, Home Spot/Perps/DeFi horizontal transitions and top-positioned
151
+ refresh feedback passed. Gallery scrollToEnd reached the last React row;
152
+ measureInWindow y786/h72 matched its native frame. Long-to-empty replacement,
153
+ restoring rows, controlled refresh counter 0 -> 1 with indicator dismissal,
154
+ normal row press and retained-page return passed. History/NFT scrolling and
155
+ media, Discover Browser -> DeFi and Market vertical scrolling plus Trending ->
156
+ Stocks horizontal transition passed without unintended detail navigation.
157
+ The final device is left on expanded Home Spot. Evidence is in the app task's
158
+ `ignore/independent-native-scroller/ios-acceptance.md` and associated recordings.
159
+
160
+ These iOS gestures use public XCTest coordinate drags. Earlier private synthesized
161
+ pan attempts had inconsistent timing and are excluded; their cause is not proven.
162
+ Gallery's floating application tab bar overlaps part of the last row, so matching
163
+ coordinates do not establish an unobscured bottom viewport. Existing development
164
+ warnings were observed. Keyboard, multi-touch, physical-device performance and
165
+ the full lifecycle/interaction matrix remain unverified. These are local source
166
+ builds; the currently installed npm version does not yet contain this migration.
167
+
168
+ ## iOS vertical-scroll press cancellation
169
+
170
+ Status: implemented in source. A source-identical `node_modules` build was
171
+ runtime verified on a dedicated iOS 26.5 HomePager simulator on 2026-09-30:
172
+ a slow drag from the Show more button did not activate it, a fling from the
173
+ same button scrolled the list, and a stationary tap activated it. The Pager
174
+ XCTest suite passed 47/47 tests on a separate iOS 26.5 simulator with its data
175
+ on the external drive on 2026-09-30.
176
+
177
+ In `CollapsiblePagerView` smooth-header mode, a single-finger
178
+ vertical drag beginning on React page content MUST cancel that Surface's active
179
+ React press before finger-up once vertical displacement reaches 10 points and
180
+ exceeds horizontal displacement. Cancellation MUST leave the native vertical
181
+ scroller's pan and momentum intact. Stationary taps, movement below the threshold,
182
+ horizontal drags, and multi-touch sequences MUST retain their existing behavior.
183
+ The iOS content press bridge owns cancellation on the UI thread; the pager's
184
+ existing horizontal-drag cancellation remains independent. The bridge MUST clear
185
+ per-touch state on end, cancellation, and reuse. Android and Web behavior is
186
+ unchanged. Acceptance requires a real iOS button-origin drag that does not fire
187
+ the button, a button-origin fling that scrolls, an ordinary tap that fires, and
188
+ focused native touch-delivery coverage.
189
+
190
+ ## Native tab-press animation default
191
+
192
+ Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
193
+ every native tab press. Callers that relied on that default MUST pass `true`.
194
+
195
+ On iOS and Android, an omitted `nativeTabPressAnimationEnabled` MUST animate a
196
+ press to an adjacent page and jump directly to a non-adjacent page. Adjacency is
197
+ the absolute difference between the target index and the current page. While a
198
+ tab command that native has already received is in flight, its target is the
199
+ current page; otherwise it is the selected index. A request superseded before it
200
+ reached native is ignored. Explicit `true` continues to animate all
201
+ tab presses, and explicit `false` continues to jump for all tab presses.
202
+ Page retention, selection callbacks, swipe gestures, imperative `setPage` /
203
+ `setPageWithoutAnimation` commands and Web behavior retain their contracts.
204
+ Callers rendering their own JS tab bar choose the matching imperative command.
205
+
206
+ Status: implemented; focused native-wrapper tests cover both directions,
207
+ explicit overrides and in-flight/superseded command targets. Home applies this policy to its JS tab bar, with iOS and
208
+ Android simulator recordings verifying distant jumps and adjacent transitions.
209
+
210
+ ## Android refresh foreground coordination
211
+
212
+ Status: implemented; Android Home simulator placement verified on 2026-09-28.
213
+ The previous below-tab placement is not accepted: the Home header can place the
214
+ indicator in the middle of asset rows. In smooth-header mode the outermost
215
+ coordinator MUST draw the active primary scroller's native refresh indicator
216
+ above the shared headers at the coordinator's top. SwipeRefreshLayout retains
217
+ trigger, animation, native visibility and refresh-state ownership. The indicator
218
+ MUST NOT be reparented, and the coordinator MUST NOT reset progress offsets.
219
+
220
+ NativeScroller and NativeList use the same foreground drawing boundary. A native
221
+ View tag, `onekey_native_scroll_coordinator_refresh_inset`, reports only the
222
+ refresh controller's actual coordinator-applied inset. An absent tag means zero.
223
+ The outer drawer subtracts that inset once, preserving caller geometry and the
224
+ native indicator matrix. This is native drawing metadata, with no JS API or row
225
+ semantics. Nested owners MUST NOT draw duplicate indicators.
226
+
227
+ The coordinator suppresses only the indicator's original drawing and restores
228
+ its prior alpha when ownership ends, the target changes, or the pager detaches.
229
+ An inactive page MUST NOT appear in the foreground. The foreground must clear
230
+ when the indicator disappears. Native animation transformations MUST NOT be
231
+ advanced a second time during drawing. Focused policy checks and device evidence
232
+ must cover alpha restoration, nested ownership, page changes and dismissal.
233
+ All five Home tabs require top-positioned feedback and stable header position
234
+ through pull, release and refresh completion. Source/compile success is not
235
+ runtime acceptance. Existing iOS/Web behavior is unchanged.
236
+
237
+ The five Home tabs were recorded from both header and content origins: the
238
+ foreground arc appears at the coordinator top and dismisses normally. The fast
239
+ release reversal that previously changed header offset from 0 to 767 px now
240
+ consumes the pending pull fling and keeps the header expanded. These recordings
241
+ verify visual feedback, not downstream network completion or new callback counts.
242
+ Production-helper tests cover alpha restoration and combined native insets;
243
+ real nested-owner replacement, dynamic caller geometry and detach remain separate
244
+ acceptance work. Direct foreground View drawing preserves the native circle and
245
+ arc but does not reproduce the original RenderNode elevation shadow.
246
+
247
+ ## Android refresh release settlement
248
+
249
+ Status: implemented; the captured release reversal passes a device A/B check.
250
+ A downward touch gesture that leaves unconsumed travel for the active
251
+ SwipeRefreshLayout is a pending native refresh pull. While that same gesture's
252
+ indicator is visible, the shared header is expanded, and primary content remains
253
+ at the top, the coordinator MUST consume nested pre-fling on release. A small
254
+ upward reversal before lifting must settle the pull instead of starting a
255
+ content fling and abruptly collapsing the header. SwipeRefreshLayout still owns
256
+ whether the release triggers or cancels refresh.
257
+
258
+ The pending target is weak, scoped to the current touch nested-scroll gesture,
259
+ and cleared at its stop or replacement. An old indicator's exit animation alone
260
+ is not a pending pull. Ordinary upward swipes, content scrolled away from the
261
+ top, inactive pages and non-smooth mode retain existing fling behavior. A pull
262
+ whose indicator is already hidden no longer qualifies. AndroidX can leave the
263
+ indicator visible when reverse travel exactly exhausts the pull; that same
264
+ gesture may still be settled instead of flung. Required regression cases include immediate reversal release,
265
+ a stationary release, below-threshold cancellation, ordinary upward fling and
266
+ header expansion before refresh. No JS timeout or data-refresh suppression is
267
+ introduced.
268
+
269
+ ## Android downward scroll ownership
270
+
271
+ Status: implemented; Android Home History runtime verified on 2026-09-28.
272
+ Downward scrolling MUST move the active content back to its top before expanding
273
+ the shared header. A refresh wrapper forwarding nested scrolling MUST report its
274
+ child's ability to scroll upward; the wrapper's own scroll range is not evidence
275
+ that the content is at the top. SwipeRefreshLayout's public canChildScrollUp()
276
+ contract, including caller overrides, supplies this check. Other nested-scroll
277
+ targets retain their own canScrollVertically(-1) check.
278
+
279
+ The correction changes only the downward pre-scroll eligibility check. Existing
280
+ delta consumption, content-observer alignment, refresh ownership and release
281
+ settlement remain unchanged. Runtime comparison captured premature expansion
282
+ with content offset 1960 px before the fix. Afterward the header stayed collapsed
283
+ while content returned to zero, and expanded only at the top, without an observer
284
+ forced collapse. Debugger state traces prove ownership, not frame timing;
285
+ recordings with the debugger detached cover History rapid direction reversal
286
+ and top refresh.
287
+ Spot's scroll-then-refresh release-reversal sequence still jumps in both the
288
+ baseline and corrected builds; that existing NativeScroller case remains open.
289
+
290
+ ## Superseded Android indicator placement
291
+
292
+ The alpha.257 approach translated the RN indicator below the shared tabs.
293
+ Its ten Home checks established visibility and event dispatch only. Subsequent
294
+ 2026-09-28 feedback showed the indicator over asset rows; those checks do not
295
+ establish correct placement. The foreground coordination contract above replaces
296
+ that translation policy. NativeList keeps its controller offset internally and
297
+ publishes only its actual native inset for foreground drawing.
298
+
299
+ ## iOS refresh indicator placement
300
+
301
+ Status: implemented; iOS Home pull-gesture visibility verified on 2026-09-27.
302
+ Spot, Perps and DeFi (RN NativeScroller), plus NFT and History (NativeList),
303
+ each pass header-origin and content-origin pulls: ten recordings show a visible
304
+ indicator above the shared header, one refresh event and normal disappearance.
305
+ This verifies UI feedback and event dispatch, not downstream request completion.
306
+ The Pager UIKit suite passes 14 tests, including six refresh-control cases
307
+ (native-tests run 36318243869). Rapid nested gestures and iPad geometry remain
308
+ separate acceptance work.
309
+ In smooth-header mode, the pager shifts the refresh control's sublayers upward
310
+ by its header-plus-sticky height. It does not change the refresh control frame,
311
+ bounds, scroll offset, refresh state or JavaScript callback. RN's caller-owned
312
+ `progressViewOffset` remains untouched, including dynamic updates whose bounds
313
+ origin could otherwise collide with a coordinator-applied value.
314
+
315
+ One internal state per UIRefreshControl combines weak pager-owner contributions.
316
+ It preserves the original sublayer transform and clipping setting. Repeated
317
+ attachment and header resizing replace an owner's contribution. Layer clipping
318
+ is disabled while coordinating so it cannot hide the translated indicator.
319
+ A control replacement, scroll-view release, pager recycling or destruction
320
+ removes only that owner's contribution and restores the baseline when none remain.
321
+ Controls installed after the scroller is attached are also coordinated. Native
322
+ UICollectionView controls and RN ScrollView controls use this same UIKit boundary.
323
+
324
+ Native layer observations reapply the contribution after an external sublayer
325
+ transform change, without observing or interpreting UIKit/RN bounds writes.
326
+ There is no per-frame JavaScript work. Acceptance must cover real UIKit pulling
327
+ and refresh completion, caller offset changes, late installation/replacement,
328
+ retained-page switching, clipping and nested-owner cleanup. Programmatic UIKit
329
+ unit tests do not substitute for real pull-gesture visibility acceptance.
330
+
331
+ When a smooth shared header is attached to the active iOS scroll view, UIKit's
332
+ temporary refresh top inset MUST NOT add a second downward displacement to that
333
+ header. The pager uses its own applied top inset as the baseline and cancels only
334
+ the refresh-added visual displacement. The scroll view's inset and offset, the
335
+ refresh indicator, and refresh callback ownership remain unchanged. Removing the
336
+ refresh inset or control reapplies the header position immediately, even without
337
+ a scroll-offset event. Caller inset changes update the pager baseline before the
338
+ header position is reapplied. Detaching the header restores its normal
339
+ transform. This applies to RN NativeScroller and NativeList pages; non-smooth
340
+ and detached headers keep their existing positioning. The 2026-09-30 Home Spot
341
+ simulator checkpoint covers
342
+ an active refresh with repeated vertical swipes and subsequent inset removal;
343
+ other Home pages and physical-device acceptance remain open.
344
+
345
+ ## iOS shared-header hit testing
346
+
347
+ Smooth-header hit forwarding is restricted to the pager's own interactive
348
+ viewport. A collapsed header clipped above that viewport MUST NOT intercept
349
+ touches intended for a sibling toolbar. Hidden, disabled and effectively
350
+ transparent pagers do not forward touches. Fabric `pointerEvents="none"` and
351
+ `"box-only"` retain their superclass behavior; `"auto"` and `"box-none"` may
352
+ forward to visible shared-header children inside the pager.
353
+
354
+ The shared header's internal hit testing still supports sticky children that
355
+ have moved outside the header host's original bounds while remaining inside the
356
+ pager. Focused UIKit tests cover sibling-toolbar hit delivery, pinned-child
357
+ delivery, pointer-event modes and interaction flags. Device acceptance includes
358
+ toolbar taps with expanded and collapsed headers on retained pages.
359
+
360
+ ## iOS cached-offset restoration after content shrink
361
+
362
+ When restoring a retained page's cached vertical offset, the pager limits only
363
+ the upper bound to the current content height, viewport and applied insets. A
364
+ cache captured before rows were removed MUST NOT restore an offset below the new
365
+ content bottom, including when removal occurs during a page transition. Valid
366
+ deep offsets and the existing lower-bound/header alignment rules are unchanged.
367
+ This check runs only on explicit restoration, not on scroll frames; UIKit pull
368
+ overscroll and refresh settlement retain their existing ownership. An unmeasured
369
+ zero-height viewport defers this upper-bound limit to a later restoration.
370
+
371
+ The focused policy test covers shrink after the list has already corrected its
372
+ offset, valid deep offsets, negative pull offsets and an unmeasured viewport.
373
+ Runtime acceptance requires clearing a retained page during a page transition,
374
+ then returning without an extra drag to expose its header/empty/footer slots.
375
+
376
+
377
+ ## Ancestor consumed-scroll contributions
378
+
379
+ Status: source implementation; real nested-pager runtime verification pending.
380
+ Android collapsible pagers consume header travel before the primary RecyclerView
381
+ receives content-scroll deltas. A leaf threshold observer needs both distances.
382
+ The coordinator publishes a non-negative physical-pixel contribution under its
383
+ own weak owner identity on the active primary RecyclerView. Nested coordinators
384
+ contribute independently; summing their values gives ancestor-consumed travel.
385
+
386
+ The native View tag named `onekey_native_scroll_coordinator_consumed_offsets`
387
+ stores a WeakHashMap with owner-view keys and integer pixel values. The optional
388
+ `onekey_native_scroll_coordinator_offset_listener` tag holds a Runnable owned by
389
+ the leaf. The publisher updates only its own entry and invokes the listener only
390
+ when that entry changes. An absent map/listener means zero/no observer.
391
+ This protocol depends only on Android View/resources and Java collections; the
392
+ pager MUST NOT depend on NativeList or infer Home/business semantics.
393
+
394
+ On switching the primary child, restoring insets, or detaching, the coordinator
395
+ removes only its own contribution. It MUST NOT retain an old RecyclerView or
396
+ another coordinator. Zero contributions may remain while the owner is attached;
397
+ weak keys prevent abandoned owners from becoming resource roots. The leaf owns
398
+ listener registration and cleanup. All operations occur on the UI thread; there
399
+ are no JavaScript scroll-frame callbacks in this protocol.
400
+
401
+ A new child receives current header travel before threshold evaluation. Native
402
+ list offsets remain local to the leaf, while consumers of this protocol add all
403
+ ancestor contributions exactly once. Programmatic scrolling/header restoration
404
+ must publish the resulting header offset through the same path.
405
+ Every NativeScroller viewport the coordinator owns, including adjacent pages it
406
+ translates, receives each header offset change, not only the observed page.
407
+ Content-offset observers are removed from the observer instance they were
408
+ registered on, and a removed page's scroller stops writing offsets immediately.
409
+
410
+ ## Acceptance
411
+
412
+ The focused JVM helper test covers independent ancestor contributions and removal.
413
+ Required Android runtime cases are one/nested collapsible pagers, header-only
414
+ scrolling, reversal to expanded top, switching pages, native child replacement,
415
+ and detach/remount without stale offsets. iOS uses its existing normalized
416
+ UIScrollView offset and requires no equivalent tag protocol. Source/helper
417
+ checks do not establish the Android runtime acceptance above.
418
+
419
+
420
+ ## Android Fabric NativeScroller content insets
421
+
422
+ A ReactScrollView's content root is laid out by Fabric. Native `setPadding(top)`
423
+ alone MUST NOT be used to reserve a collapsible header: ReactScrollView does not
424
+ perform the platform ScrollView child layout, so that leaves rows under the
425
+ header. For the supported RN 0.86 runtime, the coordinator uses
426
+ `setScrollAwayPaddingEnabledUnstable` to offset the content root and synchronize
427
+ Fabric content-origin/culling state. The equivalent inset is added to the native
428
+ scroll range at the bottom; it is not added a second time at the top. Ordinary
429
+ Android ScrollView and RecyclerView retain their existing inset paths.
430
+
431
+ Each coordinator replaces only its previous header-plus-sticky contribution.
432
+ Measured-height changes do not accumulate insets; detach restores the remaining
433
+ ScrollAway state and caller padding, including another native owner's inset.
434
+ Content-root replacement reapplies the native offset. No Home layout values,
435
+ new React props, or per-frame JS updates are introduced; iOS/Web are unchanged.
436
+
437
+ The focused JVM policy regression covers an existing inset, repeated application,
438
+ measured-header resize, nested-owner detach, and Fabric state reset. The API call
439
+ is compile-checked against the installed React Android 0.86.2 artifact. Runtime
440
+ acceptance requires first content below the expanded sticky tabs, collapse and
441
+ expand without overlap, retained-page switching, header height changes, and
442
+ reaching the actual final content without an extra header-sized blank footer.
443
+
444
+ RN's ScrollAway setter (including native state replay when Fabric culling flags
445
+ are enabled) resets physical padding to `(0, 0, 0, scrollAwayTop + scrollAwayBottom)`.
446
+ While owning an inset, the coordinator recognizes that exact native reset and
447
+ retains saved caller padding; other physical-padding updates remain observable.
448
+ The focused policy regression includes nonzero caller left/right/bottom padding,
449
+ nonzero prior native insets, explicit caller padding changes/clear, and initial
450
+ discovery before ownership. Current Home zero-padding behavior is unchanged by
451
+ this ownership guard.
@@ -9,4 +9,12 @@ FOUNDATION_EXPORT void RNCPruneReleasedPageStates(
9
9
  NSArray<NSString *> *validPageKeys
10
10
  );
11
11
 
12
+ FOUNDATION_EXPORT CGFloat RNCLimitRestoredScrollOffset(
13
+ CGFloat offset,
14
+ CGFloat contentHeight,
15
+ CGFloat viewportHeight,
16
+ CGFloat topInset,
17
+ CGFloat bottomInset
18
+ );
19
+
12
20
  NS_ASSUME_NONNULL_END
@@ -1,5 +1,18 @@
1
1
  #import "RNCCollapsiblePagerReleasedStatePolicy.h"
2
2
 
3
+ CGFloat RNCLimitRestoredScrollOffset(
4
+ CGFloat offset,
5
+ CGFloat contentHeight,
6
+ CGFloat viewportHeight,
7
+ CGFloat topInset,
8
+ CGFloat bottomInset
9
+ )
10
+ {
11
+ if (viewportHeight <= 0) return offset;
12
+ CGFloat maximum = MAX(-topInset, contentHeight - viewportHeight + bottomInset);
13
+ return MIN(offset, maximum);
14
+ }
15
+
3
16
  void RNCPruneReleasedPageStates(
4
17
  NSMutableDictionary *releasedStates,
5
18
  NSArray<NSString *> *validPageKeys