@onekeyfe/react-native-pager-view 3.0.157-alpha.254 → 3.0.157-alpha.256
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 +133 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
- package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +181 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +180 -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,133 @@
|
|
|
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 refresh indicator placement
|
|
7
|
+
|
|
8
|
+
Status: source implementation; real refresh-cycle acceptance pending.
|
|
9
|
+
In smooth-header mode, the pager shifts the refresh control's sublayers upward
|
|
10
|
+
by its header-plus-sticky height. It does not change the refresh control frame,
|
|
11
|
+
bounds, scroll offset, refresh state or JavaScript callback. RN's caller-owned
|
|
12
|
+
`progressViewOffset` remains untouched, including dynamic updates whose bounds
|
|
13
|
+
origin could otherwise collide with a coordinator-applied value.
|
|
14
|
+
|
|
15
|
+
One internal state per UIRefreshControl combines weak pager-owner contributions.
|
|
16
|
+
It preserves the original sublayer transform and clipping setting. Repeated
|
|
17
|
+
attachment and header resizing replace an owner's contribution. Layer clipping
|
|
18
|
+
is disabled while coordinating so it cannot hide the translated indicator.
|
|
19
|
+
A control replacement, scroll-view release, pager recycling or destruction
|
|
20
|
+
removes only that owner's contribution and restores the baseline when none remain.
|
|
21
|
+
Controls installed after the scroller is attached are also coordinated. Native
|
|
22
|
+
UICollectionView controls and RN ScrollView controls use this same UIKit boundary.
|
|
23
|
+
|
|
24
|
+
Native layer observations reapply the contribution after an external sublayer
|
|
25
|
+
transform change, without observing or interpreting UIKit/RN bounds writes.
|
|
26
|
+
There is no per-frame JavaScript work. Acceptance must cover real UIKit pulling
|
|
27
|
+
and refresh completion, caller offset changes, late installation/replacement,
|
|
28
|
+
retained-page switching, clipping and nested-owner cleanup. Programmatic UIKit
|
|
29
|
+
unit tests do not substitute for real pull-gesture visibility acceptance.
|
|
30
|
+
|
|
31
|
+
## iOS shared-header hit testing
|
|
32
|
+
|
|
33
|
+
Smooth-header hit forwarding is restricted to the pager's own interactive
|
|
34
|
+
viewport. A collapsed header clipped above that viewport MUST NOT intercept
|
|
35
|
+
touches intended for a sibling toolbar. Hidden, disabled and effectively
|
|
36
|
+
transparent pagers do not forward touches. Fabric `pointerEvents="none"` and
|
|
37
|
+
`"box-only"` retain their superclass behavior; `"auto"` and `"box-none"` may
|
|
38
|
+
forward to visible shared-header children inside the pager.
|
|
39
|
+
|
|
40
|
+
The shared header's internal hit testing still supports sticky children that
|
|
41
|
+
have moved outside the header host's original bounds while remaining inside the
|
|
42
|
+
pager. Focused UIKit tests cover sibling-toolbar hit delivery, pinned-child
|
|
43
|
+
delivery, pointer-event modes and interaction flags. Device acceptance includes
|
|
44
|
+
toolbar taps with expanded and collapsed headers on retained pages.
|
|
45
|
+
|
|
46
|
+
## iOS cached-offset restoration after content shrink
|
|
47
|
+
|
|
48
|
+
When restoring a retained page's cached vertical offset, the pager limits only
|
|
49
|
+
the upper bound to the current content height, viewport and applied insets. A
|
|
50
|
+
cache captured before rows were removed MUST NOT restore an offset below the new
|
|
51
|
+
content bottom, including when removal occurs during a page transition. Valid
|
|
52
|
+
deep offsets and the existing lower-bound/header alignment rules are unchanged.
|
|
53
|
+
This check runs only on explicit restoration, not on scroll frames; UIKit pull
|
|
54
|
+
overscroll and refresh settlement retain their existing ownership. An unmeasured
|
|
55
|
+
zero-height viewport defers this upper-bound limit to a later restoration.
|
|
56
|
+
|
|
57
|
+
The focused policy test covers shrink after the list has already corrected its
|
|
58
|
+
offset, valid deep offsets, negative pull offsets and an unmeasured viewport.
|
|
59
|
+
Runtime acceptance requires clearing a retained page during a page transition,
|
|
60
|
+
then returning without an extra drag to expose its header/empty/footer slots.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Ancestor consumed-scroll contributions
|
|
64
|
+
|
|
65
|
+
Status: source implementation; real nested-pager runtime verification pending.
|
|
66
|
+
Android collapsible pagers consume header travel before the primary RecyclerView
|
|
67
|
+
receives content-scroll deltas. A leaf threshold observer needs both distances.
|
|
68
|
+
The coordinator publishes a non-negative physical-pixel contribution under its
|
|
69
|
+
own weak owner identity on the active primary RecyclerView. Nested coordinators
|
|
70
|
+
contribute independently; summing their values gives ancestor-consumed travel.
|
|
71
|
+
|
|
72
|
+
The native View tag named `onekey_native_scroll_coordinator_consumed_offsets`
|
|
73
|
+
stores a WeakHashMap with owner-view keys and integer pixel values. The optional
|
|
74
|
+
`onekey_native_scroll_coordinator_offset_listener` tag holds a Runnable owned by
|
|
75
|
+
the leaf. The publisher updates only its own entry and invokes the listener only
|
|
76
|
+
when that entry changes. An absent map/listener means zero/no observer.
|
|
77
|
+
This protocol depends only on Android View/resources and Java collections; the
|
|
78
|
+
pager MUST NOT depend on NativeList or infer Home/business semantics.
|
|
79
|
+
|
|
80
|
+
On switching the primary child, restoring insets, or detaching, the coordinator
|
|
81
|
+
removes only its own contribution. It MUST NOT retain an old RecyclerView or
|
|
82
|
+
another coordinator. Zero contributions may remain while the owner is attached;
|
|
83
|
+
weak keys prevent abandoned owners from becoming resource roots. The leaf owns
|
|
84
|
+
listener registration and cleanup. All operations occur on the UI thread; there
|
|
85
|
+
are no JavaScript scroll-frame callbacks in this protocol.
|
|
86
|
+
|
|
87
|
+
A new child receives current header travel before threshold evaluation. Native
|
|
88
|
+
list offsets remain local to the leaf, while consumers of this protocol add all
|
|
89
|
+
ancestor contributions exactly once. Programmatic scrolling/header restoration
|
|
90
|
+
must publish the resulting header offset through the same path.
|
|
91
|
+
|
|
92
|
+
## Acceptance
|
|
93
|
+
|
|
94
|
+
The focused JVM helper test covers independent ancestor contributions and removal.
|
|
95
|
+
Required Android runtime cases are one/nested collapsible pagers, header-only
|
|
96
|
+
scrolling, reversal to expanded top, switching pages, native child replacement,
|
|
97
|
+
and detach/remount without stale offsets. iOS uses its existing normalized
|
|
98
|
+
UIScrollView offset and requires no equivalent tag protocol. Source/helper
|
|
99
|
+
checks do not establish the Android runtime acceptance above.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Android Fabric NativeScroller content insets
|
|
103
|
+
|
|
104
|
+
A ReactScrollView's content root is laid out by Fabric. Native `setPadding(top)`
|
|
105
|
+
alone MUST NOT be used to reserve a collapsible header: ReactScrollView does not
|
|
106
|
+
perform the platform ScrollView child layout, so that leaves rows under the
|
|
107
|
+
header. For the supported RN 0.86 runtime, the coordinator uses
|
|
108
|
+
`setScrollAwayPaddingEnabledUnstable` to offset the content root and synchronize
|
|
109
|
+
Fabric content-origin/culling state. The equivalent inset is added to the native
|
|
110
|
+
scroll range at the bottom; it is not added a second time at the top. Ordinary
|
|
111
|
+
Android ScrollView and RecyclerView retain their existing inset paths.
|
|
112
|
+
|
|
113
|
+
Each coordinator replaces only its previous header-plus-sticky contribution.
|
|
114
|
+
Measured-height changes do not accumulate insets; detach restores the remaining
|
|
115
|
+
ScrollAway state and caller padding, including another native owner's inset.
|
|
116
|
+
Content-root replacement reapplies the native offset. No Home layout values,
|
|
117
|
+
new React props, or per-frame JS updates are introduced; iOS/Web are unchanged.
|
|
118
|
+
|
|
119
|
+
The focused JVM policy regression covers an existing inset, repeated application,
|
|
120
|
+
measured-header resize, nested-owner detach, and Fabric state reset. The API call
|
|
121
|
+
is compile-checked against the installed React Android 0.86.2 artifact. Runtime
|
|
122
|
+
acceptance requires first content below the expanded sticky tabs, collapse and
|
|
123
|
+
expand without overlap, retained-page switching, header height changes, and
|
|
124
|
+
reaching the actual final content without an extra header-sized blank footer.
|
|
125
|
+
|
|
126
|
+
RN's ScrollAway setter (including native state replay when Fabric culling flags
|
|
127
|
+
are enabled) resets physical padding to `(0, 0, 0, scrollAwayTop + scrollAwayBottom)`.
|
|
128
|
+
While owning an inset, the coordinator recognizes that exact native reset and
|
|
129
|
+
retains saved caller padding; other physical-padding updates remain observable.
|
|
130
|
+
The focused policy regression includes nonzero caller left/right/bottom padding,
|
|
131
|
+
nonzero prior native insets, explicit caller padding changes/clear, and initial
|
|
132
|
+
discovery before ownership. Current Home zero-padding behavior is unchanged by
|
|
133
|
+
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
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
#import "React/RCTScrollViewComponentView.h"
|
|
12
12
|
#import "React/RCTSurfaceTouchHandler.h"
|
|
13
13
|
#import "React/RCTTouchHandler.h"
|
|
14
|
+
#import <objc/runtime.h>
|
|
14
15
|
|
|
15
16
|
// OneKey patch: deduplicate structural mutation parents before scanning the page.
|
|
16
17
|
#include <unordered_set>
|
|
@@ -18,6 +19,116 @@
|
|
|
18
19
|
using namespace facebook::react;
|
|
19
20
|
|
|
20
21
|
static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
|
|
22
|
+
static void *RNCRefreshControlLayerContext = &RNCRefreshControlLayerContext;
|
|
23
|
+
static void *RNCScrollRefreshControlContext = &RNCScrollRefreshControlContext;
|
|
24
|
+
static char RNCRefreshControlOffsetKey;
|
|
25
|
+
|
|
26
|
+
@interface RNCRefreshControlOffsetState : NSObject
|
|
27
|
+
+ (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner;
|
|
28
|
+
+ (void)removeControl:(UIRefreshControl *)control owner:(id)owner;
|
|
29
|
+
@end
|
|
30
|
+
|
|
31
|
+
@implementation RNCRefreshControlOffsetState {
|
|
32
|
+
__weak UIRefreshControl *_control;
|
|
33
|
+
NSMapTable<id, NSNumber *> *_offsets;
|
|
34
|
+
CATransform3D _baselineTransform;
|
|
35
|
+
CATransform3D _appliedTransform;
|
|
36
|
+
BOOL _originalMasksToBounds;
|
|
37
|
+
BOOL _applying;
|
|
38
|
+
BOOL _observing;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (instancetype)initWithControl:(UIRefreshControl *)control
|
|
42
|
+
{
|
|
43
|
+
if ((self = [super init])) {
|
|
44
|
+
_control = control;
|
|
45
|
+
_baselineTransform = control.layer.sublayerTransform;
|
|
46
|
+
_originalMasksToBounds = control.layer.masksToBounds;
|
|
47
|
+
_offsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
48
|
+
[control.layer addObserver:self forKeyPath:@"sublayerTransform"
|
|
49
|
+
options:NSKeyValueObservingOptionNew
|
|
50
|
+
context:RNCRefreshControlLayerContext];
|
|
51
|
+
[control.layer addObserver:self forKeyPath:@"masksToBounds"
|
|
52
|
+
options:NSKeyValueObservingOptionNew
|
|
53
|
+
context:RNCRefreshControlLayerContext];
|
|
54
|
+
_observing = YES;
|
|
55
|
+
}
|
|
56
|
+
return self;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)apply
|
|
60
|
+
{
|
|
61
|
+
UIRefreshControl *control = _control;
|
|
62
|
+
if (control == nil) return;
|
|
63
|
+
CGFloat offset = 0;
|
|
64
|
+
for (NSNumber *value in _offsets.objectEnumerator) offset += value.doubleValue;
|
|
65
|
+
CATransform3D transform = CATransform3DConcat(
|
|
66
|
+
_baselineTransform, CATransform3DMakeTranslation(0, -offset, 0));
|
|
67
|
+
_appliedTransform = transform;
|
|
68
|
+
_applying = YES;
|
|
69
|
+
// Visual placement must not change UIKit's refresh geometry or RN's bounds
|
|
70
|
+
// origin (progressViewOffset). Allow the shifted indicator outside its host.
|
|
71
|
+
[CATransaction begin];
|
|
72
|
+
[CATransaction setDisableActions:YES];
|
|
73
|
+
control.layer.masksToBounds = _offsets.count == 0 ? _originalMasksToBounds : NO;
|
|
74
|
+
if (!CATransform3DEqualToTransform(control.layer.sublayerTransform, transform)) {
|
|
75
|
+
control.layer.sublayerTransform = transform;
|
|
76
|
+
}
|
|
77
|
+
[CATransaction commit];
|
|
78
|
+
_applying = NO;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
+ (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner
|
|
82
|
+
{
|
|
83
|
+
if (control == nil) return;
|
|
84
|
+
RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
|
|
85
|
+
if (state == nil) {
|
|
86
|
+
state = [[self alloc] initWithControl:control];
|
|
87
|
+
objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
88
|
+
}
|
|
89
|
+
[state->_offsets setObject:@(offset) forKey:owner];
|
|
90
|
+
[state apply];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
+ (void)removeControl:(UIRefreshControl *)control owner:(id)owner
|
|
94
|
+
{
|
|
95
|
+
if (control == nil) return;
|
|
96
|
+
RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
|
|
97
|
+
if (state == nil) return;
|
|
98
|
+
[state->_offsets removeObjectForKey:owner];
|
|
99
|
+
[state apply];
|
|
100
|
+
if (state->_offsets.count == 0) {
|
|
101
|
+
[control.layer removeObserver:state forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
|
|
102
|
+
[control.layer removeObserver:state forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
|
|
103
|
+
state->_observing = NO;
|
|
104
|
+
objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
|
109
|
+
change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
|
|
110
|
+
{
|
|
111
|
+
if (context == RNCRefreshControlLayerContext) {
|
|
112
|
+
if (_applying) return;
|
|
113
|
+
if ([keyPath isEqualToString:@"sublayerTransform"]) {
|
|
114
|
+
CATransform3D next = [change[NSKeyValueChangeNewKey] CATransform3DValue];
|
|
115
|
+
if (CATransform3DEqualToTransform(next, _appliedTransform)) return;
|
|
116
|
+
_baselineTransform = next;
|
|
117
|
+
}
|
|
118
|
+
[self apply];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)dealloc
|
|
125
|
+
{
|
|
126
|
+
if (_observing && _control != nil) {
|
|
127
|
+
[_control.layer removeObserver:self forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
|
|
128
|
+
[_control.layer removeObserver:self forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
@end
|
|
21
132
|
static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
|
|
22
133
|
@"rnc-collapsible-pager-native-scroller:";
|
|
23
134
|
|
|
@@ -973,6 +1084,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
973
1084
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
|
|
974
1085
|
- (void)updateSharedHeaderPressCancellationGesture;
|
|
975
1086
|
- (void)detachSharedHeaderPressCancellationGesture;
|
|
1087
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
1088
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
976
1089
|
- (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
977
1090
|
- (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
978
1091
|
- (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
@@ -1023,6 +1136,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1023
1136
|
NSString *_retainedPages;
|
|
1024
1137
|
NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
|
|
1025
1138
|
NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
|
|
1139
|
+
NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
|
|
1140
|
+
NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
|
|
1026
1141
|
NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
|
|
1027
1142
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
1028
1143
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
@@ -1066,6 +1181,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1066
1181
|
_pageOffsets = [NSMutableDictionary new];
|
|
1067
1182
|
_releasedPageScrollStates = [NSMutableDictionary new];
|
|
1068
1183
|
_originalInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1184
|
+
_managedRefreshControls = [NSMapTable weakToWeakObjectsMapTable];
|
|
1185
|
+
_refreshControlObservedScrollViews = [NSHashTable weakObjectsHashTable];
|
|
1069
1186
|
_appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1070
1187
|
_originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1071
1188
|
_originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
|
|
@@ -1145,6 +1262,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1145
1262
|
|
|
1146
1263
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
1147
1264
|
{
|
|
1265
|
+
// Shared headers can sit above their clipped viewport after collapsing.
|
|
1266
|
+
// Forward only viewport touches, so invisible headers cannot cover siblings.
|
|
1267
|
+
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01 ||
|
|
1268
|
+
![self pointInside:point withEvent:event]) return nil;
|
|
1269
|
+
const auto &viewProps =
|
|
1270
|
+
*std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
|
|
1271
|
+
if (viewProps.pointerEvents == PointerEventsMode::None ||
|
|
1272
|
+
viewProps.pointerEvents == PointerEventsMode::BoxOnly) {
|
|
1273
|
+
return [super hitTest:point withEvent:event];
|
|
1274
|
+
}
|
|
1148
1275
|
if (_nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
|
|
1149
1276
|
_sharedHeaderHostView.superview != _containerView) {
|
|
1150
1277
|
CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
|
|
@@ -1191,6 +1318,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1191
1318
|
|
|
1192
1319
|
- (void)dealloc
|
|
1193
1320
|
{
|
|
1321
|
+
for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
|
|
1322
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
1323
|
+
}
|
|
1194
1324
|
[self detachSharedHeaderPressCancellationGesture];
|
|
1195
1325
|
}
|
|
1196
1326
|
|
|
@@ -1729,6 +1859,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1729
1859
|
[self detachSharedHeaderPressCancellationGesture];
|
|
1730
1860
|
[self detachScrollObserver];
|
|
1731
1861
|
[self restoreSharedHeadersToContainer];
|
|
1862
|
+
for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
|
|
1863
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
1864
|
+
}
|
|
1732
1865
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
1733
1866
|
NSValue *value = [_originalInsets objectForKey:scrollView];
|
|
1734
1867
|
if (value != nil) {
|
|
@@ -2418,10 +2551,44 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2418
2551
|
}
|
|
2419
2552
|
}
|
|
2420
2553
|
|
|
2554
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView
|
|
2555
|
+
{
|
|
2556
|
+
if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled) {
|
|
2557
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (![_refreshControlObservedScrollViews containsObject:scrollView]) {
|
|
2561
|
+
[_refreshControlObservedScrollViews addObject:scrollView];
|
|
2562
|
+
[scrollView addObserver:self forKeyPath:@"refreshControl"
|
|
2563
|
+
options:NSKeyValueObservingOptionNew
|
|
2564
|
+
context:RNCScrollRefreshControlContext];
|
|
2565
|
+
}
|
|
2566
|
+
UIRefreshControl *previous = [_managedRefreshControls objectForKey:scrollView];
|
|
2567
|
+
UIRefreshControl *current = scrollView.refreshControl;
|
|
2568
|
+
if (previous != current) {
|
|
2569
|
+
[RNCRefreshControlOffsetState removeControl:previous owner:self];
|
|
2570
|
+
[_managedRefreshControls removeObjectForKey:scrollView];
|
|
2571
|
+
if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
|
|
2572
|
+
}
|
|
2573
|
+
[RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
|
|
2574
|
+
forControl:current owner:self];
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
|
|
2578
|
+
{
|
|
2579
|
+
if ([_refreshControlObservedScrollViews containsObject:scrollView]) {
|
|
2580
|
+
[scrollView removeObserver:self forKeyPath:@"refreshControl" context:RNCScrollRefreshControlContext];
|
|
2581
|
+
[_refreshControlObservedScrollViews removeObject:scrollView];
|
|
2582
|
+
}
|
|
2583
|
+
[RNCRefreshControlOffsetState removeControl:[_managedRefreshControls objectForKey:scrollView] owner:self];
|
|
2584
|
+
[_managedRefreshControls removeObjectForKey:scrollView];
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2421
2587
|
// OneKey patch: release observers, headers and saved state while the scroll view
|
|
2422
2588
|
// still represents the old list, before Fabric applies its next owner's props.
|
|
2423
2589
|
- (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
|
|
2424
2590
|
{
|
|
2591
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
2425
2592
|
if (scrollView == _observedScrollView) {
|
|
2426
2593
|
[self detachScrollObserver];
|
|
2427
2594
|
if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
|
|
@@ -2538,6 +2705,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2538
2705
|
// This host already includes the fixed header and safe area in its frame.
|
|
2539
2706
|
// UIKit automatic adjustment would add them a second time to short pages.
|
|
2540
2707
|
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
2708
|
+
[self updateRefreshControlForScrollView:scrollView];
|
|
2541
2709
|
CGFloat previousTopInset = scrollView.contentInset.top;
|
|
2542
2710
|
CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
|
|
2543
2711
|
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
@@ -2580,6 +2748,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2580
2748
|
}
|
|
2581
2749
|
targetOffset = restoredLogicalOffset - next.top;
|
|
2582
2750
|
targetOffset = MAX(targetOffset, -next.top + _headerOffset);
|
|
2751
|
+
// An inactive list may shrink after its offset observer was detached.
|
|
2752
|
+
// Do not overwrite its corrected offset with a now-unreachable cached one.
|
|
2753
|
+
targetOffset = RNCLimitRestoredScrollOffset(
|
|
2754
|
+
targetOffset,
|
|
2755
|
+
scrollView.contentSize.height,
|
|
2756
|
+
scrollView.bounds.size.height,
|
|
2757
|
+
next.top,
|
|
2758
|
+
next.bottom
|
|
2759
|
+
);
|
|
2583
2760
|
}
|
|
2584
2761
|
// A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
|
|
2585
2762
|
if (restore || previousTopInset != next.top) {
|
|
@@ -2617,6 +2794,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2617
2794
|
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
|
|
2618
2795
|
context:(void *)context
|
|
2619
2796
|
{
|
|
2797
|
+
if (context == RNCScrollRefreshControlContext) {
|
|
2798
|
+
[self updateRefreshControlForScrollView:(UIScrollView *)object];
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2620
2801
|
if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
|
|
2621
2802
|
if ([keyPath isEqualToString:@"contentSize"]) {
|
|
2622
2803
|
NSValue *oldSize = change[NSKeyValueChangeOldKey];
|
|
@@ -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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
3
|
+
|
|
4
|
+
@interface RNCCollapsiblePagerViewComponentView (RefreshControlTesting)
|
|
5
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
6
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@interface RNCCollapsiblePagerRefreshControlTests : XCTestCase
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation RNCCollapsiblePagerRefreshControlTests
|
|
13
|
+
|
|
14
|
+
- (RNCCollapsiblePagerViewComponentView *)pagerWithHeader:(CGFloat)height
|
|
15
|
+
{
|
|
16
|
+
RNCCollapsiblePagerViewComponentView *pager = [RNCCollapsiblePagerViewComponentView new];
|
|
17
|
+
[pager setValue:@YES forKey:@"nativeSmoothHeaderScrollEnabled"];
|
|
18
|
+
[pager setValue:@(height) forKey:@"headerHeight"];
|
|
19
|
+
[pager setValue:@44 forKey:@"stickyHeaderHeight"];
|
|
20
|
+
return pager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)testLateControlAndCallerOffsetUpdatesPreserveBaseline
|
|
24
|
+
{
|
|
25
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
26
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
27
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
28
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
29
|
+
control.bounds = CGRectMake(3, -12, 320, 60);
|
|
30
|
+
scroll.refreshControl = control;
|
|
31
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
32
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -12, 0.001);
|
|
33
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
34
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
35
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -12, 0.001);
|
|
36
|
+
control.layer.sublayerTransform = control.layer.sublayerTransform;
|
|
37
|
+
// Also cover an explicit no-op KVO notification, even if CALayer's setter
|
|
38
|
+
// suppresses notifications for equal transforms on the current OS.
|
|
39
|
+
[control.layer willChangeValueForKey:@"sublayerTransform"];
|
|
40
|
+
[control.layer didChangeValueForKey:@"sublayerTransform"];
|
|
41
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
42
|
+
|
|
43
|
+
// Fabric's progressViewOffset setter replaces bounds.origin.y directly.
|
|
44
|
+
control.bounds = CGRectMake(3, -20, 320, 60);
|
|
45
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
46
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
47
|
+
[pager setValue:@100 forKey:@"headerHeight"];
|
|
48
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
49
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -144, 0.001);
|
|
50
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
51
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
52
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
53
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.x, 3, 0.001);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
- (void)testUIKitSizeChangesAndRefreshCycleDoNotAccumulateOffset
|
|
57
|
+
{
|
|
58
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
59
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
60
|
+
// UIKit ignores offscreen beginRefreshing calls. Exercise the actual refresh
|
|
61
|
+
// lifecycle in a visible window rather than weakening the state assertion.
|
|
62
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:scroll.frame];
|
|
63
|
+
UIViewController *controller = [UIViewController new];
|
|
64
|
+
window.rootViewController = controller;
|
|
65
|
+
scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
66
|
+
[controller.view addSubview:scroll];
|
|
67
|
+
window.hidden = NO;
|
|
68
|
+
[self addTeardownBlock:^{ window.hidden = YES; }];
|
|
69
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
70
|
+
scroll.refreshControl = control;
|
|
71
|
+
[window layoutIfNeeded];
|
|
72
|
+
[scroll layoutIfNeeded];
|
|
73
|
+
XCTAssertEqual(control.window, window);
|
|
74
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
75
|
+
CGRect bounds = control.bounds;
|
|
76
|
+
bounds.size = CGSizeMake(320, 60);
|
|
77
|
+
control.bounds = bounds;
|
|
78
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
79
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
80
|
+
[control beginRefreshing];
|
|
81
|
+
[control layoutIfNeeded];
|
|
82
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
83
|
+
XCTAssertTrue(control.isRefreshing);
|
|
84
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
85
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
86
|
+
[control endRefreshing];
|
|
87
|
+
[control layoutIfNeeded];
|
|
88
|
+
XCTAssertFalse(control.isRefreshing);
|
|
89
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
90
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
91
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
92
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
93
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
- (void)testReplacementAndNestedOwnerReleaseRestoreOnlyTheirContributions
|
|
97
|
+
{
|
|
98
|
+
RNCCollapsiblePagerViewComponentView *first = [self pagerWithHeader:64];
|
|
99
|
+
RNCCollapsiblePagerViewComponentView *second = [self pagerWithHeader:20];
|
|
100
|
+
UICollectionView *scroll = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)
|
|
101
|
+
collectionViewLayout:[UICollectionViewFlowLayout new]];
|
|
102
|
+
UIRefreshControl *original = [UIRefreshControl new];
|
|
103
|
+
scroll.refreshControl = original;
|
|
104
|
+
[first updateRefreshControlForScrollView:scroll];
|
|
105
|
+
[second updateRefreshControlForScrollView:scroll];
|
|
106
|
+
XCTAssertEqualWithAccuracy(original.layer.sublayerTransform.m42, -172, 0.001);
|
|
107
|
+
[first releaseRefreshControlForScrollView:scroll];
|
|
108
|
+
XCTAssertEqualWithAccuracy(original.layer.sublayerTransform.m42, -64, 0.001);
|
|
109
|
+
|
|
110
|
+
UIRefreshControl *replacement = [UIRefreshControl new];
|
|
111
|
+
replacement.bounds = CGRectMake(0, -8, 320, 60);
|
|
112
|
+
scroll.refreshControl = replacement;
|
|
113
|
+
XCTAssertTrue(CATransform3DIsIdentity(original.layer.sublayerTransform));
|
|
114
|
+
XCTAssertEqualWithAccuracy(replacement.layer.sublayerTransform.m42, -64, 0.001);
|
|
115
|
+
XCTAssertEqualWithAccuracy(replacement.bounds.origin.y, -8, 0.001);
|
|
116
|
+
[second prepareForRecycle];
|
|
117
|
+
XCTAssertEqualWithAccuracy(replacement.bounds.origin.y, -8, 0.001);
|
|
118
|
+
XCTAssertTrue(CATransform3DIsIdentity(replacement.layer.sublayerTransform));
|
|
119
|
+
scroll.refreshControl = nil;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
- (void)testPagerDestructionRestoresControl
|
|
123
|
+
{
|
|
124
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
125
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
126
|
+
scroll.refreshControl = control;
|
|
127
|
+
@autoreleasepool {
|
|
128
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
129
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
130
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
131
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
132
|
+
}
|
|
133
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
134
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
- (void)testCallerOffsetCollisionDoesNotAffectVisualContribution
|
|
138
|
+
{
|
|
139
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:56];
|
|
140
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
141
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
142
|
+
scroll.refreshControl = control;
|
|
143
|
+
control.bounds = CGRectMake(0, -200, 320, 60);
|
|
144
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
145
|
+
control.bounds = CGRectMake(0, -100, 320, 60);
|
|
146
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -100, 0.001);
|
|
147
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -100, 0.001);
|
|
148
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
149
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -100, 0.001);
|
|
150
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
- (void)testNestedOwnerDestructionPreservesBaselineAndRestoresClipping
|
|
154
|
+
{
|
|
155
|
+
RNCCollapsiblePagerViewComponentView *second = [self pagerWithHeader:20];
|
|
156
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
157
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
158
|
+
scroll.refreshControl = control;
|
|
159
|
+
CATransform3D baseline = CATransform3DMakeTranslation(7, 13, 0);
|
|
160
|
+
control.layer.sublayerTransform = baseline;
|
|
161
|
+
control.layer.masksToBounds = YES;
|
|
162
|
+
@autoreleasepool {
|
|
163
|
+
RNCCollapsiblePagerViewComponentView *first = [self pagerWithHeader:64];
|
|
164
|
+
[first updateRefreshControlForScrollView:scroll];
|
|
165
|
+
[second updateRefreshControlForScrollView:scroll];
|
|
166
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 172, 0.001);
|
|
167
|
+
XCTAssertFalse(control.layer.masksToBounds);
|
|
168
|
+
}
|
|
169
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 64, 0.001);
|
|
170
|
+
// A UIKit layer reset must not discard the remaining visual contribution.
|
|
171
|
+
control.layer.sublayerTransform = baseline;
|
|
172
|
+
control.layer.masksToBounds = YES;
|
|
173
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 64, 0.001);
|
|
174
|
+
XCTAssertFalse(control.layer.masksToBounds);
|
|
175
|
+
[second releaseRefreshControlForScrollView:scroll];
|
|
176
|
+
XCTAssertTrue(CATransform3DEqualToTransform(control.layer.sublayerTransform, baseline));
|
|
177
|
+
XCTAssertTrue(control.layer.masksToBounds);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@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.256",
|
|
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
|
},
|