@onekeyfe/react-native-pager-view 3.0.157-alpha.254 → 3.0.157-alpha.255
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.
- package/README.md +2 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +68 -8
- package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +17 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +4 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +37 -0
- package/docs/SPEC.md +108 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +19 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
- package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|
|
20
20
|
import androidx.viewpager2.widget.ViewPager2
|
|
21
21
|
import com.facebook.react.R as ReactR
|
|
22
22
|
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
23
|
+
import com.facebook.react.views.scroll.ReactScrollView
|
|
23
24
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
24
25
|
import java.util.WeakHashMap
|
|
25
26
|
import kotlin.math.max
|
|
@@ -508,6 +509,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
508
509
|
applyHeaderOffset()
|
|
509
510
|
}
|
|
510
511
|
|
|
512
|
+
private fun publishConsumedOffset(recycler: RecyclerView, remove: Boolean = false) {
|
|
513
|
+
val key = R.id.onekey_native_scroll_coordinator_consumed_offsets
|
|
514
|
+
val existing = recycler.getTag(key) as? NativeScrollCoordinatorContributions
|
|
515
|
+
val contributions = existing ?: if (remove) return else NativeScrollCoordinatorContributions().also {
|
|
516
|
+
recycler.setTag(key, it)
|
|
517
|
+
}
|
|
518
|
+
val changed = if (remove) contributions.clearOwner(this) else contributions.update(this, headerOffsetPx)
|
|
519
|
+
if (changed) {
|
|
520
|
+
(recycler.getTag(R.id.onekey_native_scroll_coordinator_offset_listener) as? Runnable)?.run()
|
|
521
|
+
}
|
|
522
|
+
if (contributions.isEmpty()) recycler.setTag(key, null)
|
|
523
|
+
}
|
|
524
|
+
|
|
511
525
|
private fun applyHeaderOffset() {
|
|
512
526
|
val offset = headerOffsetPx.toFloat()
|
|
513
527
|
pager.translationY = -offset
|
|
@@ -515,6 +529,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
515
529
|
stickyHeaderView?.translationY = -offset
|
|
516
530
|
nativeTabBarView.translationY = -offset
|
|
517
531
|
nativeSubHeaderView.translationY = -offset
|
|
532
|
+
observedRecyclerView?.let { publishConsumedOffset(it) }
|
|
518
533
|
}
|
|
519
534
|
|
|
520
535
|
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
@@ -616,12 +631,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
616
631
|
private fun attachRecyclerObserver(recycler: RecyclerView) {
|
|
617
632
|
if (observedRecyclerView === recycler) {
|
|
618
633
|
applyRecyclerInsets(recycler, selectedPage)
|
|
634
|
+
publishConsumedOffset(recycler)
|
|
619
635
|
return
|
|
620
636
|
}
|
|
621
637
|
|
|
622
638
|
detachPrimaryScrollObserver()
|
|
623
639
|
observedRecyclerView = recycler
|
|
624
640
|
applyRecyclerInsets(recycler, selectedPage)
|
|
641
|
+
publishConsumedOffset(recycler)
|
|
625
642
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
626
643
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
627
644
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
@@ -646,6 +663,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
646
663
|
val listener = observedScrollListener
|
|
647
664
|
if (recycler != null) {
|
|
648
665
|
pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
|
|
666
|
+
publishConsumedOffset(recycler, remove = true)
|
|
649
667
|
}
|
|
650
668
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
651
669
|
if (recycler != null) {
|
|
@@ -775,11 +793,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
775
793
|
}
|
|
776
794
|
updateScrollerPaddingOwnership(scroller, original)
|
|
777
795
|
val topInset = headerHeightPx + stickyHeaderHeightPx
|
|
778
|
-
val top = original.top + topInset
|
|
779
796
|
scroller.clipToPadding = false
|
|
780
|
-
if (scroller
|
|
781
|
-
|
|
782
|
-
|
|
797
|
+
if (scroller is ReactScrollView) {
|
|
798
|
+
// Fabric owns the content child's layout, so ScrollView padding alone
|
|
799
|
+
// cannot move it. RN's native ScrollAway API also updates Fabric culling.
|
|
800
|
+
val state = scroller.reactScrollViewScrollState
|
|
801
|
+
val scrollAwayTop = nativeScrollerScrollAwayTop(
|
|
802
|
+
state.scrollAwayPaddingTop, original.appliedTopInset, topInset,
|
|
803
|
+
)
|
|
804
|
+
if (state.scrollAwayPaddingTop != scrollAwayTop ||
|
|
805
|
+
scroller.getChildAt(0)?.translationY != scrollAwayTop.toFloat()) {
|
|
806
|
+
scroller.setScrollAwayPaddingEnabledUnstable(scrollAwayTop, state.scrollAwayPaddingBottom)
|
|
807
|
+
}
|
|
808
|
+
val bottom = original.bottom + topInset
|
|
809
|
+
if (scroller.paddingLeft != original.left || scroller.paddingTop != original.top ||
|
|
810
|
+
scroller.paddingRight != original.right || scroller.paddingBottom != bottom) {
|
|
811
|
+
scroller.setPadding(original.left, original.top, original.right, bottom)
|
|
812
|
+
}
|
|
813
|
+
} else {
|
|
814
|
+
val top = original.top + topInset
|
|
815
|
+
if (scroller.paddingLeft != original.left || scroller.paddingTop != top ||
|
|
816
|
+
scroller.paddingRight != original.right || scroller.paddingBottom != original.bottom) {
|
|
817
|
+
scroller.setPadding(original.left, top, original.right, original.bottom)
|
|
818
|
+
}
|
|
783
819
|
}
|
|
784
820
|
original.appliedTopInset = topInset
|
|
785
821
|
|
|
@@ -804,13 +840,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
804
840
|
scroller: ScrollView,
|
|
805
841
|
original: ScrollerPadding,
|
|
806
842
|
) {
|
|
807
|
-
val
|
|
843
|
+
val isReactScroller = scroller is ReactScrollView
|
|
844
|
+
if (scroller is ReactScrollView) {
|
|
845
|
+
val state = scroller.reactScrollViewScrollState
|
|
846
|
+
if (nativeScrollerPaddingWasResetByScrollAway(
|
|
847
|
+
original.appliedTopInset,
|
|
848
|
+
scroller.paddingLeft, scroller.paddingTop, scroller.paddingRight, scroller.paddingBottom,
|
|
849
|
+
state.scrollAwayPaddingTop, state.scrollAwayPaddingBottom,
|
|
850
|
+
)) {
|
|
851
|
+
// RN may replay ScrollAway state before this pass. Its setter clears
|
|
852
|
+
// caller padding; that native reset must not become our new baseline.
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
val ownedTop = if (isReactScroller) 0 else original.appliedTopInset
|
|
857
|
+
val ownedBottom = if (isReactScroller) original.appliedTopInset else 0
|
|
808
858
|
if (scroller.paddingLeft != original.left) original.left = scroller.paddingLeft
|
|
809
|
-
if (scroller.paddingTop !=
|
|
810
|
-
original.top = (scroller.paddingTop -
|
|
859
|
+
if (scroller.paddingTop != original.top + ownedTop) {
|
|
860
|
+
original.top = (scroller.paddingTop - ownedTop).coerceAtLeast(0)
|
|
811
861
|
}
|
|
812
862
|
if (scroller.paddingRight != original.right) original.right = scroller.paddingRight
|
|
813
|
-
if (scroller.paddingBottom != original.bottom
|
|
863
|
+
if (scroller.paddingBottom != original.bottom + ownedBottom) {
|
|
864
|
+
original.bottom = (scroller.paddingBottom - ownedBottom).coerceAtLeast(0)
|
|
865
|
+
}
|
|
814
866
|
if (scroller.clipToPadding) original.clipToPadding = true
|
|
815
867
|
}
|
|
816
868
|
|
|
@@ -835,6 +887,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
835
887
|
}
|
|
836
888
|
|
|
837
889
|
private fun restoreRecyclerInsets(recycler: RecyclerView) {
|
|
890
|
+
publishConsumedOffset(recycler, remove = true)
|
|
838
891
|
val original = originalRecyclerPadding.remove(recycler) ?: return
|
|
839
892
|
updateRecyclerPaddingOwnership(recycler, original)
|
|
840
893
|
restoredRecyclerKeys.remove(recycler)
|
|
@@ -846,6 +899,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
846
899
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
847
900
|
updateScrollerPaddingOwnership(scroller, original)
|
|
848
901
|
restoredScrollerKeys.remove(scroller)
|
|
902
|
+
if (scroller is ReactScrollView) {
|
|
903
|
+
val state = scroller.reactScrollViewScrollState
|
|
904
|
+
scroller.setScrollAwayPaddingEnabledUnstable(
|
|
905
|
+
nativeScrollerScrollAwayTop(state.scrollAwayPaddingTop, original.appliedTopInset, 0),
|
|
906
|
+
state.scrollAwayPaddingBottom,
|
|
907
|
+
)
|
|
908
|
+
}
|
|
849
909
|
scroller.clipToPadding = original.clipToPadding
|
|
850
910
|
scroller.setPadding(original.left, original.top, original.right, original.bottom)
|
|
851
911
|
}
|
package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
import java.util.WeakHashMap
|
|
4
|
+
|
|
5
|
+
/** Native tag payload; leaf observers can read it as Map without a pager dependency. */
|
|
6
|
+
internal class NativeScrollCoordinatorContributions : WeakHashMap<Any, Int>() {
|
|
7
|
+
fun update(owner: Any, pixels: Int): Boolean {
|
|
8
|
+
val value = pixels.coerceAtLeast(0)
|
|
9
|
+
if (get(owner) == value) return false
|
|
10
|
+
put(owner, value)
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun clearOwner(owner: Any): Boolean = remove(owner) != null
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
// Replace only this pager's contribution; another native owner may share the scroller.
|
|
4
|
+
internal fun nativeScrollerScrollAwayTop(currentTop: Int, previousInset: Int, nextInset: Int): Int =
|
|
5
|
+
(currentTop - previousInset).coerceAtLeast(0) + nextInset
|
|
6
|
+
|
|
7
|
+
// ReactScrollView's ScrollAway setter always writes this exact padding shape.
|
|
8
|
+
internal fun nativeScrollerPaddingWasResetByScrollAway(
|
|
9
|
+
appliedInset: Int,
|
|
10
|
+
left: Int,
|
|
11
|
+
top: Int,
|
|
12
|
+
right: Int,
|
|
13
|
+
bottom: Int,
|
|
14
|
+
scrollAwayTop: Int,
|
|
15
|
+
scrollAwayBottom: Int,
|
|
16
|
+
): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
|
|
17
|
+
bottom == scrollAwayTop + scrollAwayBottom
|
package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
/** Standalone JVM regression: compile together with the production helper. */
|
|
4
|
+
fun main() {
|
|
5
|
+
val outer = Any()
|
|
6
|
+
val inner = Any()
|
|
7
|
+
val firstChild = NativeScrollCoordinatorContributions()
|
|
8
|
+
check(firstChild.update(outer, 120))
|
|
9
|
+
check(firstChild.update(inner, 60))
|
|
10
|
+
check(firstChild.values.sum() == 180)
|
|
11
|
+
check(!firstChild.update(outer, 120))
|
|
12
|
+
check(firstChild.update(outer, 30))
|
|
13
|
+
check(firstChild.values.sum() == 90)
|
|
14
|
+
check(firstChild.clearOwner(outer))
|
|
15
|
+
check(firstChild.values.sum() == 60)
|
|
16
|
+
check(!firstChild.clearOwner(outer))
|
|
17
|
+
val secondChild = NativeScrollCoordinatorContributions()
|
|
18
|
+
check(secondChild.update(outer, 30))
|
|
19
|
+
check(secondChild.values.sum() == 30)
|
|
20
|
+
check(firstChild.values.sum() == 60)
|
|
21
|
+
check(firstChild.clearOwner(inner))
|
|
22
|
+
check(firstChild.isEmpty())
|
|
23
|
+
check(secondChild.update(outer, -1))
|
|
24
|
+
check(secondChild.values.sum() == 0)
|
|
25
|
+
println("Native coordinator contributions: nested owners, duplicate updates, cleanup, and replacement passed")
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
/** Standalone JVM contract regression for Fabric scroller inset ownership. */
|
|
4
|
+
fun main() {
|
|
5
|
+
val initialTop = 24
|
|
6
|
+
val firstHeader = 182 + 48
|
|
7
|
+
val initialApplied = nativeScrollerScrollAwayTop(initialTop, 0, firstHeader)
|
|
8
|
+
check(initialApplied == 254)
|
|
9
|
+
check(nativeScrollerScrollAwayTop(initialApplied, firstHeader, firstHeader) == initialApplied)
|
|
10
|
+
val measuredHeader = 310 + 48
|
|
11
|
+
val resized = nativeScrollerScrollAwayTop(initialApplied, firstHeader, measuredHeader)
|
|
12
|
+
check(resized == initialTop + measuredHeader)
|
|
13
|
+
// A second pager's contribution survives the first pager's detach.
|
|
14
|
+
val nested = nativeScrollerScrollAwayTop(resized, 0, 100)
|
|
15
|
+
val outerDetached = nativeScrollerScrollAwayTop(nested, measuredHeader, 0)
|
|
16
|
+
check(outerDetached == initialTop + 100)
|
|
17
|
+
check(nativeScrollerScrollAwayTop(outerDetached, 100, 0) == initialTop)
|
|
18
|
+
// Fabric may reset state while replacing its content root; do not apply a negative baseline.
|
|
19
|
+
check(nativeScrollerScrollAwayTop(0, measuredHeader, firstHeader) == firstHeader)
|
|
20
|
+
// Caller L/R/B = 16/16/40 must survive RN's setter replay after our 230px inset.
|
|
21
|
+
val caller = intArrayOf(16, 0, 16, 40)
|
|
22
|
+
val rnReplay = intArrayOf(0, 0, 0, 230)
|
|
23
|
+
val keepCaller = nativeScrollerPaddingWasResetByScrollAway(
|
|
24
|
+
230, rnReplay[0], rnReplay[1], rnReplay[2], rnReplay[3], 230, 0,
|
|
25
|
+
)
|
|
26
|
+
check(keepCaller)
|
|
27
|
+
val restored = if (keepCaller) caller else rnReplay
|
|
28
|
+
check(restored.contentEquals(intArrayOf(16, 0, 16, 40)))
|
|
29
|
+
// Nonzero native bottom state is part of RN's reset signature too.
|
|
30
|
+
check(nativeScrollerPaddingWasResetByScrollAway(230, 0, 0, 0, 278, 254, 24))
|
|
31
|
+
// Actual caller updates, including clearing all padding, remain observable.
|
|
32
|
+
check(!nativeScrollerPaddingWasResetByScrollAway(230, 24, 0, 24, 290, 230, 0))
|
|
33
|
+
check(!nativeScrollerPaddingWasResetByScrollAway(230, 0, 0, 0, 0, 230, 0))
|
|
34
|
+
// Initial discovery must capture caller values before this pager owns an inset.
|
|
35
|
+
check(!nativeScrollerPaddingWasResetByScrollAway(0, 0, 0, 0, 230, 230, 0))
|
|
36
|
+
println("NativeScroller ScrollAway ownership: initial, repeat, measured resize, nested detach, Fabric reset, RN padding replay, and caller updates passed")
|
|
37
|
+
}
|
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
## iOS shared-header hit testing
|
|
7
|
+
|
|
8
|
+
Smooth-header hit forwarding is restricted to the pager's own interactive
|
|
9
|
+
viewport. A collapsed header clipped above that viewport MUST NOT intercept
|
|
10
|
+
touches intended for a sibling toolbar. Hidden, disabled and effectively
|
|
11
|
+
transparent pagers do not forward touches. Fabric `pointerEvents="none"` and
|
|
12
|
+
`"box-only"` retain their superclass behavior; `"auto"` and `"box-none"` may
|
|
13
|
+
forward to visible shared-header children inside the pager.
|
|
14
|
+
|
|
15
|
+
The shared header's internal hit testing still supports sticky children that
|
|
16
|
+
have moved outside the header host's original bounds while remaining inside the
|
|
17
|
+
pager. Focused UIKit tests cover sibling-toolbar hit delivery, pinned-child
|
|
18
|
+
delivery, pointer-event modes and interaction flags. Device acceptance includes
|
|
19
|
+
toolbar taps with expanded and collapsed headers on retained pages.
|
|
20
|
+
|
|
21
|
+
## iOS cached-offset restoration after content shrink
|
|
22
|
+
|
|
23
|
+
When restoring a retained page's cached vertical offset, the pager limits only
|
|
24
|
+
the upper bound to the current content height, viewport and applied insets. A
|
|
25
|
+
cache captured before rows were removed MUST NOT restore an offset below the new
|
|
26
|
+
content bottom, including when removal occurs during a page transition. Valid
|
|
27
|
+
deep offsets and the existing lower-bound/header alignment rules are unchanged.
|
|
28
|
+
This check runs only on explicit restoration, not on scroll frames; UIKit pull
|
|
29
|
+
overscroll and refresh settlement retain their existing ownership. An unmeasured
|
|
30
|
+
zero-height viewport defers this upper-bound limit to a later restoration.
|
|
31
|
+
|
|
32
|
+
The focused policy test covers shrink after the list has already corrected its
|
|
33
|
+
offset, valid deep offsets, negative pull offsets and an unmeasured viewport.
|
|
34
|
+
Runtime acceptance requires clearing a retained page during a page transition,
|
|
35
|
+
then returning without an extra drag to expose its header/empty/footer slots.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Ancestor consumed-scroll contributions
|
|
39
|
+
|
|
40
|
+
Status: source implementation; real nested-pager runtime verification pending.
|
|
41
|
+
Android collapsible pagers consume header travel before the primary RecyclerView
|
|
42
|
+
receives content-scroll deltas. A leaf threshold observer needs both distances.
|
|
43
|
+
The coordinator publishes a non-negative physical-pixel contribution under its
|
|
44
|
+
own weak owner identity on the active primary RecyclerView. Nested coordinators
|
|
45
|
+
contribute independently; summing their values gives ancestor-consumed travel.
|
|
46
|
+
|
|
47
|
+
The native View tag named `onekey_native_scroll_coordinator_consumed_offsets`
|
|
48
|
+
stores a WeakHashMap with owner-view keys and integer pixel values. The optional
|
|
49
|
+
`onekey_native_scroll_coordinator_offset_listener` tag holds a Runnable owned by
|
|
50
|
+
the leaf. The publisher updates only its own entry and invokes the listener only
|
|
51
|
+
when that entry changes. An absent map/listener means zero/no observer.
|
|
52
|
+
This protocol depends only on Android View/resources and Java collections; the
|
|
53
|
+
pager MUST NOT depend on NativeList or infer Home/business semantics.
|
|
54
|
+
|
|
55
|
+
On switching the primary child, restoring insets, or detaching, the coordinator
|
|
56
|
+
removes only its own contribution. It MUST NOT retain an old RecyclerView or
|
|
57
|
+
another coordinator. Zero contributions may remain while the owner is attached;
|
|
58
|
+
weak keys prevent abandoned owners from becoming resource roots. The leaf owns
|
|
59
|
+
listener registration and cleanup. All operations occur on the UI thread; there
|
|
60
|
+
are no JavaScript scroll-frame callbacks in this protocol.
|
|
61
|
+
|
|
62
|
+
A new child receives current header travel before threshold evaluation. Native
|
|
63
|
+
list offsets remain local to the leaf, while consumers of this protocol add all
|
|
64
|
+
ancestor contributions exactly once. Programmatic scrolling/header restoration
|
|
65
|
+
must publish the resulting header offset through the same path.
|
|
66
|
+
|
|
67
|
+
## Acceptance
|
|
68
|
+
|
|
69
|
+
The focused JVM helper test covers independent ancestor contributions and removal.
|
|
70
|
+
Required Android runtime cases are one/nested collapsible pagers, header-only
|
|
71
|
+
scrolling, reversal to expanded top, switching pages, native child replacement,
|
|
72
|
+
and detach/remount without stale offsets. iOS uses its existing normalized
|
|
73
|
+
UIScrollView offset and requires no equivalent tag protocol. Source/helper
|
|
74
|
+
checks do not establish the Android runtime acceptance above.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Android Fabric NativeScroller content insets
|
|
78
|
+
|
|
79
|
+
A ReactScrollView's content root is laid out by Fabric. Native `setPadding(top)`
|
|
80
|
+
alone MUST NOT be used to reserve a collapsible header: ReactScrollView does not
|
|
81
|
+
perform the platform ScrollView child layout, so that leaves rows under the
|
|
82
|
+
header. For the supported RN 0.86 runtime, the coordinator uses
|
|
83
|
+
`setScrollAwayPaddingEnabledUnstable` to offset the content root and synchronize
|
|
84
|
+
Fabric content-origin/culling state. The equivalent inset is added to the native
|
|
85
|
+
scroll range at the bottom; it is not added a second time at the top. Ordinary
|
|
86
|
+
Android ScrollView and RecyclerView retain their existing inset paths.
|
|
87
|
+
|
|
88
|
+
Each coordinator replaces only its previous header-plus-sticky contribution.
|
|
89
|
+
Measured-height changes do not accumulate insets; detach restores the remaining
|
|
90
|
+
ScrollAway state and caller padding, including another native owner's inset.
|
|
91
|
+
Content-root replacement reapplies the native offset. No Home layout values,
|
|
92
|
+
new React props, or per-frame JS updates are introduced; iOS/Web are unchanged.
|
|
93
|
+
|
|
94
|
+
The focused JVM policy regression covers an existing inset, repeated application,
|
|
95
|
+
measured-header resize, nested-owner detach, and Fabric state reset. The API call
|
|
96
|
+
is compile-checked against the installed React Android 0.86.2 artifact. Runtime
|
|
97
|
+
acceptance requires first content below the expanded sticky tabs, collapse and
|
|
98
|
+
expand without overlap, retained-page switching, header height changes, and
|
|
99
|
+
reaching the actual final content without an extra header-sized blank footer.
|
|
100
|
+
|
|
101
|
+
RN's ScrollAway setter (including native state replay when Fabric culling flags
|
|
102
|
+
are enabled) resets physical padding to `(0, 0, 0, scrollAwayTop + scrollAwayBottom)`.
|
|
103
|
+
While owning an inset, the coordinator recognizes that exact native reset and
|
|
104
|
+
retains saved caller padding; other physical-padding updates remain observable.
|
|
105
|
+
The focused policy regression includes nonzero caller left/right/bottom padding,
|
|
106
|
+
nonzero prior native insets, explicit caller padding changes/clear, and initial
|
|
107
|
+
discovery before ownership. Current Home zero-padding behavior is unchanged by
|
|
108
|
+
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
|
|
@@ -1145,6 +1145,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1145
1145
|
|
|
1146
1146
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
1147
1147
|
{
|
|
1148
|
+
// Shared headers can sit above their clipped viewport after collapsing.
|
|
1149
|
+
// Forward only viewport touches, so invisible headers cannot cover siblings.
|
|
1150
|
+
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01 ||
|
|
1151
|
+
![self pointInside:point withEvent:event]) return nil;
|
|
1152
|
+
const auto &viewProps =
|
|
1153
|
+
*std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
|
|
1154
|
+
if (viewProps.pointerEvents == PointerEventsMode::None ||
|
|
1155
|
+
viewProps.pointerEvents == PointerEventsMode::BoxOnly) {
|
|
1156
|
+
return [super hitTest:point withEvent:event];
|
|
1157
|
+
}
|
|
1148
1158
|
if (_nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
|
|
1149
1159
|
_sharedHeaderHostView.superview != _containerView) {
|
|
1150
1160
|
CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
|
|
@@ -2580,6 +2590,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2580
2590
|
}
|
|
2581
2591
|
targetOffset = restoredLogicalOffset - next.top;
|
|
2582
2592
|
targetOffset = MAX(targetOffset, -next.top + _headerOffset);
|
|
2593
|
+
// An inactive list may shrink after its offset observer was detached.
|
|
2594
|
+
// Do not overwrite its corrected offset with a now-unreachable cached one.
|
|
2595
|
+
targetOffset = RNCLimitRestoredScrollOffset(
|
|
2596
|
+
targetOffset,
|
|
2597
|
+
scrollView.contentSize.height,
|
|
2598
|
+
scrollView.bounds.size.height,
|
|
2599
|
+
next.top,
|
|
2600
|
+
next.bottom
|
|
2601
|
+
);
|
|
2583
2602
|
}
|
|
2584
2603
|
// A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
|
|
2585
2604
|
if (restore || previousTopInset != next.top) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
|
|
3
|
+
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
4
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
5
|
+
|
|
6
|
+
using namespace facebook::react;
|
|
7
|
+
|
|
8
|
+
@interface RNCCollapsiblePagerHitTestTests : XCTestCase
|
|
9
|
+
@property (nonatomic, strong) UIView *root;
|
|
10
|
+
@property (nonatomic, strong) UIButton *toolbar;
|
|
11
|
+
@property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
|
|
12
|
+
@property (nonatomic, strong) UIButton *sticky;
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@implementation RNCCollapsiblePagerHitTestTests
|
|
16
|
+
|
|
17
|
+
- (void)setPointerEvents:(PointerEventsMode)mode
|
|
18
|
+
{
|
|
19
|
+
auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
20
|
+
props->nativeSmoothHeaderScrollEnabled = true;
|
|
21
|
+
props->pointerEvents = mode;
|
|
22
|
+
[self.pager updateProps:props oldProps:nullptr];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (void)setUp
|
|
26
|
+
{
|
|
27
|
+
[super setUp];
|
|
28
|
+
self.root = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
29
|
+
self.toolbar = [[UIButton alloc] initWithFrame:CGRectMake(0, 36, 320, 64)];
|
|
30
|
+
[self.root addSubview:self.toolbar];
|
|
31
|
+
self.pager = [[RNCCollapsiblePagerViewComponentView alloc] init];
|
|
32
|
+
[self setPointerEvents:PointerEventsMode::Auto];
|
|
33
|
+
self.pager.frame = CGRectMake(0, 100, 320, 500);
|
|
34
|
+
[self.root addSubview:self.pager];
|
|
35
|
+
|
|
36
|
+
// Model smooth mode's header reparenting and its collapsed, clipped content.
|
|
37
|
+
UIView *scroll = [[UIView alloc] initWithFrame:self.pager.bounds];
|
|
38
|
+
[self.pager addSubview:scroll];
|
|
39
|
+
UIView *header = [self.pager valueForKey:@"sharedHeaderHostView"];
|
|
40
|
+
for (UIView *child in header.subviews.copy) [child removeFromSuperview];
|
|
41
|
+
[scroll addSubview:header];
|
|
42
|
+
header.frame = CGRectMake(0, -64, 320, 108);
|
|
43
|
+
[header addSubview:[[UIButton alloc] initWithFrame:CGRectMake(0, 0, 320, 64)]];
|
|
44
|
+
self.sticky = [[UIButton alloc] initWithFrame:CGRectMake(0, 140, 320, 44)];
|
|
45
|
+
[header addSubview:self.sticky];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)testCollapsedHeaderDoesNotInterceptSiblingToolbar
|
|
49
|
+
{
|
|
50
|
+
XCTAssertEqual([self.root hitTest:CGPointMake(20, 80) withEvent:nil], self.toolbar);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)testPinnedChildOutsideHeaderHostStillReceivesViewportTouches
|
|
54
|
+
{
|
|
55
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
|
|
56
|
+
[self setPointerEvents:PointerEventsMode::BoxNone];
|
|
57
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
- (void)testFabricPointerEventsAreNotBypassed
|
|
61
|
+
{
|
|
62
|
+
[self setPointerEvents:PointerEventsMode::None];
|
|
63
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
64
|
+
[self setPointerEvents:PointerEventsMode::BoxOnly];
|
|
65
|
+
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.pager);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)testNoninteractivePagerDoesNotForwardHeaderTouches
|
|
69
|
+
{
|
|
70
|
+
self.pager.hidden = YES;
|
|
71
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
72
|
+
self.pager.hidden = NO;
|
|
73
|
+
self.pager.userInteractionEnabled = NO;
|
|
74
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
75
|
+
self.pager.userInteractionEnabled = YES;
|
|
76
|
+
self.pager.alpha = 0.005;
|
|
77
|
+
XCTAssertNil([self.pager hitTest:CGPointMake(20, 90) withEvent:nil]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@end
|
|
@@ -7,6 +7,29 @@
|
|
|
7
7
|
|
|
8
8
|
@implementation RNCCollapsiblePagerReleasedStatePolicyTests
|
|
9
9
|
|
|
10
|
+
- (void)testRestorationAfterInactiveContentShrinkCannotReapplyStaleCache
|
|
11
|
+
{
|
|
12
|
+
// The list already corrected to -44 while inactive, but the pager still
|
|
13
|
+
// retains the old logical offset captured before rows were removed.
|
|
14
|
+
CGFloat savedLogicalOffset = 513.67;
|
|
15
|
+
CGFloat restored = RNCLimitRestoredScrollOffset(
|
|
16
|
+
savedLogicalOffset - 108, 132, 708, 108, 532);
|
|
17
|
+
XCTAssertEqualWithAccuracy(restored, -44, 0.001);
|
|
18
|
+
XCTAssertEqualWithAccuracy(restored + 108, 64, 0.001);
|
|
19
|
+
XCTAssertEqualWithAccuracy(
|
|
20
|
+
RNCLimitRestoredScrollOffset(restored, 132, 708, 108, 532), restored, 0.001);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)testRestorationPreservesValidDeepAndNegativeOffsets
|
|
24
|
+
{
|
|
25
|
+
XCTAssertEqualWithAccuracy(
|
|
26
|
+
RNCLimitRestoredScrollOffset(600, 2400, 708, 108, 0), 600, 0.001);
|
|
27
|
+
XCTAssertEqualWithAccuracy(
|
|
28
|
+
RNCLimitRestoredScrollOffset(-160, 132, 708, 108, 532), -160, 0.001);
|
|
29
|
+
XCTAssertEqualWithAccuracy(
|
|
30
|
+
RNCLimitRestoredScrollOffset(600, 0, 0, 108, 0), 600, 0.001);
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
- (void)testPrunesRemovedKeysAndKeepsStableKeysAcrossReorders
|
|
11
34
|
{
|
|
12
35
|
NSObject *alpha = [NSObject new];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.157-alpha.
|
|
3
|
+
"version": "3.0.157-alpha.255",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"lib",
|
|
22
22
|
"android",
|
|
23
23
|
"ios",
|
|
24
|
+
"docs",
|
|
24
25
|
"react-native-pager-view.podspec",
|
|
25
26
|
"!ios/build",
|
|
26
27
|
"!android/build",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"typescript": "^5.9.2"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
|
-
"@onekeyfe/react-native-native-logger": "3.0.157-alpha.
|
|
70
|
+
"@onekeyfe/react-native-native-logger": "3.0.157-alpha.255",
|
|
70
71
|
"react": "*",
|
|
71
72
|
"react-native": "*"
|
|
72
73
|
},
|