@onekeyfe/react-native-pager-view 3.0.174 → 3.0.176
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 +151 -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 +15 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +124 -1
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +39 -0
- package/lib/module/CollapsiblePagerView.web.js +60 -11
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +2 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +5 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +2 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.web.tsx +90 -14
- package/src/CollapsiblePagerViewNativeComponent.ts +2 -0
|
@@ -123,6 +123,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
123
123
|
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
124
124
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
125
125
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
126
|
+
private val deferredScrollerGeometry = WeakHashMap<ScrollView, Triple<Int, Int, Int>>()
|
|
126
127
|
private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
|
|
127
128
|
private val ownedScrollerRestoreRequests = HashMap<String, Pair<WeakReference<NativeScrollerViewport>, Int?>>()
|
|
128
129
|
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
@@ -204,6 +205,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
204
205
|
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
205
206
|
private var observedPageRemoved = false
|
|
206
207
|
private var attachmentGeneration = 0
|
|
208
|
+
internal var headerSettledAtTop = false
|
|
209
|
+
private set
|
|
210
|
+
var onHeaderSettledStateChanged: (() -> Unit)? = null
|
|
211
|
+
private var headerSettledOwner: WeakReference<View>? = null
|
|
212
|
+
private var headerSettledKey: String? = null
|
|
213
|
+
private var headerSettledGeneration = -1
|
|
214
|
+
private var headerSettledGeometry: String? = null
|
|
215
|
+
private var headerSettledSince = 0L
|
|
216
|
+
private var headerSettledWasBusy = true
|
|
217
|
+
private var headerSettledCheck: Runnable? = null
|
|
218
|
+
private var headerCommandOwner: WeakReference<View>? = null
|
|
219
|
+
|
|
207
220
|
private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
|
|
208
221
|
private val hostIdentity = System.identityHashCode(this)
|
|
209
222
|
private var headerView: View? = null
|
|
@@ -228,6 +241,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
228
241
|
finishPagerTopIfSettled()
|
|
229
242
|
// OneKey patch: a filtered short page can reduce the existing collapse range.
|
|
230
243
|
setHeaderOffset(headerOffsetPx)
|
|
244
|
+
updateHeaderSettledState()
|
|
231
245
|
if (offsetDiagEnabled() && offsetDiagNextDraw.isNotEmpty()) {
|
|
232
246
|
val samples = offsetDiagNextDraw.entries.map { it.key to it.value }
|
|
233
247
|
offsetDiagNextDraw.clear()
|
|
@@ -500,6 +514,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
500
514
|
alignedBodyKeys.clear()
|
|
501
515
|
transitionAlignedViews.clear()
|
|
502
516
|
deferredRecyclerGeometry.clear()
|
|
517
|
+
deferredScrollerGeometry.clear()
|
|
503
518
|
displayBodyOverrides.clear()
|
|
504
519
|
displayRecyclerAnchors.clear()
|
|
505
520
|
ownedScrollerRestoreRequests.clear()
|
|
@@ -554,6 +569,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
554
569
|
}
|
|
555
570
|
|
|
556
571
|
internal fun onPagerScrollStateChanged(state: Int) {
|
|
572
|
+
updateHeaderSettledState(forceBusy = state != ViewPager2.SCROLL_STATE_IDLE)
|
|
557
573
|
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
558
574
|
beginPageTransition()
|
|
559
575
|
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
@@ -855,6 +871,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
855
871
|
if (phase == 0 && view === (observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) cancelPagerTop()
|
|
856
872
|
if (phase != 0 && matchesPagerTop(view)) { finishPagerTopIfSettled(); return }
|
|
857
873
|
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
874
|
+
val stateOwner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
875
|
+
val stateKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
876
|
+
if (view === stateOwner && stateKey == currentPageKey() && view.isAttachedToWindow && isAttachedToWindow && transitionHeaderOffsets == null && !syntheticPageRestore) {
|
|
877
|
+
updateHeaderSettledState()
|
|
878
|
+
if (phase == 1) headerCommandOwner = WeakReference(view)
|
|
879
|
+
else if (phase == 0 || phase == 2) headerCommandOwner = null
|
|
880
|
+
updateHeaderSettledState(forceBusy = phase != 2)
|
|
881
|
+
}
|
|
858
882
|
if (!nativeSmoothHeaderScrollEnabled || view !== owner || transitionHeaderOffsets != null || syntheticPageRestore ||
|
|
859
883
|
!isAttachedToWindow || !view.isAttachedToWindow) return
|
|
860
884
|
val key = currentPageKey()
|
|
@@ -885,6 +909,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
885
909
|
pendingPageRestores.remove(key)
|
|
886
910
|
ownedScrollerRestoreRequests.remove(key)
|
|
887
911
|
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
912
|
+
if (view is ScrollView) deferredScrollerGeometry.remove(view)
|
|
888
913
|
(view as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
889
914
|
suspendedBodyKeys.remove(key)
|
|
890
915
|
alignedBodyKeys.remove(key)
|
|
@@ -963,6 +988,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
963
988
|
displayRecyclerAnchors.remove(key)
|
|
964
989
|
recyclerReadingAnchors.remove(key)
|
|
965
990
|
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
991
|
+
if (view is ScrollView) deferredScrollerGeometry.remove(view)
|
|
966
992
|
pageOffsets[key] = 0
|
|
967
993
|
val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, target, ++pagerTopToken)
|
|
968
994
|
pagerTopRequest = request
|
|
@@ -1030,6 +1056,94 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1030
1056
|
return (contentHeight.roundToInt() - height).coerceIn(0, headerHeightPx)
|
|
1031
1057
|
}
|
|
1032
1058
|
|
|
1059
|
+
internal fun headerStatePageKey(): String = currentPageKey()
|
|
1060
|
+
|
|
1061
|
+
private fun publishHeaderSettledState(settled: Boolean, identityChanged: Boolean = false) {
|
|
1062
|
+
if (headerSettledAtTop == settled && !identityChanged) return
|
|
1063
|
+
headerSettledAtTop = settled
|
|
1064
|
+
log("header-settled-state page=$selectedPage generation=$attachmentGeneration settled=$settled")
|
|
1065
|
+
onHeaderSettledStateChanged?.invoke()
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private fun cancelHeaderSettledCheck() {
|
|
1069
|
+
headerSettledCheck?.let(::removeCallbacks)
|
|
1070
|
+
headerSettledCheck = null
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
private fun updateHeaderSettledState(forceBusy: Boolean = false) {
|
|
1074
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
1075
|
+
val ownerKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
1076
|
+
val valid = isAttachedToWindow && width > 0 && height > 0 && headerHeightPx > 0 &&
|
|
1077
|
+
headerView?.isAttachedToWindow == true && owner != null && owner.isAttachedToWindow &&
|
|
1078
|
+
owner.width > 0 && owner.height > 0 && ownerKey == currentPageKey() &&
|
|
1079
|
+
selectedPage in 0 until adapter.itemCount && isDescendant(owner, adapter.pageAt(selectedPage))
|
|
1080
|
+
val identityChanged = headerSettledOwner?.get() !== owner || headerSettledKey != currentPageKey() ||
|
|
1081
|
+
headerSettledGeneration != attachmentGeneration
|
|
1082
|
+
if (identityChanged) {
|
|
1083
|
+
cancelHeaderSettledCheck()
|
|
1084
|
+
headerCommandOwner = null
|
|
1085
|
+
headerSettledOwner = owner?.let(::WeakReference)
|
|
1086
|
+
headerSettledKey = currentPageKey()
|
|
1087
|
+
headerSettledGeneration = attachmentGeneration
|
|
1088
|
+
headerSettledGeometry = null
|
|
1089
|
+
headerSettledWasBusy = true
|
|
1090
|
+
publishHeaderSettledState(false, true)
|
|
1091
|
+
}
|
|
1092
|
+
if (!valid || owner == null || headerOffsetPx != 0) {
|
|
1093
|
+
cancelHeaderSettledCheck()
|
|
1094
|
+
headerSettledGeometry = null
|
|
1095
|
+
headerSettledWasBusy = true
|
|
1096
|
+
publishHeaderSettledState(false)
|
|
1097
|
+
return
|
|
1098
|
+
}
|
|
1099
|
+
val windowPoint = IntArray(2)
|
|
1100
|
+
owner.getLocationInWindow(windowPoint)
|
|
1101
|
+
val recycler = owner as? RecyclerView
|
|
1102
|
+
val manager = recycler?.layoutManager as? LinearLayoutManager
|
|
1103
|
+
val first = manager?.findFirstVisibleItemPosition() ?: -1
|
|
1104
|
+
val firstTop = manager?.let { layoutManager ->
|
|
1105
|
+
layoutManager.findViewByPosition(first)?.let(layoutManager::getDecoratedTop)
|
|
1106
|
+
} ?: 0
|
|
1107
|
+
val content = (owner as? ViewGroup)?.getChildAt(0)
|
|
1108
|
+
val geometry = listOf(headerOffsetPx, headerHeightPx, stickyHeaderHeightPx, width, height,
|
|
1109
|
+
owner.width, owner.height, owner.paddingTop, owner.paddingBottom, owner.scrollY,
|
|
1110
|
+
windowPoint[0], windowPoint[1], first, firstTop, recycler?.adapter?.itemCount ?: 0,
|
|
1111
|
+
content?.height ?: 0, headerView?.height ?: 0).joinToString(":")
|
|
1112
|
+
val busy = forceBusy || pager.isFakeDragging || pager.scrollState != ViewPager2.SCROLL_STATE_IDLE ||
|
|
1113
|
+
transitionHeaderOffsets != null || syntheticPageRestore || pagerTopRequest != null ||
|
|
1114
|
+
headerCommandOwner?.get() === owner ||
|
|
1115
|
+
nestedScrollingParentHelper.nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 ||
|
|
1116
|
+
(recycler != null && (recycler.scrollState != RecyclerView.SCROLL_STATE_IDLE ||
|
|
1117
|
+
recycler.isComputingLayout || recycler.layoutManager?.isSmoothScrolling == true)) ||
|
|
1118
|
+
(owner is NativeScrollerViewport && owner.isReadingMotionActive())
|
|
1119
|
+
val changed = geometry != headerSettledGeometry
|
|
1120
|
+
val now = SystemClock.uptimeMillis()
|
|
1121
|
+
if (changed || busy || headerSettledWasBusy) {
|
|
1122
|
+
headerSettledSince = now
|
|
1123
|
+
headerSettledGeometry = geometry
|
|
1124
|
+
cancelHeaderSettledCheck()
|
|
1125
|
+
publishHeaderSettledState(false)
|
|
1126
|
+
}
|
|
1127
|
+
headerSettledWasBusy = busy
|
|
1128
|
+
if (!busy && now - headerSettledSince >= 150L) {
|
|
1129
|
+
cancelHeaderSettledCheck()
|
|
1130
|
+
publishHeaderSettledState(true)
|
|
1131
|
+
return
|
|
1132
|
+
}
|
|
1133
|
+
if (headerSettledCheck == null) {
|
|
1134
|
+
val weakOwner = WeakReference(owner)
|
|
1135
|
+
val key = currentPageKey()
|
|
1136
|
+
val generation = attachmentGeneration
|
|
1137
|
+
val check = Runnable {
|
|
1138
|
+
headerSettledCheck = null
|
|
1139
|
+
if (generation == attachmentGeneration && key == currentPageKey() && weakOwner.get() ===
|
|
1140
|
+
(observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) updateHeaderSettledState()
|
|
1141
|
+
}
|
|
1142
|
+
headerSettledCheck = check
|
|
1143
|
+
postDelayed(check, 150L)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1033
1147
|
private fun setHeaderOffset(offset: Int) {
|
|
1034
1148
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
1035
1149
|
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
@@ -1037,6 +1151,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1037
1151
|
val next = offset.coerceIn(0, maximum)
|
|
1038
1152
|
if (headerOffsetPx == next) return
|
|
1039
1153
|
headerOffsetPx = next
|
|
1154
|
+
updateHeaderSettledState(forceBusy = true)
|
|
1040
1155
|
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
1041
1156
|
pageHeaderOffsets[currentPageKey()] = next
|
|
1042
1157
|
}
|
|
@@ -1195,9 +1310,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1195
1310
|
val key = currentPageKey()
|
|
1196
1311
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
1197
1312
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
1313
|
+
if (observedRecyclerView === recyclerView) updateHeaderSettledState(forceBusy = newState != RecyclerView.SCROLL_STATE_IDLE)
|
|
1198
1314
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) finishPagerTopIfSettled()
|
|
1199
1315
|
}
|
|
1200
1316
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
1317
|
+
if (observedRecyclerView === recyclerView && (dx != 0 || dy != 0)) updateHeaderSettledState(forceBusy = true)
|
|
1201
1318
|
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
1202
1319
|
key != currentPageKey() || !isAttachedToWindow ||
|
|
1203
1320
|
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) {
|
|
@@ -1327,6 +1444,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1327
1444
|
}
|
|
1328
1445
|
|
|
1329
1446
|
private fun detachPrimaryScrollObserver(recordOffset: Boolean = true) {
|
|
1447
|
+
cancelHeaderSettledCheck()
|
|
1448
|
+
headerCommandOwner = null
|
|
1449
|
+
headerSettledGeometry = null
|
|
1450
|
+
headerSettledWasBusy = true
|
|
1451
|
+
publishHeaderSettledState(false)
|
|
1330
1452
|
detachOwnedScrollerObserver(recordOffset)
|
|
1331
1453
|
detachRecyclerObserver(recordOffset)
|
|
1332
1454
|
detachNativeScrollerObserver(recordOffset)
|
|
@@ -1599,6 +1721,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1599
1721
|
original.appliedTopInset = topInset
|
|
1600
1722
|
|
|
1601
1723
|
val key = pageKey(pageIndex)
|
|
1724
|
+
val geometry = Triple(scroller.getChildAt(0)?.height ?: 0, scroller.height,
|
|
1725
|
+
scroller.paddingTop + scroller.paddingBottom)
|
|
1726
|
+
deferredScrollerGeometry[scroller]?.let { deferred ->
|
|
1727
|
+
if (geometry != deferred) {
|
|
1728
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1729
|
+
restoredScrollerKeys.remove(scroller)
|
|
1730
|
+
if (nativeSmoothHeaderScrollEnabled && headerOffsetPx == headerHeightPx) {
|
|
1731
|
+
displayBodyOverrides.remove(key)
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1602
1735
|
if (restoredScrollerKeys[scroller] != key) {
|
|
1603
1736
|
restoredScrollerKeys[scroller] = key
|
|
1604
1737
|
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
@@ -1626,6 +1759,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1626
1759
|
return@post
|
|
1627
1760
|
}
|
|
1628
1761
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1762
|
+
val unreachableReadingIntent = nativeSmoothHeaderScrollEnabled &&
|
|
1763
|
+
headerOffsetPx == headerHeightPx && displayBodyOverrides.containsKey(key) &&
|
|
1764
|
+
(pageOffsets[key] ?: 0) > scroller.scrollY && !scroller.canScrollVertically(1)
|
|
1765
|
+
if (nativeSmoothHeaderScrollEnabled && (scroller.scrollY != saved || unreachableReadingIntent)) {
|
|
1766
|
+
deferredScrollerGeometry[scroller] = Triple(scroller.getChildAt(0)?.height ?: 0,
|
|
1767
|
+
scroller.height, scroller.paddingTop + scroller.paddingBottom)
|
|
1768
|
+
offsetDiag("scrollview-restore-pending-saved=$saved-current=${scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
|
|
1769
|
+
return@post
|
|
1770
|
+
}
|
|
1771
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1629
1772
|
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1630
1773
|
else pendingPageRestores.remove(key)
|
|
1631
1774
|
offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
@@ -1695,6 +1838,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1695
1838
|
|
|
1696
1839
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
1697
1840
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
1841
|
+
deferredScrollerGeometry.remove(scroller)
|
|
1698
1842
|
updateScrollerPaddingOwnership(scroller, original)
|
|
1699
1843
|
restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
|
|
1700
1844
|
if (scroller is ReactScrollView) {
|
|
@@ -2070,10 +2214,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2070
2214
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
2071
2215
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
2072
2216
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
2217
|
+
updateHeaderSettledState(forceBusy = true)
|
|
2073
2218
|
if (type == ViewCompat.TYPE_TOUCH) {
|
|
2074
2219
|
val request = pageRestoreRequests[currentPageKey()]
|
|
2075
2220
|
offsetDiag("touch-cancel-before", target, request = request)
|
|
2076
2221
|
observedRecyclerView?.let { deferredRecyclerGeometry.remove(it) }
|
|
2222
|
+
observedNativeScroller?.let { deferredScrollerGeometry.remove(it) }
|
|
2077
2223
|
pageRestoreRequests.remove(currentPageKey())
|
|
2078
2224
|
pendingPageRestores.remove(currentPageKey())
|
|
2079
2225
|
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
@@ -2084,6 +2230,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2084
2230
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
2085
2231
|
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
2086
2232
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
2233
|
+
updateHeaderSettledState()
|
|
2087
2234
|
onHeaderOffsetChanged?.invoke()
|
|
2088
2235
|
}
|
|
2089
2236
|
|
|
@@ -2191,6 +2338,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2191
2338
|
}
|
|
2192
2339
|
|
|
2193
2340
|
override fun onDetachedFromWindow() {
|
|
2341
|
+
cancelHeaderSettledCheck()
|
|
2342
|
+
headerCommandOwner = null
|
|
2343
|
+
publishHeaderSettledState(false)
|
|
2194
2344
|
pendingRefreshPull = null
|
|
2195
2345
|
releaseRefreshForeground()
|
|
2196
2346
|
attachmentGeneration += 1
|
|
@@ -2211,6 +2361,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2211
2361
|
alignedBodyKeys.clear()
|
|
2212
2362
|
transitionAlignedViews.clear()
|
|
2213
2363
|
deferredRecyclerGeometry.clear()
|
|
2364
|
+
deferredScrollerGeometry.clear()
|
|
2214
2365
|
displayBodyOverrides.clear()
|
|
2215
2366
|
displayRecyclerAnchors.clear()
|
|
2216
2367
|
ownedScrollerRestoreRequests.clear()
|
|
@@ -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
|
@@ -38,6 +38,12 @@ Late callbacks cannot revive cancelled restoration or erase a newer request.
|
|
|
38
38
|
Observers and commands must match page key, view identity and generation.
|
|
39
39
|
Removed or recycled owners cannot apply callbacks to another page; recycling
|
|
40
40
|
resets owned memory.
|
|
41
|
+
The Web backend shares C and stores per-page B separately from absolute DOM
|
|
42
|
+
scrollTop. Selection aligns to C while expanded and C + B while fully collapsed;
|
|
43
|
+
programmatic alignment and geometry-only scrollend events MUST NOT erase the
|
|
44
|
+
suspended reading position. Real vertical input or a valid List command takes
|
|
45
|
+
over the current display. PageTop/StickyTop clear only the current B.
|
|
46
|
+
|
|
41
47
|
Legacy mode and pagers without a collapsible header retain their existing
|
|
42
48
|
behavior. Vertical scrolling, refresh, snapping and repeated-current-tab behavior
|
|
43
49
|
are unchanged. This state belongs to native UI instances, with no new React prop
|
|
@@ -717,3 +723,12 @@ cases additionally verified recorded refresh, header, restoration and gesture
|
|
|
717
723
|
paths; they do not establish every possible sequence or runtime acceptance of a
|
|
718
724
|
newly installed module release. Physical-device feel and semantic accessibility
|
|
719
725
|
activation remain unverified.
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
### Native Header stability snapshot
|
|
729
|
+
|
|
730
|
+
`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.
|
|
731
|
+
|
|
732
|
+
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.
|
|
733
|
+
|
|
734
|
+
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]) {
|
|
@@ -44,6 +44,8 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
44
44
|
pageOffsets = new Map();
|
|
45
45
|
pageHosts = new Map();
|
|
46
46
|
sharedHeaderOffset = 0;
|
|
47
|
+
selectionRestoreToken = 0;
|
|
48
|
+
bodyAlignment = null;
|
|
47
49
|
scrollEnabled = this.props.scrollEnabled ?? true;
|
|
48
50
|
lastMountSignature = null;
|
|
49
51
|
configureFrame = null;
|
|
@@ -71,6 +73,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
71
73
|
const pageCount = this.pages().length;
|
|
72
74
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
73
75
|
if (selectedPage !== this.state.selectedPage) {
|
|
76
|
+
this.selectionRestoreToken += 1;
|
|
74
77
|
this.cancelPendingTopFrame();
|
|
75
78
|
this.finishPageTransition();
|
|
76
79
|
this.setState({
|
|
@@ -83,6 +86,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
83
86
|
this.notifyMountedPagesChanged();
|
|
84
87
|
}
|
|
85
88
|
componentWillUnmount() {
|
|
89
|
+
this.selectionRestoreToken += 1;
|
|
86
90
|
this.cancelPendingTopFrame();
|
|
87
91
|
this.savePageOffset(this.state.selectedPage);
|
|
88
92
|
if (this.configureFrame !== null) {
|
|
@@ -116,16 +120,32 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
116
120
|
if (this.pageScrollElement(this.state.selectedPage) === pending.element && this.pages().map(pageKey)[this.state.selectedPage] === pending.key) this.savePageOffset(this.state.selectedPage);
|
|
117
121
|
};
|
|
118
122
|
onQueuedTopWheel = event => {
|
|
119
|
-
if (event.deltaY !== 0) this.
|
|
123
|
+
if (event.deltaY !== 0) this.takeOverVerticalScroll();
|
|
124
|
+
};
|
|
125
|
+
onScrollbarPointerDown = event => {
|
|
126
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
127
|
+
if (!element || event.target !== element || event.button !== 0) return;
|
|
128
|
+
const rect = element.getBoundingClientRect();
|
|
129
|
+
const computed = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
130
|
+
const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
|
|
131
|
+
const borderRight = Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
|
|
132
|
+
const clientStart = rect.left + element.clientLeft;
|
|
133
|
+
const clientEnd = clientStart + element.clientWidth;
|
|
134
|
+
const leftGutter = clientStart - (rect.left + borderLeft);
|
|
135
|
+
const rightGutter = rect.right - borderRight - clientEnd;
|
|
136
|
+
const inVerticalTrack = event.clientY >= rect.top + element.clientTop && event.clientY < rect.top + element.clientTop + element.clientHeight;
|
|
137
|
+
const inGutter = leftGutter > 0 && event.clientX >= rect.left + borderLeft && event.clientX < clientStart || rightGutter > 0 && event.clientX >= clientEnd && event.clientX < rect.right - borderRight;
|
|
138
|
+
if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
|
|
120
139
|
};
|
|
121
140
|
onQueuedTopKey = event => {
|
|
122
|
-
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.
|
|
141
|
+
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.takeOverVerticalScroll();
|
|
123
142
|
};
|
|
124
143
|
scrollCurrentPageToTop(offset, animated) {
|
|
125
144
|
if (this.horizontalTouch || this.transitionTimer !== null) return;
|
|
126
145
|
const element = this.pageScrollElement(this.state.selectedPage);
|
|
127
146
|
const key = this.pages().map(pageKey)[this.state.selectedPage];
|
|
128
147
|
if (!element?.isConnected || !key) return;
|
|
148
|
+
this.selectionRestoreToken += 1;
|
|
129
149
|
this.cancelPendingTopFrame();
|
|
130
150
|
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
131
151
|
if (EventConstructor) element.dispatchEvent(new EventConstructor("ok-collapsible-pager-cancel-scroll-command"));
|
|
@@ -138,8 +158,12 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
138
158
|
if (this.pendingTopFrame?.frame !== frame) return;
|
|
139
159
|
this.pendingTopFrame = null;
|
|
140
160
|
if (!element.isConnected || this.horizontalTouch || this.transitionTimer !== null || this.pageScrollElement(this.state.selectedPage) !== element || this.pages().map(pageKey)[this.state.selectedPage] !== key) return;
|
|
141
|
-
this.pageOffsets.set(key,
|
|
161
|
+
this.pageOffsets.set(key, 0);
|
|
142
162
|
this.sharedHeaderOffset = offset;
|
|
163
|
+
this.bodyAlignment = {
|
|
164
|
+
element,
|
|
165
|
+
key
|
|
166
|
+
};
|
|
143
167
|
element.scrollTo({
|
|
144
168
|
top: offset,
|
|
145
169
|
behavior: animated ? "smooth" : "instant"
|
|
@@ -218,8 +242,9 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
218
242
|
const configured = this.configuredScrollElement;
|
|
219
243
|
if (!configured) return;
|
|
220
244
|
this.cancelPendingTopFrame();
|
|
221
|
-
configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.
|
|
245
|
+
configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
|
|
222
246
|
configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
|
|
247
|
+
configured.element.removeEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
223
248
|
configured.element.removeEventListener("keydown", this.onQueuedTopKey);
|
|
224
249
|
configured.element.removeEventListener("scrollend", this.onVerticalScrollSettled);
|
|
225
250
|
const style = timelineStyle(configured.element);
|
|
@@ -234,6 +259,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
234
259
|
configured.element.removeAttribute("data-collapsible-pager-active");
|
|
235
260
|
this.notifyInsetsChanged(configured.element);
|
|
236
261
|
this.configuredScrollElement = null;
|
|
262
|
+
if (this.bodyAlignment?.element === configured.element) this.bodyAlignment = null;
|
|
237
263
|
}
|
|
238
264
|
configureActiveScrollElement = () => {
|
|
239
265
|
this.configureFrame = null;
|
|
@@ -269,8 +295,9 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
269
295
|
pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET)
|
|
270
296
|
};
|
|
271
297
|
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
272
|
-
element.addEventListener("ok-native-list-scroll-command-accepted", this.
|
|
298
|
+
element.addEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
|
|
273
299
|
element.addEventListener("wheel", this.onQueuedTopWheel);
|
|
300
|
+
element.addEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
274
301
|
element.addEventListener("keydown", this.onQueuedTopKey);
|
|
275
302
|
}
|
|
276
303
|
const style = timelineStyle(element);
|
|
@@ -299,8 +326,18 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
299
326
|
this.configureActiveScrollElement();
|
|
300
327
|
}
|
|
301
328
|
}
|
|
302
|
-
|
|
303
|
-
|
|
329
|
+
takeOverVerticalScroll = () => {
|
|
330
|
+
this.selectionRestoreToken += 1;
|
|
331
|
+
this.bodyAlignment = null;
|
|
332
|
+
this.cancelPendingTopFrame();
|
|
333
|
+
this.savePageOffset(this.state.selectedPage);
|
|
334
|
+
};
|
|
335
|
+
onListScrollCommandAccepted = event => {
|
|
336
|
+
if (event.target !== this.pageScrollElement(this.state.selectedPage)) return;
|
|
337
|
+
this.takeOverVerticalScroll();
|
|
338
|
+
};
|
|
339
|
+
onVerticalScrollSettled = event => {
|
|
340
|
+
if (this.pendingTopFrame || event && event.target !== this.pageScrollElement(this.state.selectedPage)) return;
|
|
304
341
|
this.savePageOffset(this.state.selectedPage);
|
|
305
342
|
this.emitDiagnostics("scroll-settled");
|
|
306
343
|
};
|
|
@@ -308,15 +345,24 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
308
345
|
const key = this.pages().map(pageKey)[index];
|
|
309
346
|
const scrollElement = this.pageScrollElement(index);
|
|
310
347
|
if (key && scrollElement) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
348
|
+
if (this.bodyAlignment?.key === key && this.bodyAlignment.element === scrollElement) return;
|
|
349
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
350
|
+
this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
|
|
351
|
+
this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), D);
|
|
313
352
|
}
|
|
314
353
|
}
|
|
315
354
|
restorePageOffset(index) {
|
|
316
355
|
const key = this.pages().map(pageKey)[index];
|
|
317
356
|
const scrollElement = this.pageScrollElement(index);
|
|
318
357
|
if (!key || !scrollElement) return;
|
|
319
|
-
|
|
358
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
359
|
+
const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
|
|
360
|
+
const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
|
|
361
|
+
this.bodyAlignment = {
|
|
362
|
+
element: scrollElement,
|
|
363
|
+
key
|
|
364
|
+
};
|
|
365
|
+
scrollElement.scrollTop = expectedQ;
|
|
320
366
|
}
|
|
321
367
|
selectPage(selectedPage, animated) {
|
|
322
368
|
const pages = this.pages();
|
|
@@ -324,6 +370,8 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
324
370
|
if (position === null || position < 0 || position >= pages.length || position === this.state.selectedPage) {
|
|
325
371
|
return;
|
|
326
372
|
}
|
|
373
|
+
const selectionToken = ++this.selectionRestoreToken;
|
|
374
|
+
const expectedKey = this.pages().map(pageKey)[position];
|
|
327
375
|
this.cancelPendingTopFrame();
|
|
328
376
|
this.savePageOffset(this.state.selectedPage);
|
|
329
377
|
if (animated) this.beginPageTransition();else this.finishPageTransition();
|
|
@@ -333,6 +381,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
333
381
|
}, () => {
|
|
334
382
|
this.scheduleScrollConfiguration();
|
|
335
383
|
const restore = () => {
|
|
384
|
+
if (this.selectionRestoreToken !== selectionToken || this.state.selectedPage !== position || this.pages().map(pageKey)[position] !== expectedKey || !this.pageScrollElement(position)?.isConnected) return;
|
|
336
385
|
this.configureActiveScrollElement();
|
|
337
386
|
this.restorePageOffset(position);
|
|
338
387
|
};
|
|
@@ -392,7 +441,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
392
441
|
if (!touch) return;
|
|
393
442
|
const deltaX = touch.pageX - this.touchStartX;
|
|
394
443
|
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
395
|
-
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.
|
|
444
|
+
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.takeOverVerticalScroll();
|
|
396
445
|
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
397
446
|
this.cancelPendingTopFrame();
|
|
398
447
|
this.horizontalTouch = true;
|
|
@@ -59,6 +59,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
59
59
|
private pageOffsets;
|
|
60
60
|
private pageHosts;
|
|
61
61
|
private sharedHeaderOffset;
|
|
62
|
+
private selectionRestoreToken;
|
|
63
|
+
private bodyAlignment;
|
|
62
64
|
private scrollEnabled;
|
|
63
65
|
private lastMountSignature;
|
|
64
66
|
private configureFrame;
|
|
@@ -80,6 +82,7 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
80
82
|
}>) => void;
|
|
81
83
|
private cancelPendingTopFrame;
|
|
82
84
|
private onQueuedTopWheel;
|
|
85
|
+
private onScrollbarPointerDown;
|
|
83
86
|
private onQueuedTopKey;
|
|
84
87
|
private scrollCurrentPageToTop;
|
|
85
88
|
setScrollEnabled: (scrollEnabled: boolean) => void;
|
|
@@ -97,6 +100,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
|
|
|
97
100
|
private restoreConfiguredScrollElement;
|
|
98
101
|
private configureActiveScrollElement;
|
|
99
102
|
private scheduleScrollConfiguration;
|
|
103
|
+
private takeOverVerticalScroll;
|
|
104
|
+
private onListScrollCommandAccepted;
|
|
100
105
|
private onVerticalScrollSettled;
|
|
101
106
|
private savePageOffset;
|
|
102
107
|
private restorePageOffset;
|
|
@@ -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.176",
|
|
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.176",
|
|
76
76
|
"react": "*",
|
|
77
77
|
"react-native": "*"
|
|
78
78
|
},
|
|
@@ -154,6 +154,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
154
154
|
private pageOffsets = new Map<string, number>();
|
|
155
155
|
private pageHosts = new Map<number, HTMLElement>();
|
|
156
156
|
private sharedHeaderOffset = 0;
|
|
157
|
+
private selectionRestoreToken = 0;
|
|
158
|
+
private bodyAlignment: Readonly<{
|
|
159
|
+
element: HTMLElement;
|
|
160
|
+
key: string;
|
|
161
|
+
}> | null = null;
|
|
157
162
|
private scrollEnabled = this.props.scrollEnabled ?? true;
|
|
158
163
|
private lastMountSignature: string | null = null;
|
|
159
164
|
private configureFrame: number | null = null;
|
|
@@ -188,6 +193,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
188
193
|
const pageCount = this.pages().length;
|
|
189
194
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
190
195
|
if (selectedPage !== this.state.selectedPage) {
|
|
196
|
+
this.selectionRestoreToken += 1;
|
|
191
197
|
this.cancelPendingTopFrame();
|
|
192
198
|
this.finishPageTransition();
|
|
193
199
|
this.setState({
|
|
@@ -201,6 +207,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
componentWillUnmount() {
|
|
210
|
+
this.selectionRestoreToken += 1;
|
|
204
211
|
this.cancelPendingTopFrame();
|
|
205
212
|
this.savePageOffset(this.state.selectedPage);
|
|
206
213
|
if (this.configureFrame !== null) {
|
|
@@ -247,7 +254,33 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
247
254
|
};
|
|
248
255
|
|
|
249
256
|
private onQueuedTopWheel = (event: WheelEvent) => {
|
|
250
|
-
if (event.deltaY !== 0) this.
|
|
257
|
+
if (event.deltaY !== 0) this.takeOverVerticalScroll();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
private onScrollbarPointerDown = (event: PointerEvent) => {
|
|
261
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
262
|
+
if (!element || event.target !== element || event.button !== 0) return;
|
|
263
|
+
const rect = element.getBoundingClientRect();
|
|
264
|
+
const computed =
|
|
265
|
+
element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
266
|
+
const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
|
|
267
|
+
const borderRight =
|
|
268
|
+
Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
|
|
269
|
+
const clientStart = rect.left + element.clientLeft;
|
|
270
|
+
const clientEnd = clientStart + element.clientWidth;
|
|
271
|
+
const leftGutter = clientStart - (rect.left + borderLeft);
|
|
272
|
+
const rightGutter = rect.right - borderRight - clientEnd;
|
|
273
|
+
const inVerticalTrack =
|
|
274
|
+
event.clientY >= rect.top + element.clientTop &&
|
|
275
|
+
event.clientY < rect.top + element.clientTop + element.clientHeight;
|
|
276
|
+
const inGutter =
|
|
277
|
+
(leftGutter > 0 &&
|
|
278
|
+
event.clientX >= rect.left + borderLeft &&
|
|
279
|
+
event.clientX < clientStart) ||
|
|
280
|
+
(rightGutter > 0 &&
|
|
281
|
+
event.clientX >= clientEnd &&
|
|
282
|
+
event.clientX < rect.right - borderRight);
|
|
283
|
+
if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
|
|
251
284
|
};
|
|
252
285
|
|
|
253
286
|
private onQueuedTopKey = (event: KeyboardEvent) => {
|
|
@@ -262,7 +295,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
262
295
|
" ",
|
|
263
296
|
].includes(event.key)
|
|
264
297
|
)
|
|
265
|
-
this.
|
|
298
|
+
this.takeOverVerticalScroll();
|
|
266
299
|
};
|
|
267
300
|
|
|
268
301
|
private scrollCurrentPageToTop(offset: number, animated: boolean) {
|
|
@@ -270,6 +303,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
270
303
|
const element = this.pageScrollElement(this.state.selectedPage);
|
|
271
304
|
const key = this.pages().map(pageKey)[this.state.selectedPage];
|
|
272
305
|
if (!element?.isConnected || !key) return;
|
|
306
|
+
this.selectionRestoreToken += 1;
|
|
273
307
|
this.cancelPendingTopFrame();
|
|
274
308
|
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
275
309
|
if (EventConstructor)
|
|
@@ -292,8 +326,9 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
292
326
|
this.pages().map(pageKey)[this.state.selectedPage] !== key
|
|
293
327
|
)
|
|
294
328
|
return;
|
|
295
|
-
this.pageOffsets.set(key,
|
|
329
|
+
this.pageOffsets.set(key, 0);
|
|
296
330
|
this.sharedHeaderOffset = offset;
|
|
331
|
+
this.bodyAlignment = { element, key };
|
|
297
332
|
element.scrollTo({
|
|
298
333
|
top: offset,
|
|
299
334
|
behavior: animated ? "smooth" : "instant",
|
|
@@ -392,9 +427,13 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
392
427
|
this.cancelPendingTopFrame();
|
|
393
428
|
configured.element.removeEventListener(
|
|
394
429
|
"ok-native-list-scroll-command-accepted",
|
|
395
|
-
this.
|
|
430
|
+
this.onListScrollCommandAccepted
|
|
396
431
|
);
|
|
397
432
|
configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
|
|
433
|
+
configured.element.removeEventListener(
|
|
434
|
+
"pointerdown",
|
|
435
|
+
this.onScrollbarPointerDown
|
|
436
|
+
);
|
|
398
437
|
configured.element.removeEventListener("keydown", this.onQueuedTopKey);
|
|
399
438
|
configured.element.removeEventListener(
|
|
400
439
|
"scrollend",
|
|
@@ -412,6 +451,8 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
412
451
|
configured.element.removeAttribute("data-collapsible-pager-active");
|
|
413
452
|
this.notifyInsetsChanged(configured.element);
|
|
414
453
|
this.configuredScrollElement = null;
|
|
454
|
+
if (this.bodyAlignment?.element === configured.element)
|
|
455
|
+
this.bodyAlignment = null;
|
|
415
456
|
}
|
|
416
457
|
|
|
417
458
|
private configureActiveScrollElement = () => {
|
|
@@ -458,9 +499,10 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
458
499
|
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
459
500
|
element.addEventListener(
|
|
460
501
|
"ok-native-list-scroll-command-accepted",
|
|
461
|
-
this.
|
|
502
|
+
this.onListScrollCommandAccepted
|
|
462
503
|
);
|
|
463
504
|
element.addEventListener("wheel", this.onQueuedTopWheel);
|
|
505
|
+
element.addEventListener("pointerdown", this.onScrollbarPointerDown);
|
|
464
506
|
element.addEventListener("keydown", this.onQueuedTopKey);
|
|
465
507
|
}
|
|
466
508
|
|
|
@@ -501,8 +543,26 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
501
543
|
}
|
|
502
544
|
}
|
|
503
545
|
|
|
504
|
-
private
|
|
505
|
-
|
|
546
|
+
private takeOverVerticalScroll = () => {
|
|
547
|
+
this.selectionRestoreToken += 1;
|
|
548
|
+
this.bodyAlignment = null;
|
|
549
|
+
this.cancelPendingTopFrame();
|
|
550
|
+
this.savePageOffset(this.state.selectedPage);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
private onListScrollCommandAccepted = (event: Event) => {
|
|
554
|
+
if (event.target !== this.pageScrollElement(this.state.selectedPage))
|
|
555
|
+
return;
|
|
556
|
+
this.takeOverVerticalScroll();
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
private onVerticalScrollSettled = (event?: Event) => {
|
|
560
|
+
if (
|
|
561
|
+
this.pendingTopFrame ||
|
|
562
|
+
(event &&
|
|
563
|
+
event.target !== this.pageScrollElement(this.state.selectedPage))
|
|
564
|
+
)
|
|
565
|
+
return;
|
|
506
566
|
this.savePageOffset(this.state.selectedPage);
|
|
507
567
|
this.emitDiagnostics("scroll-settled");
|
|
508
568
|
};
|
|
@@ -511,10 +571,16 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
511
571
|
const key = this.pages().map(pageKey)[index];
|
|
512
572
|
const scrollElement = this.pageScrollElement(index);
|
|
513
573
|
if (key && scrollElement) {
|
|
514
|
-
|
|
574
|
+
if (
|
|
575
|
+
this.bodyAlignment?.key === key &&
|
|
576
|
+
this.bodyAlignment.element === scrollElement
|
|
577
|
+
)
|
|
578
|
+
return;
|
|
579
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
580
|
+
this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
|
|
515
581
|
this.sharedHeaderOffset = Math.min(
|
|
516
582
|
Math.max(scrollElement.scrollTop, 0),
|
|
517
|
-
|
|
583
|
+
D
|
|
518
584
|
);
|
|
519
585
|
}
|
|
520
586
|
}
|
|
@@ -523,10 +589,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
523
589
|
const key = this.pages().map(pageKey)[index];
|
|
524
590
|
const scrollElement = this.pageScrollElement(index);
|
|
525
591
|
if (!key || !scrollElement) return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
592
|
+
const D = Math.max(0, this.props.headerHeight);
|
|
593
|
+
const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
|
|
594
|
+
const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
|
|
595
|
+
this.bodyAlignment = { element: scrollElement, key };
|
|
596
|
+
scrollElement.scrollTop = expectedQ;
|
|
530
597
|
}
|
|
531
598
|
|
|
532
599
|
private selectPage(selectedPage: number, animated: boolean) {
|
|
@@ -540,6 +607,8 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
540
607
|
) {
|
|
541
608
|
return;
|
|
542
609
|
}
|
|
610
|
+
const selectionToken = ++this.selectionRestoreToken;
|
|
611
|
+
const expectedKey = this.pages().map(pageKey)[position];
|
|
543
612
|
this.cancelPendingTopFrame();
|
|
544
613
|
this.savePageOffset(this.state.selectedPage);
|
|
545
614
|
|
|
@@ -550,6 +619,13 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
550
619
|
() => {
|
|
551
620
|
this.scheduleScrollConfiguration();
|
|
552
621
|
const restore = () => {
|
|
622
|
+
if (
|
|
623
|
+
this.selectionRestoreToken !== selectionToken ||
|
|
624
|
+
this.state.selectedPage !== position ||
|
|
625
|
+
this.pages().map(pageKey)[position] !== expectedKey ||
|
|
626
|
+
!this.pageScrollElement(position)?.isConnected
|
|
627
|
+
)
|
|
628
|
+
return;
|
|
553
629
|
this.configureActiveScrollElement();
|
|
554
630
|
this.restorePageOffset(position);
|
|
555
631
|
};
|
|
@@ -629,7 +705,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
629
705
|
const deltaX = touch.pageX - this.touchStartX;
|
|
630
706
|
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
631
707
|
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX))
|
|
632
|
-
this.
|
|
708
|
+
this.takeOverVerticalScroll();
|
|
633
709
|
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
634
710
|
this.cancelPendingTopFrame();
|
|
635
711
|
this.horizontalTouch = true;
|