@onekeyfe/react-native-pager-view 3.0.157-alpha.256 → 3.0.157-alpha.257

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.
@@ -9,6 +9,7 @@ import android.view.ViewConfiguration
9
9
  import android.view.ViewGroup
10
10
  import android.view.ViewTreeObserver
11
11
  import android.widget.HorizontalScrollView
12
+ import android.widget.ImageView
12
13
  import android.widget.ScrollView
13
14
  // OneKey patch: FrameLayout is inherited through NestedScrollableHost.
14
15
  // import android.widget.FrameLayout
@@ -17,11 +18,14 @@ import androidx.core.view.NestedScrollingParentHelper
17
18
  import androidx.core.view.ViewCompat
18
19
  import androidx.recyclerview.widget.LinearLayoutManager
19
20
  import androidx.recyclerview.widget.RecyclerView
21
+ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
20
22
  import androidx.viewpager2.widget.ViewPager2
21
23
  import com.facebook.react.R as ReactR
22
24
  import com.facebook.react.uimanager.events.NativeGestureUtil
23
25
  import com.facebook.react.views.scroll.ReactScrollView
26
+ import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
24
27
  import com.margelo.nitro.nativelogger.OneKeyLog
28
+ import java.lang.ref.WeakReference
25
29
  import java.util.WeakHashMap
26
30
  import kotlin.math.max
27
31
  import kotlin.math.min
@@ -116,6 +120,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
116
120
  private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
117
121
  private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
118
122
  private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
123
+ private val scrollerRefreshIndicators = WeakHashMap<ScrollView, WeakReference<View>>()
119
124
  private val pageOffsets = HashMap<String, Int>()
120
125
  private var observedRecyclerView: RecyclerView? = null
121
126
  private var observedScrollListener: RecyclerView.OnScrollListener? = null
@@ -604,6 +609,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
604
609
  }
605
610
 
