@onekeyfe/react-native-pager-view 3.0.174 → 3.0.175
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +123 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +5 -0
- package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +4 -0
- package/docs/SPEC.md +9 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +124 -1
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +39 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +2 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +2 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerViewNativeComponent.ts +2 -0
|
@@ -204,6 +204,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
204
204
|
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
205
205
|
private var observedPageRemoved = false
|
|
206
206
|
private var attachmentGeneration = 0
|
|
207
|
+
internal var headerSettledAtTop = false
|
|
208
|
+
private set
|
|
209
|
+
var onHeaderSettledStateChanged: (() -> Unit)? = null
|
|
210
|
+
private var headerSettledOwner: WeakReference<View>? = null
|
|
211
|
+
private var headerSettledKey: String? = null
|
|
212
|
+
private var headerSettledGeneration = -1
|
|
213
|
+
private var headerSettledGeometry: String? = null
|
|
214
|
+
private var headerSettledSince = 0L
|
|
215
|
+
private var headerSettledWasBusy = true
|
|
216
|
+
private var headerSettledCheck: Runnable? = null
|
|
217
|
+
private var headerCommandOwner: WeakReference<View>? = null
|
|
218
|
+
|
|
207
219
|
private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
|
|
208
220
|
private val hostIdentity = System.identityHashCode(this)
|
|
209
221
|
private var headerView: View? = null
|
|
@@ -228,6 +240,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
228
240
|
finishPagerTopIfSettled()
|
|
229
241
|
// OneKey patch: a filtered short page can reduce the existing collapse range.
|
|
230
242
|
setHeaderOffset(headerOffsetPx)
|
|
243
|
+
updateHeaderSettledState()
|
|
231
244
|
if (offsetDiagEnabled() && offsetDiagNextDraw.isNotEmpty()) {
|
|
232
245
|
val samples = offsetDiagNextDraw.entries.map { it.key to it.value }
|
|
233
246
|
offsetDiagNextDraw.clear()
|
|
@@ -554,6 +567,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
554
567
|
}
|
|
555
568
|
|
|
556
569
|
internal fun onPagerScrollStateChanged(state: Int) {
|
|
570
|
+
updateHeaderSettledState(forceBusy = state != ViewPager2.SCROLL_STATE_IDLE)
|
|
557
571
|
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
558
572
|
beginPageTransition()
|
|
559
573
|
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
@@ -855,6 +869,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
855
869
|
if (phase == 0 && view === (observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) cancelPagerTop()
|
|
856
870
|
if (phase != 0 && matchesPagerTop(view)) { finishPagerTopIfSettled(); return }
|
|
857
871
|
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
872
|
+
val stateOwner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
873
|
+
val stateKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
874
|
+
if (view === stateOwner && stateKey == currentPageKey() && view.isAttachedToWindow && isAttachedToWindow && transitionHeaderOffsets == null && !syntheticPageRestore) {
|
|
875
|
+
updateHeaderSettledState()
|
|
876
|
+
if (phase == 1) headerCommandOwner = WeakReference(view)
|
|
877
|
+
else if (phase == 0 || phase == 2) headerCommandOwner = null
|
|
878
|
+
updateHeaderSettledState(forceBusy = phase != 2)
|
|
879
|
+
}
|
|
858
880
|
if (!nativeSmoothHeaderScrollEnabled || view !== owner || transitionHeaderOffsets != null || syntheticPageRestore ||
|
|
859
881
|
!isAttachedToWindow || !view.isAttachedToWindow) return
|
|
860
882
|
val key = currentPageKey()
|
|
@@ -1030,6 +1052,94 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1030
1052
|
return (contentHeight.roundToInt() - height).coerceIn(0, headerHeightPx)
|
|
1031
1053
|
}
|
|
1032
1054
|
|
|
1055
|
+
internal fun headerStatePageKey(): String = currentPageKey()
|
|
1056
|
+
|
|
1057
|
+
private fun publishHeaderSettledState(settled: Boolean, identityChanged: Boolean = false) {
|
|
1058
|
+
if (headerSettledAtTop == settled && !identityChanged) return
|
|
1059
|
+
headerSettledAtTop = settled
|
|
1060
|
+
log("header-settled-state page=$selectedPage generation=$attachmentGeneration settled=$settled")
|
|
1061
|
+
onHeaderSettledStateChanged?.invoke()
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private fun cancelHeaderSettledCheck() {
|
|
1065
|
+
headerSettledCheck?.let(::removeCallbacks)
|
|
1066
|
+
headerSettledCheck = null
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private fun updateHeaderSettledState(forceBusy: Boolean = false) {
|
|
1070
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
1071
|
+
val ownerKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
1072
|
+
val valid = isAttachedToWindow && width > 0 && height > 0 && headerHeightPx > 0 &&
|
|
1073
|
+
headerView?.isAttachedToWindow == true && owner != null && owner.isAttachedToWindow &&
|
|
1074
|
+
owner.width > 0 && owner.height > 0 && ownerKey == currentPageKey() &&
|
|
1075
|
+
selectedPage in 0 until adapter.itemCount && isDescendant(owner, adapter.pageAt(selectedPage))
|
|
1076
|
+
val identityChanged = headerSettledOwner?.get() !== owner || headerSettledKey != currentPageKey() ||
|
|
1077
|
+
headerSettledGeneration != attachmentGeneration
|
|
1078
|
+
if (identityChanged) {
|
|
1079
|
+
cancelHeaderSettledCheck()
|
|
1080
|
+
headerCommandOwner = null
|
|
1081
|
+
headerSettledOwner = owner?.let(::WeakReference)
|
|
1082
|
+
headerSettledKey = currentPageKey()
|
|
1083
|
+
headerSettledGeneration = attachmentGeneration
|
|
1084
|
+
headerSettledGeometry = null
|
|
1085
|
+
headerSettledWasBusy = true
|
|
1086
|
+
publishHeaderSettledState(false, true)
|
|
1087
|
+
}
|
|
1088
|
+
if (!valid || owner == null || headerOffsetPx != 0) {
|
|
1089
|
+
cancelHeaderSettledCheck()
|
|
1090
|
+
headerSettledGeometry = null
|
|
1091
|
+
headerSettledWasBusy = true
|
|
1092
|
+
publishHeaderSettledState(false)
|
|
1093
|
+
return
|
|
1094
|
+
}
|
|
1095
|
+
val windowPoint = IntArray(2)
|
|
1096
|
+
owner.getLocationInWindow(windowPoint)
|
|
1097
|
+
val recycler = owner as? RecyclerView
|
|
1098
|
+
val manager = recycler?.layoutManager as? LinearLayoutManager
|
|
1099
|
+
val first = manager?.findFirstVisibleItemPosition() ?: -1
|
|
1100
|
+
val firstTop = manager?.let { layoutManager ->
|
|
1101
|
+
layoutManager.findViewByPosition(first)?.let(layoutManager::getDecoratedTop)
|
|
1102
|
+
} ?: 0
|
|
1103
|
+
val content = (owner as? ViewGroup)?.getChildAt(0)
|
|
1104
|
+
val geometry = listOf(headerOffsetPx, headerHeightPx, stickyHeaderHeightPx, width, height,
|
|
1105
|
+
owner.width, owner.height, owner.paddingTop, owner.paddingBottom, owner.scrollY,
|
|
1106
|
+
windowPoint[0], windowPoint[1], first, firstTop, recycler?.adapter?.itemCount ?: 0,
|
|
1107
|
+
content?.height ?: 0, headerView?.height ?: 0).joinToString(":")
|
|
1108
|
+
val busy = forceBusy || pager.isFakeDragging || pager.scrollState != ViewPager2.SCROLL_STATE_IDLE ||
|
|
1109
|
+
transitionHeaderOffsets != null || syntheticPageRestore || pagerTopRequest != null ||
|
|
1110
|
+
headerCommandOwner?.get() === owner ||
|
|
1111
|
+
nestedScrollingParentHelper.nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 ||
|
|
1112
|
+
(recycler != null && (recycler.scrollState != RecyclerView.SCROLL_STATE_IDLE ||
|
|
1113
|
+
recycler.isComputingLayout || recycler.layoutManager?.isSmoothScrolling == true)) ||
|
|
1114
|
+
(owner is NativeScrollerViewport && owner.isReadingMotionActive())
|
|
1115
|
+
val changed = geometry != headerSettledGeometry
|
|
1116
|
+
val now = SystemClock.uptimeMillis()
|
|
1117
|
+
if (changed || busy || headerSettledWasBusy) {
|
|
1118
|
+
headerSettledSince = now
|
|
1119
|
+
headerSettledGeometry = geometry
|
|
1120
|
+
cancelHeaderSettledCheck()
|
|
1121
|
+
publishHeaderSettledState(false)
|
|
1122
|
+
}
|
|
1123
|
+
headerSettledWasBusy = busy
|
|
1124
|
+
if (!busy && now - headerSettledSince >= 150L) {
|
|
1125
|
+
cancelHeaderSettledCheck()
|
|
1126
|
+
publishHeaderSettledState(true)
|
|
1127
|
+
return
|
|
1128
|
+
}
|
|
1129
|
+
if (headerSettledCheck == null) {
|
|
1130
|
+
val weakOwner = WeakReference(owner)
|
|
1131
|
+
val key = currentPageKey()
|
|
1132
|
+
val generation = attachmentGeneration
|
|
1133
|
+
val check = Runnable {
|
|
1134
|
+
headerSettledCheck = null
|
|
1135
|
+
if (generation == attachmentGeneration && key == currentPageKey() && weakOwner.get() ===
|
|
1136
|
+
(observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) updateHeaderSettledState()
|
|
1137
|
+
}
|
|
1138
|
+
headerSettledCheck = check
|
|
1139
|
+
postDelayed(check, 150L)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1033
1143
|
private fun setHeaderOffset(offset: Int) {
|
|
1034
1144
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
1035
1145
|
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
@@ -1037,6 +1147,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1037
1147
|
val next = offset.coerceIn(0, maximum)
|
|
1038
1148
|
if (headerOffsetPx == next) return
|
|
1039
1149
|
headerOffsetPx = next
|
|
1150
|
+
updateHeaderSettledState(forceBusy = true)
|
|
1040
1151
|
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
1041
1152
|
pageHeaderOffsets[currentPageKey()] = next
|
|
1042
1153
|
}
|
|
@@ -1195,9 +1306,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1195
1306
|
val key = currentPageKey()
|
|
1196
1307
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
1197
1308
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
1309
|
+
if (observedRecyclerView === recyclerView) updateHeaderSettledState(forceBusy = newState != RecyclerView.SCROLL_STATE_IDLE)
|
|
1198
1310
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) finishPagerTopIfSettled()
|
|
1199
1311
|
}
|
|
1200
1312
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
1313
|
+
if (observedRecyclerView === recyclerView && (dx != 0 || dy != 0)) updateHeaderSettledState(forceBusy = true)
|
|
1201
1314
|
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
1202
1315
|
key != currentPageKey() || !isAttachedToWindow ||
|
|
1203
1316
|
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) {
|
|
@@ -1327,6 +1440,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1327
1440
|
}
|
|
1328
1441
|
|
|
1329
1442
|
private fun detachPrimaryScrollObserver(recordOffset: Boolean = true) {
|
|
1443
|
+
cancelHeaderSettledCheck()
|
|
1444
|
+
headerCommandOwner = null
|
|
1445
|
+
headerSettledGeometry = null
|
|
1446
|
+
headerSettledWasBusy = true
|
|
1447
|
+
publishHeaderSettledState(false)
|
|
1330
1448
|
detachOwnedScrollerObserver(recordOffset)
|
|
1331
1449
|
detachRecyclerObserver(recordOffset)
|
|
1332
1450
|
detachNativeScrollerObserver(recordOffset)
|
|
@@ -2070,6 +2188,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2070
2188
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
2071
2189
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
2072
2190
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
2191
|
+
updateHeaderSettledState(forceBusy = true)
|
|
2073
2192
|
if (type == ViewCompat.TYPE_TOUCH) {
|
|
2074
2193
|
val request = pageRestoreRequests[currentPageKey()]
|
|
2075
2194
|
offsetDiag("touch-cancel-before", target, request = request)
|
|
@@ -2084,6 +2203,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2084
2203
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
2085
2204
|
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
2086
2205
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
2206
|
+
updateHeaderSettledState()
|
|
2087
2207
|
onHeaderOffsetChanged?.invoke()
|
|
2088
2208
|
}
|
|
2089
2209
|
|
|
@@ -2191,6 +2311,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2191
2311
|
}
|
|
2192
2312
|
|
|
2193
2313
|
override fun onDetachedFromWindow() {
|
|
2314
|
+
cancelHeaderSettledCheck()
|
|
2315
|
+
headerCommandOwner = null
|
|
2316
|
+
publishHeaderSettledState(false)
|
|
2194
2317
|
pendingRefreshPull = null
|
|
2195
2318
|
releaseRefreshForeground()
|
|
2196
2319
|
attachmentGeneration += 1
|
|
@@ -94,6 +94,9 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
94
94
|
),
|
|
95
95
|
)
|
|
96
96
|
}
|
|
97
|
+
host.onHeaderSettledStateChanged = {
|
|
98
|
+
dispatchDiagnostics(reactContext, host, "header-settled-state")
|
|
99
|
+
}
|
|
97
100
|
host.onHeaderOffsetChanged = {
|
|
98
101
|
if (!host.pager.isFakeDragging && host.pager.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
|
|
99
102
|
dispatchDiagnostics(reactContext, host, "vertical-idle")
|
|
@@ -127,6 +130,8 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
127
130
|
host.observedScrollableCount(),
|
|
128
131
|
host.retainedPages,
|
|
129
132
|
reason,
|
|
133
|
+
host.headerStatePageKey(),
|
|
134
|
+
host.headerSettledAtTop,
|
|
130
135
|
),
|
|
131
136
|
)
|
|
132
137
|
}
|
package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt
CHANGED
|
@@ -14,6 +14,8 @@ class CollapsibleStateChangedEvent(
|
|
|
14
14
|
private val observedScrollableCount: Int,
|
|
15
15
|
private val retainedPages: String,
|
|
16
16
|
private val reason: String,
|
|
17
|
+
private val pageKey: String,
|
|
18
|
+
private val headerSettledAtTop: Boolean,
|
|
17
19
|
) : Event<CollapsibleStateChangedEvent>(viewTag) {
|
|
18
20
|
override fun getEventName() = EVENT_NAME
|
|
19
21
|
|
|
@@ -31,6 +33,8 @@ class CollapsibleStateChangedEvent(
|
|
|
31
33
|
putInt("observedScrollableCount", observedScrollableCount)
|
|
32
34
|
putString("retainedPages", retainedPages)
|
|
33
35
|
putString("reason", reason)
|
|
36
|
+
putString("pageKey", pageKey)
|
|
37
|
+
putBoolean("headerSettledAtTop", headerSettledAtTop)
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
companion object {
|
package/docs/SPEC.md
CHANGED
|
@@ -717,3 +717,12 @@ cases additionally verified recorded refresh, header, restoration and gesture
|
|
|
717
717
|
paths; they do not establish every possible sequence or runtime acceptance of a
|
|
718
718
|
newly installed module release. Physical-device feel and semantic accessibility
|
|
719
719
|
activation remain unverified.
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
### Native Header stability snapshot
|
|
723
|
+
|
|
724
|
+
`onCollapsibleStateChanged` retains its existing diagnostic fields and reasons and adds native-only `pageKey: string` and `headerSettledAtTop: boolean`. The key is the actual mounted React child key, not an application tab identifier. Unattached, unknown, replaced or transitioning owners MUST report false. A mounted current owner emits an initial identity snapshot without requiring a first user scroll.
|
|
725
|
+
|
|
726
|
+
The Header is settled only when C is at its expanded top, the current attached view/key/index/generation still matches, and there is no actual drag, deceleration, scrolling command or horizontal transition. Offset/inset/content/header geometry MUST remain stable for 150ms before true is emitted. Motion or geometry changes revoke true promptly. Deferred reading memory alone MUST NOT be treated as active motion. Pull-to-refresh may settle at a normalized negative offset after drag, bounce and inset geometry stop; network refreshing alone is not a motion gate. Checks stop for collapsed, detached and recycled owners; delayed callbacks revalidate their captured identity. State changes are deduplicated; identity changes still publish an unknown/false snapshot.
|
|
727
|
+
|
|
728
|
+
The native Home Spotlight adapter consumes this snapshot in the main UI runtime and keeps unknown states paused. It MUST use the mounted page-key mapping and reject old page/source callbacks. Native snapshots already include the 150ms stabilization delay: consumers MUST NOT add another such delay. Legacy Tabs keep their existing normalized-offset and debounce behavior. Anchor measurement, guide qualification and the existing display delay remain independent gates.
|
|
@@ -1517,6 +1517,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1517
1517
|
#endif
|
|
1518
1518
|
NSInteger _transitionSourceIndex;
|
|
1519
1519
|
BOOL _syntheticPageRestore;
|
|
1520
|
+
BOOL _headerSettledAtTop;
|
|
1521
|
+
__weak UIScrollView *_headerSettledOwner;
|
|
1522
|
+
NSString *_headerSettledKey;
|
|
1523
|
+
NSUInteger _headerSettledGeneration;
|
|
1524
|
+
NSString *_headerSettledGeometry;
|
|
1525
|
+
CFTimeInterval _headerSettledSince;
|
|
1526
|
+
BOOL _headerSettledWasBusy;
|
|
1527
|
+
NSTimer *_headerSettledTimer;
|
|
1528
|
+
__weak UIScrollView *_headerCommandOwner;
|
|
1529
|
+
|
|
1520
1530
|
BOOL _finishingPageStateTransition;
|
|
1521
1531
|
NSMutableSet<NSString *> *_pendingPageRestoreKeys;
|
|
1522
1532
|
__weak UIScrollView *_sharedHeaderScrollView;
|
|
@@ -1677,6 +1687,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1677
1687
|
{
|
|
1678
1688
|
[super didMoveToWindow];
|
|
1679
1689
|
[self updateSharedHeaderPressCancellationGesture];
|
|
1690
|
+
[self updateHeaderSettledStateForceBusy:NO];
|
|
1680
1691
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
1681
1692
|
[self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight];
|
|
1682
1693
|
}
|
|
@@ -1703,6 +1714,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1703
1714
|
|
|
1704
1715
|
- (void)dealloc
|
|
1705
1716
|
{
|
|
1717
|
+
[self cancelHeaderSettledCheck];
|
|
1706
1718
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
|
|
1707
1719
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
1708
1720
|
[self updateNativeScrollerViewport:scrollView stickyHeight:0];
|
|
@@ -2363,6 +2375,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2363
2375
|
}
|
|
2364
2376
|
}
|
|
2365
2377
|
[self layoutSharedHeaders];
|
|
2378
|
+
[self updateHeaderSettledStateForceBusy:NO];
|
|
2366
2379
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2367
2380
|
if (!self->_isBeingRecycled) {
|
|
2368
2381
|
[self prepareAdjacentPageInsets];
|
|
@@ -2373,6 +2386,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2373
2386
|
|
|
2374
2387
|
- (void)prepareForRecycle
|
|
2375
2388
|
{
|
|
2389
|
+
[self cancelHeaderSettledCheck];
|
|
2390
|
+
[self publishHeaderSettledAtTop:NO identityChanged:NO];
|
|
2376
2391
|
_isBeingRecycled = YES;
|
|
2377
2392
|
_generation++;
|
|
2378
2393
|
[_mountingRemovedScrollViews removeAllObjects];
|
|
@@ -2896,10 +2911,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2896
2911
|
_currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
|
|
2897
2912
|
[self attachSharedHeadersToScrollView:candidate];
|
|
2898
2913
|
[self updateSharedHeaderPressCancellationGesture];
|
|
2914
|
+
[self updateHeaderSettledStateForceBusy:NO];
|
|
2899
2915
|
}
|
|
2900
2916
|
|
|
2901
2917
|
- (void)detachScrollObserver
|
|
2902
2918
|
{
|
|
2919
|
+
[self cancelHeaderSettledCheck];
|
|
2920
|
+
_headerCommandOwner = nil;
|
|
2921
|
+
_headerSettledGeometry = nil;
|
|
2922
|
+
_headerSettledWasBusy = YES;
|
|
2923
|
+
[self publishHeaderSettledAtTop:NO identityChanged:NO];
|
|
2903
2924
|
[self cancelPagerTop];
|
|
2904
2925
|
UIScrollView *scrollView = _observedScrollView;
|
|
2905
2926
|
if (scrollView != nil) {
|
|
@@ -3117,6 +3138,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3117
3138
|
|
|
3118
3139
|
- (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer
|
|
3119
3140
|
{
|
|
3141
|
+
if (recognizer == _observedScrollView.panGestureRecognizer) {
|
|
3142
|
+
[self updateHeaderSettledStateForceBusy:
|
|
3143
|
+
recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged];
|
|
3144
|
+
}
|
|
3120
3145
|
if (!_nativeSmoothHeaderScrollEnabled || recognizer != _observedScrollView.panGestureRecognizer) {
|
|
3121
3146
|
return;
|
|
3122
3147
|
}
|
|
@@ -3454,6 +3479,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3454
3479
|
_observedPageIndex != _currentIndex || _observedPageGeneration != _generation ||
|
|
3455
3480
|
_transitioning || _isPagerDragging || _transitionPageOffsets != nil || _syntheticPageRestore) return;
|
|
3456
3481
|
NSInteger phase = [notification.userInfo[@"phase"] integerValue];
|
|
3482
|
+
[self updateHeaderSettledStateForceBusy:NO];
|
|
3483
|
+
if (phase == 1) _headerCommandOwner = scrollView;
|
|
3484
|
+
else if (phase == 0 || phase == 2) _headerCommandOwner = nil;
|
|
3485
|
+
[self updateHeaderSettledStateForceBusy:phase != 2];
|
|
3457
3486
|
if ([self matchesPagerTop:scrollView]) {
|
|
3458
3487
|
if (phase == 0) [self cancelPagerTop];
|
|
3459
3488
|
else return;
|
|
@@ -3569,6 +3598,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3569
3598
|
return;
|
|
3570
3599
|
}
|
|
3571
3600
|
if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
|
|
3601
|
+
if (![change[NSKeyValueChangeOldKey] isEqual:change[NSKeyValueChangeNewKey]]) {
|
|
3602
|
+
[self updateHeaderSettledStateForceBusy:YES];
|
|
3603
|
+
}
|
|
3572
3604
|
if ((_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:_observedPageKey]) &&
|
|
3573
3605
|
(_observedPageGeneration != _generation || _observedPageKey == nil ||
|
|
3574
3606
|
_observedPageIndex != _currentIndex || ![_observedPageKey isEqualToString:self.currentPageKey] ||
|
|
@@ -3668,6 +3700,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3668
3700
|
|
|
3669
3701
|
- (void)applyHeaderOffset
|
|
3670
3702
|
{
|
|
3703
|
+
[self updateHeaderSettledStateForceBusy:NO];
|
|
3671
3704
|
BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
|
|
3672
3705
|
_sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
|
|
3673
3706
|
if (attachedToList) {
|
|
@@ -3884,6 +3917,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3884
3917
|
|
|
3885
3918
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
|
3886
3919
|
{
|
|
3920
|
+
[self updateHeaderSettledStateForceBusy:YES];
|
|
3887
3921
|
[self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
|
|
3888
3922
|
if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
|
|
3889
3923
|
[_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
|
|
@@ -4081,6 +4115,93 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
4081
4115
|
if (emitter) emitter->onPageSelected({.position = (double)position});
|
|
4082
4116
|
}
|
|
4083
4117
|
|
|
4118
|
+
- (void)publishHeaderSettledAtTop:(BOOL)settled identityChanged:(BOOL)identityChanged
|
|
4119
|
+
{
|
|
4120
|
+
if (_headerSettledAtTop == settled && !identityChanged) return;
|
|
4121
|
+
_headerSettledAtTop = settled;
|
|
4122
|
+
RNCCollapsiblePagerLog([NSString stringWithFormat:@"header-settled-state page=%ld generation=%lu settled=%d",
|
|
4123
|
+
(long)_currentIndex, (unsigned long)_generation, settled]);
|
|
4124
|
+
[self emitDiagnostics:@"header-settled-state"];
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
- (void)cancelHeaderSettledCheck
|
|
4128
|
+
{
|
|
4129
|
+
[_headerSettledTimer invalidate];
|
|
4130
|
+
_headerSettledTimer = nil;
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
- (void)updateHeaderSettledStateForceBusy:(BOOL)forceBusy
|
|
4134
|
+
{
|
|
4135
|
+
UIScrollView *owner = _observedScrollView;
|
|
4136
|
+
NSString *key = self.currentPageKey;
|
|
4137
|
+
BOOL valid = !_isBeingRecycled && self.window != nil && _headerView.window == self.window &&
|
|
4138
|
+
_headerHeight > 0 && owner.window == self.window && owner != nil &&
|
|
4139
|
+
CGRectGetWidth(owner.bounds) > 0 && CGRectGetHeight(owner.bounds) > 0 &&
|
|
4140
|
+
_observedPageGeneration == _generation && _observedPageIndex == _currentIndex &&
|
|
4141
|
+
_currentIndex >= 0 && _currentIndex < _pageControllers.count &&
|
|
4142
|
+
[_observedPageKey isEqualToString:key] && [owner isDescendantOfView:_pageControllers[_currentIndex].view];
|
|
4143
|
+
BOOL identityChanged = _headerSettledOwner != owner || ![_headerSettledKey isEqualToString:key] ||
|
|
4144
|
+
_headerSettledGeneration != _generation;
|
|
4145
|
+
if (identityChanged) {
|
|
4146
|
+
[self cancelHeaderSettledCheck];
|
|
4147
|
+
_headerCommandOwner = nil;
|
|
4148
|
+
_headerSettledOwner = owner;
|
|
4149
|
+
_headerSettledKey = [key copy];
|
|
4150
|
+
_headerSettledGeneration = _generation;
|
|
4151
|
+
_headerSettledGeometry = nil;
|
|
4152
|
+
_headerSettledWasBusy = YES;
|
|
4153
|
+
[self publishHeaderSettledAtTop:NO identityChanged:YES];
|
|
4154
|
+
}
|
|
4155
|
+
if (!valid || fabs(_headerOffset) > 0.5) {
|
|
4156
|
+
[self cancelHeaderSettledCheck];
|
|
4157
|
+
_headerSettledGeometry = nil;
|
|
4158
|
+
_headerSettledWasBusy = YES;
|
|
4159
|
+
[self publishHeaderSettledAtTop:NO identityChanged:NO];
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
CGRect headerRect = [_headerView convertRect:_headerView.bounds toView:self.window];
|
|
4163
|
+
CGRect ownerRect = [owner convertRect:owner.bounds toView:self.window];
|
|
4164
|
+
UIEdgeInsets inset = owner.contentInset;
|
|
4165
|
+
UIEdgeInsets adjustedInset = owner.adjustedContentInset;
|
|
4166
|
+
NSString *geometry = [NSString stringWithFormat:
|
|
4167
|
+
@"%.4f:%ld:%ld:%@:%@:%@:%@:%@:%@:%@",
|
|
4168
|
+
_headerOffset, (long)_headerHeight, (long)_stickyHeaderHeight,
|
|
4169
|
+
NSStringFromCGPoint(owner.contentOffset), NSStringFromCGSize(owner.contentSize),
|
|
4170
|
+
NSStringFromCGRect(owner.bounds), NSStringFromUIEdgeInsets(inset),
|
|
4171
|
+
NSStringFromUIEdgeInsets(adjustedInset), NSStringFromCGRect(headerRect), NSStringFromCGRect(ownerRect)];
|
|
4172
|
+
UIGestureRecognizerState panState = owner.panGestureRecognizer.state;
|
|
4173
|
+
BOOL busy = forceBusy || _transitioning || _isPagerDragging || _transitionPageOffsets != nil ||
|
|
4174
|
+
_syntheticPageRestore || _pagerTopPageKey != nil || _headerCommandOwner == owner ||
|
|
4175
|
+
owner.isDragging || owner.isDecelerating || owner.isTracking ||
|
|
4176
|
+
panState == UIGestureRecognizerStateBegan || panState == UIGestureRecognizerStateChanged;
|
|
4177
|
+
CFTimeInterval now = CACurrentMediaTime();
|
|
4178
|
+
if (![geometry isEqualToString:_headerSettledGeometry] || busy || _headerSettledWasBusy) {
|
|
4179
|
+
_headerSettledSince = now;
|
|
4180
|
+
_headerSettledGeometry = geometry;
|
|
4181
|
+
[self cancelHeaderSettledCheck];
|
|
4182
|
+
[self publishHeaderSettledAtTop:NO identityChanged:NO];
|
|
4183
|
+
}
|
|
4184
|
+
_headerSettledWasBusy = busy;
|
|
4185
|
+
if (!busy && now - _headerSettledSince >= 0.15) {
|
|
4186
|
+
[self cancelHeaderSettledCheck];
|
|
4187
|
+
[self publishHeaderSettledAtTop:YES identityChanged:NO];
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (_headerSettledTimer == nil) {
|
|
4191
|
+
__weak RNCCollapsiblePagerViewComponentView *weakSelf = self;
|
|
4192
|
+
__weak UIScrollView *weakOwner = owner;
|
|
4193
|
+
NSUInteger generation = _generation;
|
|
4194
|
+
_headerSettledTimer = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:NO block:^(NSTimer *timer) {
|
|
4195
|
+
RNCCollapsiblePagerViewComponentView *strongSelf = weakSelf;
|
|
4196
|
+
if (strongSelf == nil) return;
|
|
4197
|
+
if (strongSelf->_headerSettledTimer != timer) return;
|
|
4198
|
+
strongSelf->_headerSettledTimer = nil;
|
|
4199
|
+
if (strongSelf->_generation == generation && [strongSelf.currentPageKey isEqualToString:key] &&
|
|
4200
|
+
strongSelf->_observedScrollView == weakOwner) [strongSelf updateHeaderSettledStateForceBusy:NO];
|
|
4201
|
+
}];
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4084
4205
|
- (void)emitDiagnostics:(NSString *)reason
|
|
4085
4206
|
{
|
|
4086
4207
|
const auto emitter = [self eventEmitter];
|
|
@@ -4096,7 +4217,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
4096
4217
|
.attachedPageCount = (int)attached,
|
|
4097
4218
|
.observedScrollableCount = (int)_originalInsets.count,
|
|
4098
4219
|
.retainedPages = std::string(_retainedPages.UTF8String ?: "[]"),
|
|
4099
|
-
.reason = std::string(reason.UTF8String ?: "unknown")
|
|
4220
|
+
.reason = std::string(reason.UTF8String ?: "unknown"),
|
|
4221
|
+
.pageKey = std::string(self.currentPageKey.UTF8String ?: ""),
|
|
4222
|
+
.headerSettledAtTop = (bool)_headerSettledAtTop
|
|
4100
4223
|
});
|
|
4101
4224
|
}
|
|
4102
4225
|
|
|
@@ -19,6 +19,7 @@ using namespace facebook::react;
|
|
|
19
19
|
- (void)updatePageStateTransitionProgress:(CGFloat)progress;
|
|
20
20
|
- (void)finishPageStateTransition;
|
|
21
21
|
- (void)scrollToTop:(NSInteger)target animated:(BOOL)animated;
|
|
22
|
+
- (void)updateHeaderSettledStateForceBusy:(BOOL)forceBusy;
|
|
22
23
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)index;
|
|
23
24
|
- (void)applyInsetsToScrollView:(UIScrollView *)scrollView pageIndex:(NSInteger)index restore:(BOOL)restore;
|
|
24
25
|
@end
|
|
@@ -33,6 +34,44 @@ using namespace facebook::react;
|
|
|
33
34
|
|
|
34
35
|
@implementation RNCCollapsiblePagerFirstFrameTests
|
|
35
36
|
|
|
37
|
+
- (void)testHeaderSettledAtTopRequiresCurrentQuietOwnerAndCancelsDetachedTimer
|
|
38
|
+
{
|
|
39
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
40
|
+
// The fixture mounts page 0 directly; UIKit must not select it again.
|
|
41
|
+
[pager setValue:@YES forKey:@"hasAppliedInitialPage"];
|
|
42
|
+
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 120)];
|
|
43
|
+
[pager addSubview:header];
|
|
44
|
+
[pager setValue:header forKey:@"headerView"];
|
|
45
|
+
RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
|
|
46
|
+
[pager.window layoutIfNeeded];
|
|
47
|
+
[pager layoutIfNeeded];
|
|
48
|
+
XCTestExpectation *initialLayout = [self expectationWithDescription:@"Initial observer attachment drained"];
|
|
49
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [initialLayout fulfill]; });
|
|
50
|
+
[self waitForExpectations:@[initialLayout] timeout:1];
|
|
51
|
+
[pager detachScrollObserver];
|
|
52
|
+
[pager attachScrollObserverForCurrentPage];
|
|
53
|
+
[pager updateHeaderSettledStateForceBusy:NO];
|
|
54
|
+
XCTAssertFalse([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
55
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
|
|
56
|
+
XCTAssertTrue([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
57
|
+
|
|
58
|
+
[scroller scrollTo:0 y:200 - scroller.scrollView.contentInset.top animated:NO];
|
|
59
|
+
XCTAssertFalse([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
60
|
+
[scroller scrollTo:0 y:-scroller.scrollView.adjustedContentInset.top animated:NO];
|
|
61
|
+
[pager updateHeaderSettledStateForceBusy:NO];
|
|
62
|
+
XCTAssertFalse([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
63
|
+
[pager detachScrollObserver];
|
|
64
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
|
|
65
|
+
XCTAssertFalse([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
66
|
+
XCTAssertNil([pager valueForKey:@"headerSettledTimer"]);
|
|
67
|
+
|
|
68
|
+
[pager attachScrollObserverForCurrentPage];
|
|
69
|
+
[pager updateHeaderSettledStateForceBusy:NO];
|
|
70
|
+
XCTAssertFalse([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
71
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
|
|
72
|
+
XCTAssertTrue([[pager valueForKey:@"headerSettledAtTop"] boolValue]);
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
- (void)testReachableTopCommandBeforeLayoutMustNotReplayPendingPagePosition
|
|
37
76
|
{
|
|
38
77
|
for (NSNumber *command in @[@0, @1, @2]) {
|
|
@@ -19,6 +19,8 @@ export type OnCollapsibleStateChangedEventData = Readonly<{
|
|
|
19
19
|
observedScrollableCount: Int32;
|
|
20
20
|
retainedPages: string;
|
|
21
21
|
reason: string;
|
|
22
|
+
pageKey: string;
|
|
23
|
+
headerSettledAtTop: boolean;
|
|
22
24
|
}>;
|
|
23
25
|
export type OnNativeTabPressEventData = Readonly<{
|
|
24
26
|
position: Int32;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.175",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"typescript": "^5.9.2"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@onekeyfe/react-native-native-logger": "3.0.
|
|
75
|
+
"@onekeyfe/react-native-native-logger": "3.0.175",
|
|
76
76
|
"react": "*",
|
|
77
77
|
"react-native": "*"
|
|
78
78
|
},
|