@onekeyfe/react-native-pager-view 3.0.173 → 3.0.175

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.
@@ -43,6 +43,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
43
43
  )
44
44
  host.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
45
45
  override fun onPageScrolled(position: Int, offset: Float, offsetPixels: Int) {
46
+ host.onPagerScrolled(position, offset)
46
47
  dispatch(reactContext, host, PageScrollEvent(host.id, position, offset))
47
48
  }
48
49
 
@@ -55,6 +56,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
55
56
  }
56
57
 
57
58
  override fun onPageScrollStateChanged(state: Int) {
59
+ host.onPagerScrollStateChanged(state)
58
60
  val value = when (state) {
59
61
  ViewPager2.SCROLL_STATE_IDLE -> "idle"
60
62
  ViewPager2.SCROLL_STATE_DRAGGING -> "dragging"
@@ -92,6 +94,9 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
92
94
  ),
93
95
  )
94
96
  }
97
+ host.onHeaderSettledStateChanged = {
98
+ dispatchDiagnostics(reactContext, host, "header-settled-state")
99
+ }
95
100
  host.onHeaderOffsetChanged = {
96
101
  if (!host.pager.isFakeDragging && host.pager.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
97
102
  dispatchDiagnostics(reactContext, host, "vertical-idle")
@@ -125,6 +130,8 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
125
130
  host.observedScrollableCount(),
126
131
  host.retainedPages,
127
132
  reason,
133
+ host.headerStatePageKey(),
134
+ host.headerSettledAtTop,
128
135
  ),
129
136
  )
130
137
  }
@@ -322,15 +329,25 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
322
329
  delegate.receiveCommand(root, commandId, args)
323
330
  }
324
331
 
332
+ override fun scrollToTop(view: CollapsiblePagerHost?, target: Int, animated: Boolean) {
333
+ view?.scrollToTop(target, animated)
334
+ }
335
+
325
336
  override fun setPage(view: CollapsiblePagerHost?, selectedPage: Int) {
326
337
  if (view != null && selectedPage in 0 until view.adapter.itemCount) {
338
+ view.beginPageTransition(selectedPage)
327
339
  view.pager.setCurrentItem(selectedPage, true)
340
+ if (view.pager.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
341
+ view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
342
+ }
328
343
  }
329
344
  }
330
345
 
331
346
  override fun setPageWithoutAnimation(view: CollapsiblePagerHost?, selectedPage: Int) {
332
347
  if (view != null && selectedPage in 0 until view.adapter.itemCount) {
348
+ view.beginPageTransition(selectedPage)
333
349
  view.pager.setCurrentItem(selectedPage, false)
350
+ view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
334
351
  }
335
352
  }
336
353
 
@@ -255,6 +255,8 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
255
255
 
256
256
  internal fun cancelPendingScrollRestore() = pendingRestore.cancel()
257
257
 
258
+ internal fun hasPendingScrollRestore(): Boolean = pendingRestore.target != null
259
+
258
260
  internal fun setRefreshOffset(value: Int) {
259
261
  refreshOffset = value
260
262
  applyRefreshOffset()
@@ -293,11 +295,22 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
293
295
  }
294
296
 
295
297
  internal fun scrollToPosition(x: Int, y: Int, animated: Boolean) {
298
+ notifyScrollCommand(0, y <= 0)
296
299
  viewport.stopMotion()
297
300
  pendingRestore.cancel()
301
+ notifyScrollCommand(1, y <= 0)
298
302
  if (animated) viewport.scrollToAnimated(y)
299
303
  else viewport.scrollTo(0, y.coerceAtLeast(0))
300
304
  emitScroll(force = true)
305
+ if (!viewport.isReadingMotionActive()) notifyScrollCommand(2, y <= 0)
306
+ }
307
+
308
+ internal fun notifyScrollCommand(phase: Int, explicitTop: Boolean = false) {
309
+ var owner = parent
310
+ while (owner is View) {
311
+ if (owner is CollapsiblePagerHost) owner.acceptObservedScrollCommand(viewport, explicitTop, phase)
312
+ owner = owner.parent
313
+ }
301
314
  }
302
315
 
303
316
  internal fun scrollToEnd(animated: Boolean) = scrollToPosition(
@@ -587,6 +600,8 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
587
600
  super.fling(velocityY)
588
601
  }
589
602
 