606
611
  private fun prepareAdjacentPages() {
612
+ // Retained pages and replaced controls may leave the attached view tree.
613
+ for ((scroller, indicator) in scrollerRefreshIndicators.toMap()) {
614
+ if (nativeScrollerRefreshIndicator(scroller) !== indicator.get()) {
615
+ restoreNativeScrollerRefreshIndicator(scroller)
616
+ }
617
+ }
607
618
  val first = max(0, selectedPage - 1)
608
619
  val last = min(adapter.itemCount - 1, selectedPage + 1)
609
620
  for (index in first..last) {
@@ -794,6 +805,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
794
805
  updateScrollerPaddingOwnership(scroller, original)
795
806
  val topInset = headerHeightPx + stickyHeaderHeightPx
796
807
  scroller.clipToPadding = false
808
+ applyNativeScrollerRefreshIndicator(scroller, topInset)
797
809
  if (scroller is ReactScrollView) {
798
810
  // Fabric owns the content child's layout, so ScrollView padding alone
799
811
  // cannot move it. RN's native ScrollAway API also updates Fabric culling.
@@ -896,6 +908,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
896
908
  }
897
909
 
898
910
  private fun restoreNativeScrollerInsets(scroller: ScrollView) {
911
+ restoreNativeScrollerRefreshIndicator(scroller)
899
912
  val original = originalScrollerPadding.remove(scroller) ?: return
900
913
  updateScrollerPaddingOwnership(scroller, original)
901
914
  restoredScrollerKeys.remove(scroller)
@@ -910,6 +923,44 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
910
923
  scroller.setPadding(original.left, original.top, original.right, original.bottom)
911
924
  }
912
925
 
926
+ private fun nativeScrollerRefreshIndicator(scroller: ScrollView): View? {
927
+ if (!nativeSmoothHeaderScrollEnabled) return null
928
+ var ancestor = scroller.parent
929
+ while (ancestor != null && ancestor !== this) ancestor = ancestor.parent
930
+ if (ancestor !== this) return null
931
+ val refresh = scroller.parent as? ReactSwipeRefreshLayout ?: return null
932
+ for (index in 0 until refresh.childCount) {
933
+ val child = refresh.getChildAt(index)
934
+ if (child is ImageView && child.drawable is CircularProgressDrawable) return child
935
+ }
936
+ return null
937
+ }
938
+
939
+ private fun applyNativeScrollerRefreshIndicator(scroller: ScrollView, inset: Int) {
940
+ val indicator = nativeScrollerRefreshIndicator(scroller)
941
+ if (scrollerRefreshIndicators[scroller]?.get() !== indicator) {
942
+ restoreNativeScrollerRefreshIndicator(scroller)
943
+ }
944
+ if (indicator == null) return
945
+ if (scrollerRefreshIndicators[scroller]?.get() !== indicator) {
946
+ scrollerRefreshIndicators[scroller] = WeakReference(indicator)
947
+ }
948
+ val translation = refreshIndicatorTranslations.getOrPut(indicator) {
949
+ NativeScrollerRefreshTranslation(indicator.translationY)
950
+ }
951
+ // Moving only the drawing preserves RN's offsets and AndroidX's active pull/refresh animation.
952
+ // setProgressViewOffset would reset that animation and clear isRefreshing.
953
+ val next = translation.update(indicator.translationY, this, inset.toFloat())
954
+ if (indicator.translationY != next) indicator.translationY = next
955
+ }
956
+
957
+ private fun restoreNativeScrollerRefreshIndicator(scroller: ScrollView) {
958
+ val indicator = scrollerRefreshIndicators.remove(scroller)?.get() ?: return
959
+ val translation = refreshIndicatorTranslations[indicator] ?: return
960
+ indicator.translationY = translation.update(indicator.translationY, this, null)
961
+ if (translation.isEmpty) refreshIndicatorTranslations.remove(indicator)
962
+ }
963
+
913
964
  private fun headerRegionAt(y: Float): String? {
914
965
  val headerBottom = headerHeightPx - headerOffsetPx
915
966
  val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
@@ -1232,6 +1283,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1232
1283
  }
1233
1284
 
1234
1285
  companion object {
1286
+ private val refreshIndicatorTranslations = WeakHashMap<View, NativeScrollerRefreshTranslation>()
1235
1287
  const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
1236
1288
  const val PAGE_SLOT_OFFSET = 2
1237
1289
  }
@@ -1,5 +1,7 @@
1
1
  package com.reactnativepagerview
2
2
 
3
+ import java.util.WeakHashMap
4
+
3
5
  // Replace only this pager's contribution; another native owner may share the scroller.
4
6
  internal fun nativeScrollerScrollAwayTop(currentTop: Int, previousInset: Int, nextInset: Int): Int =
5
7
  (currentTop - previousInset).coerceAtLeast(0) + nextInset
@@ -15,3 +17,21 @@ internal fun nativeScrollerPaddingWasResetByScrollAway(
15
17
  scrollAwayBottom: Int,
16
18
  ): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
17
19
  bottom == scrollAwayTop + scrollAwayBottom
20
+
21
+ internal class NativeScrollerRefreshTranslation(initialTranslation: Float) {
22
+ private val contributions = WeakHashMap<Any, Float>()
23
+ private var baseline = initialTranslation
24
+ private var lastApplied = initialTranslation
25
+
26
+ val isEmpty: Boolean
27
+ get() = contributions.isEmpty()
28
+
29
+ fun update(currentTranslation: Float, owner: Any, inset: Float?): Float {
30
+ // Native refresh layout owns top/scale, while an external translation is
31
+ // an absolute baseline rather than another copy of our previous inset.
32
+ if (currentTranslation != lastApplied) baseline = currentTranslation
33
+ if (inset == null) contributions.remove(owner) else contributions[owner] = inset
34
+ lastApplied = baseline + contributions.values.sum()
35
+ return lastApplied
36
+ }
37
+ }
@@ -34,4 +34,62 @@ fun main() {
34
34
  // Initial discovery must capture caller values before this pager owns an inset.
35
35
  check(!nativeScrollerPaddingWasResetByScrollAway(0, 0, 0, 0, 230, 230, 0))
36
36
  println("NativeScroller ScrollAway ownership: initial, repeat, measured resize, nested detach, Fabric reset, RN padding replay, and caller updates passed")
37
+
38
+ val firstOwner = Any()
39
+ val secondOwner = Any()
40
+ val translation = NativeScrollerRefreshTranslation(7.5f)
41
+ check(translation.isEmpty)
42
+ var current = translation.update(7.5f, firstOwner, 230f)
43
+ check(current == 237.5f)
44
+ check(!translation.isEmpty)
45
+ current = translation.update(current, firstOwner, 230f)
46
+ check(current == 237.5f)
47
+ current = translation.update(current, firstOwner, 358f)
48
+ check(current == 365.5f)
49
+ current = translation.update(current, secondOwner, 64f)
50
+ check(current == 429.5f)
51
+ current = translation.update(current, firstOwner, null)
52
+ check(current == 71.5f)
53
+ check(!translation.isEmpty)
54
+ current = translation.update(current, secondOwner, null)
55
+ check(current == 7.5f)
56
+ check(translation.isEmpty)
57
+
58
+ // Reversing release order must restore the same original translation.
59
+ current = translation.update(current, firstOwner, 230f)
60
+ current = translation.update(current, secondOwner, 64f)
61
+ current = translation.update(current, secondOwner, null)
62
+ check(current == 237.5f)
63
+ current = translation.update(current, firstOwner, null)
64
+ check(current == 7.5f)
65
+ check(translation.isEmpty)
66
+
67
+ // A caller's absolute translation update replaces the baseline, not its inset.
68
+ current = translation.update(current, firstOwner, 230f)
69
+ check(current == 237.5f)
70
+ current = translation.update(-12.5f, firstOwner, 230f)
71
+ check(current == 217.5f)
72
+ current = translation.update(current, secondOwner, 64f)
73
+ check(current == 281.5f)
74
+ current = translation.update(current, firstOwner, null)
75
+ check(current == 51.5f)
76
+ current = translation.update(current, secondOwner, null)
77
+ check(current == -12.5f)
78
+ check(translation.isEmpty)
79
+
80
+ // Zero is a real owned contribution; only null releases ownership.
81
+ current = translation.update(current, firstOwner, 0f)
82
+ check(current == -12.5f)
83
+ check(!translation.isEmpty)
84
+ current = translation.update(current, secondOwner, 0f)
85
+ current = translation.update(current, firstOwner, null)
86
+ check(current == -12.5f)
87
+ check(!translation.isEmpty)
88
+ current = translation.update(current, secondOwner, null)
89
+ check(current == -12.5f)
90
+ check(translation.isEmpty)
91
+ current = translation.update(9f, firstOwner, null)
92
+ check(current == 9f)
93
+ check(translation.isEmpty)
94
+ println("NativeScroller refresh translation: repeat, resize, nested release orders, external baseline, zero contribution, and final restoration passed")
37
95
  }
package/docs/SPEC.md CHANGED
@@ -3,6 +3,40 @@
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
+ ## Android NativeScroller refresh indicator placement
7
+
8
+ Status: implemented; Android API 36 Home pull-gesture visibility verified on
9
+ 2026-09-28. Spot, Perps and DeFi (RN NativeScroller), plus the unchanged NFT and
10
+ History (NativeList), each pass header-origin and content-origin pulls. Ten
11
+ recordings show a visible indicator below the shared tabs, one refresh event and
12
+ normal disappearance. This verifies UI feedback and event dispatch, not
13
+ downstream request completion. Dynamic caller offset, refresh-time header resize
14
+ and nested-pager replacement remain separate device acceptance work.
15
+ In smooth-header mode, each pager contributes its header-plus-sticky height in
16
+ physical pixels to the RN NativeScroller refresh indicator's `translationY`.
17
+ The coordinator identifies only the refresh layout's direct indicator child;
18
+ it MUST NOT translate the content ScrollView or change refresh offsets, bounds,
19
+ scale, animation, visibility, trigger distance, refreshing state or callbacks.
20
+ RN retains ownership of `progressViewOffset`, including dynamic caller updates.
21
+ NativeList keeps its existing independent refresh-indicator positioning.
22
+
23
+ One state per indicator combines weak owner contributions on the UI thread.
24
+ Repeated application and header resizing replace only the calling owner's
25
+ contribution. An external translation different from the last applied value
26
+ becomes the new baseline. Removing an owner preserves the remaining contributions;
27
+ removing the final owner restores the latest baseline. A zero contribution still
28
+ has an owner until explicitly released. State MUST NOT strongly retain an owner
29
+ or the indicator. Late mounting, indicator replacement and scroller/pager detach
30
+ use the existing native layout lifecycle; there is no JavaScript frame callback.
31
+
32
+ The standalone JVM policy test covers nonzero baselines, repeated application,
33
+ resize, nested owners removed in either order, external translation updates,
34
+ zero contributions and final restoration. It does not prove Android view
35
+ discovery, actual refresh state or rendered placement. Runtime acceptance requires
36
+ header-origin and content-origin pulls on NativeScroller pages, caller offset
37
+ changes, refresh-time layout changes, replacement/detach, and visible disappearance
38
+ after completion; existing NativeList pages must remain unchanged.
39
+
6
40
  ## iOS refresh indicator placement
7
41
 
8
42
  Status: source implementation; real refresh-cycle acceptance pending.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.157-alpha.256",
3
+ "version": "3.0.157-alpha.257",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",