603
+ internal fun isReadingMotionActive() = dragging || momentum
604
+
590
605
  internal fun scrollToAnimated(y: Int) {
591
606
  val child = getChildAt(0) ?: return
592
607
  val margins = child.layoutParams as FrameLayout.LayoutParams
@@ -637,6 +652,7 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
637
652
  private fun finishMomentum() {
638
653
  if (!momentum) return
639
654
  momentum = false
655
+ host.notifyScrollCommand(2)
640
656
  host.emitScroll(force = true)
641
657
  host.emitLifecycle("topMomentumScrollEnd")
642
658
  }
@@ -14,6 +14,8 @@ class CollapsibleStateChangedEvent(
14
14
  private val observedScrollableCount: Int,
15
15
  private val retainedPages: String,
16
16
  private val reason: String,
17
+ private val pageKey: String,
18
+ private val headerSettledAtTop: Boolean,
17
19
  ) : Event<CollapsibleStateChangedEvent>(viewTag) {
18
20
  override fun getEventName() = EVENT_NAME
19
21
 
@@ -31,6 +33,8 @@ class CollapsibleStateChangedEvent(
31
33
  putInt("observedScrollableCount", observedScrollableCount)
32
34
  putString("retainedPages", retainedPages)
33
35
  putString("reason", reason)
36
+ putString("pageKey", pageKey)
37
+ putBoolean("headerSettledAtTop", headerSettledAtTop)
34
38
  }
35
39
 
36
40
  companion object {
@@ -2,4 +2,6 @@
2
2
  <item name="onekey_native_scroll_coordinator_consumed_offsets" type="id" />
3
3
  <item name="onekey_native_scroll_coordinator_offset_listener" type="id" />
4
4
  <item name="onekey_native_scroll_coordinator_refresh_inset" type="id" />
5
+ <item name="onekey_native_scroll_coordinator_command_listener" type="id" />
6
+ <item name="onekey_native_scroll_coordinator_command_cancel_listener" type="id" />
5
7
  </resources>
@@ -27,5 +27,16 @@ fun main() {
27
27
  restore.cancel()
28
28
  check(restore.target == null)
29
29
  check(restore.anchor(120) == 120)
30
+ // Cancellation clears the old target without requiring the reached offset to change.
31
+ restore.request(target = 640, reached = 120)
32
+ check(restore.anchor(120) == 640)
33
+ restore.cancel()
34
+ check(restore.target == null)
35
+ check(restore.anchor(120) == 120)
36
+ // A fresh request owns its target after cancellation.
37
+ restore.request(target = 320, reached = 120)
38
+ check(restore.target == 320)
39
+ restore.settle(reached = 320)
40
+ check(restore.target == null)
30
41
  println("Native scroller pending restore: clamp retry, partial growth, settle and cancel passed")
31
42
  }
package/docs/SPEC.md CHANGED
@@ -3,6 +3,60 @@
3
3
  Existing PagerView and CollapsiblePagerView APIs are documented in README.md.
4
4
  The sections below define native coordination boundaries without new React props.
5
5
 
6
+ ## Smooth shared-header and per-page body memory
7
+
8
+ With nativeSmoothHeaderScrollEnabled, the header collapse C is shared across all
9
+ pages and the available collapse distance is D. Each page separately remembers
10
+ its sticky-body reading distance B (unvisited pages default to zero). Outside
11
+ pull overscroll, the rendered whole-page distance is Q = C + effectiveB.
12
+ B denotes the saved reading position. For an owned vertical LinearLayoutManager
13
+ RecyclerView, restoration uses the first visible adapter position and its
14
+ decorated top relative to padding; scrollbar offset estimates MUST NOT decide
15
+ completion of that anchor restoration.
16
+ When C = D, selection restores the destination's saved B. When C < D, effectiveB
17
+ is zero while the previous saved B remains suspended. Horizontal selection by
18
+ tap, swipe or an existing programmatic command MUST preserve C throughout.
19
+
20
+ Alignment, prefetch, replacement, inset changes and pending restoration MUST NOT
21
+ commit new B. Actual consumed vertical scrolling takes over the visible body
22
+ position; reaching full collapse during that gesture MUST NOT resurrect the old
23
+ suspended B. Selecting a page anew at full collapse may restore its saved B.
24
+ Valid explicit native scroll commands, including same-offset commands, cancel
25
+ only their current owned restoration and take over body memory. Invalid commands
26
+ and stale owners do not affect another page. Existing command coordinate systems
27
+ are unchanged; in particular NativeScroller iOS uses physical offsets whereas
28
+ NativeList offset commands exclude the content-start inset.
29
+
30
+ Transitions use immutable destination memory and the source's actual displayed
31
+ position. Cancellation restores that source presentation, even when a cached B
32
+ was temporarily unreachable. Loading or zero geometry preserves pending intent;
33
+ a genuinely short page clamps its display without silently replacing saved B.
34
+ Cancelling a horizontal gesture invalidates queued callbacks, but preserves
35
+ unreachable reading intent until real vertical consumption or a valid command
36
+ takes over; later usable geometry may resume it.
37
+ Late callbacks cannot revive cancelled restoration or erase a newer request.
38
+ Observers and commands must match page key, view identity and generation.
39
+ Removed or recycled owners cannot apply callbacks to another page; recycling
40
+ resets owned memory.
41
+ Legacy mode and pagers without a collapsible header retain their existing
42
+ behavior. Vertical scrolling, refresh, snapping and repeated-current-tab behavior
43
+ are unchanged. This state belongs to native UI instances, with no new React prop
44
+ or per-frame JavaScript synchronization.
45
+
46
+ ## Current-page top commands
47
+
48
+ `scrollToPageTop(options?: Readonly<{ animated?: boolean }>)` restores the current
49
+ page to C = 0 and B = 0. `scrollToStickyTop(options?: Readonly<{ animated?: boolean }>)`
50
+ restores it to C = D and B = 0, including from a partially expanded header.
51
+ Animation defaults to true. Neither command clears another page's memory.
52
+ Both commands require a stable, attached current owner; horizontal transitions
53
+ or missing owners reject them rather than queueing a command for another page.
54
+ Acceptance cancels older owned list commands and pending restoration before the
55
+ new target is established. Same-target commands also take over old reading
56
+ intent. Real vertical consumption, a newer valid command, or owner detachment
57
+ releases the target. Existing NativeList and NativeScroller public coordinates
58
+ are unchanged. The same page-top/sticky-top destinations are available without smooth-header mode.
59
+
6
60
  ## Independent NativeScroller migration
7
61
 
8
62
  Status: implemented in source. Both native builds pass. Android focused emulator
@@ -454,7 +508,8 @@ When restoring a retained page's cached vertical offset, the pager limits only
454
508
  the upper bound to the current content height, viewport and applied insets. A
455
509
  cache captured before rows were removed MUST NOT restore an offset below the new
456
510
  content bottom, including when removal occurs during a page transition. Valid
457
- deep offsets and the existing lower-bound/header alignment rules are unchanged.
511
+ deep offsets and pull-overscroll behavior are preserved; smooth-header selection
512
+ follows the shared-header and per-page body-memory contract above.
458
513
  This check runs only on explicit restoration, not on scroll frames; UIKit pull
459
514
  overscroll and refresh settlement retain their existing ownership. An unmeasured
460
515
  zero-height viewport defers this upper-bound limit to a later restoration.
@@ -662,3 +717,12 @@ cases additionally verified recorded refresh, header, restoration and gesture
662
717
  paths; they do not establish every possible sequence or runtime acceptance of a
663
718
  newly installed module release. Physical-device feel and semantic accessibility
664
719
  activation remain unverified.
720
+
721
+
722
+ ### Native Header stability snapshot
723
+
724
+ `onCollapsibleStateChanged` retains its existing diagnostic fields and reasons and adds native-only `pageKey: string` and `headerSettledAtTop: boolean`. The key is the actual mounted React child key, not an application tab identifier. Unattached, unknown, replaced or transitioning owners MUST report false. A mounted current owner emits an initial identity snapshot without requiring a first user scroll.
725
+
726
+ The Header is settled only when C is at its expanded top, the current attached view/key/index/generation still matches, and there is no actual drag, deceleration, scrolling command or horizontal transition. Offset/inset/content/header geometry MUST remain stable for 150ms before true is emitted. Motion or geometry changes revoke true promptly. Deferred reading memory alone MUST NOT be treated as active motion. Pull-to-refresh may settle at a normalized negative offset after drag, bounce and inset geometry stop; network refreshing alone is not a motion gate. Checks stop for collapsed, detached and recycled owners; delayed callbacks revalidate their captured identity. State changes are deduplicated; identity changes still publish an unknown/false snapshot.
727
+
728
+ The native Home Spotlight adapter consumes this snapshot in the main UI runtime and keeps unknown states paused. It MUST use the mounted page-key mapping and reject old page/source callbacks. Native snapshots already include the 150ms stabilization delay: consumers MUST NOT add another such delay. Legacy Tabs keep their existing normalized-offset and debounce behavior. Anchor measurement, guide qualification and the existing display delay remain independent gates.