@onekeyfe/react-native-pager-view 3.0.171 → 3.0.173
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 +19 -222
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +0 -8
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +0 -2
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +0 -11
- package/docs/SPEC.md +1 -38
- package/ios/RNCCollapsiblePagerViewComponentView.mm +25 -256
- package/ios/RNCNativeScrollerComponentView.mm +0 -34
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +0 -173
- package/package.json +2 -2
|
@@ -123,7 +123,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
123
123
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
124
124
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
125
125
|
private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
|
|
126
|
-
private val ownedScrollerRestoreRequests = HashMap<String, Pair<WeakReference<NativeScrollerViewport>, Int?>>()
|
|
127
126
|
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
128
127
|
private var refreshForegroundWasVisible = false
|
|
129
128
|
private val refreshForegroundMatrix = Matrix()
|
|
@@ -131,23 +130,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
131
130
|
private val refreshForegroundAncestors = ArrayList<View>()
|
|
132
131
|
private var refreshForegroundAncestorAlpha = 1f
|
|
133
132
|
private val pageOffsets = HashMap<String, Int>()
|
|
134
|
-
private val pageHeaderOffsets = HashMap<String, Int>()
|
|
135
|
-
private var transitionHeaderOffsets: Map<String, Int>? = null
|
|
136
|
-
private var transitionPageOffsets: Map<String, Int>? = null
|
|
137
|
-
private var transitionSourcePage = 0
|
|
138
|
-
private var transitionTargetPage: Int? = null
|
|
139
|
-
private var syntheticPageRestore = false
|
|
140
|
-
private var resumeTransitionSource: Int? = null
|
|
141
|
-
private val pendingPageRestores = HashSet<String>()
|
|
142
133
|
private var observedRecyclerView: RecyclerView? = null
|
|
143
|
-
private var observedRecyclerKey: String? = null
|
|
144
134
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
145
135
|
private var observedOwnedScroller: NativeScrollerViewport? = null
|
|
146
136
|
private var observedOwnedScrollerKey: String? = null
|
|
147
137
|
private var observedOwnedScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
148
138
|
private var observedOwnedScrollerTreeObserver: ViewTreeObserver? = null
|
|
149
139
|
private var observedNativeScroller: ScrollView? = null
|
|
150
|
-
private var observedNativeScrollerKey: String? = null
|
|
151
140
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
152
141
|
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
153
142
|
private var observedPageRemoved = false
|
|
@@ -241,19 +230,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
241
230
|
OneKeyLog.debug("CollapsiblePager", "host=$hostIdentity $message")
|
|
242
231
|
}
|
|
243
232
|
|
|
244
|
-
private var offsetDiagSequence = 0L
|
|
245
|
-
|
|
246
|
-
private fun offsetDiag(phase: String, view: View? = null, key: String = currentPageKey()) {
|
|
247
|
-
if ((context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) == 0) return
|
|
248
|
-
val safeKey = if (key in setOf("portfolio", "perps", "defi", "nft", "history")) key else "other"
|
|
249
|
-
val observed = observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView
|
|
250
|
-
OneKeyLog.debug("CollapsiblePager", "[HOME-OFFSET-DIAG] phase=$phase seq=${++offsetDiagSequence} " +
|
|
251
|
-
"wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} host=$hostIdentity " +
|
|
252
|
-
"generation=$attachmentGeneration page=$selectedPage key=$safeKey header=$headerOffsetPx headerHeight=$headerHeightPx " +
|
|
253
|
-
"view=${view?.let { System.identityHashCode(it) }} tag=${view?.id} observed=${observed?.let { System.identityHashCode(it) }} " +
|
|
254
|
-
"offset=${view?.let(::verticalScrollOffset)} cache=${pageOffsets[key]} boundsHeight=${view?.height}")
|
|
255
|
-
}
|
|
256
|
-
|
|
257
233
|
private fun outerPagerIndex(): Int {
|
|
258
234
|
var ancestor = parent as? View
|
|
259
235
|
while (ancestor != null) {
|
|
@@ -410,80 +386,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
410
386
|
|
|
411
387
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
412
388
|
|
|
413
|
-
internal fun beginPageTransition(target: Int? = null) {
|
|
414
|
-
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
415
|
-
if (transitionHeaderOffsets == null) {
|
|
416
|
-
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
417
|
-
detachPrimaryScrollObserver()
|
|
418
|
-
transitionSourcePage = selectedPage
|
|
419
|
-
transitionHeaderOffsets = pageHeaderOffsets.toMap()
|
|
420
|
-
transitionPageOffsets = pageOffsets.toMap()
|
|
421
|
-
}
|
|
422
|
-
if (target != null) transitionTargetPage = target
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
internal fun onPagerScrolled(position: Int, offset: Float) {
|
|
426
|
-
val headers = transitionHeaderOffsets ?: return
|
|
427
|
-
val target = transitionTargetPage
|
|
428
|
-
val first = if (target != null) transitionSourcePage else position
|
|
429
|
-
val second = target ?: (position + 1).coerceAtMost(adapter.itemCount - 1)
|
|
430
|
-
val fraction = if (target != null && target != first) {
|
|
431
|
-
((position + offset - first) / (target - first)).coerceIn(0f, 1f)
|
|
432
|
-
} else offset
|
|
433
|
-
val firstH = headers[pageKey(first)] ?: 0
|
|
434
|
-
val secondH = headers[pageKey(second)] ?: 0
|
|
435
|
-
syntheticPageRestore = true
|
|
436
|
-
setHeaderOffset((firstH + (secondH - firstH) * fraction).roundToInt())
|
|
437
|
-
syntheticPageRestore = false
|
|
438
|
-
updateNativeTabProgress(position, offset)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
internal fun onPagerScrollStateChanged(state: Int) {
|
|
442
|
-
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
443
|
-
beginPageTransition()
|
|
444
|
-
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
445
|
-
finishPageTransition(pager.currentItem)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private fun finishPageTransition(position: Int) {
|
|
450
|
-
val headers = transitionHeaderOffsets ?: return
|
|
451
|
-
detachPrimaryScrollObserver(recordOffset = false)
|
|
452
|
-
releaseRefreshForeground()
|
|
453
|
-
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
454
|
-
syntheticPageRestore = true
|
|
455
|
-
setHeaderOffset(headers[currentPageKey()] ?: 0)
|
|
456
|
-
transitionHeaderOffsets = null
|
|
457
|
-
transitionPageOffsets = null
|
|
458
|
-
transitionTargetPage = null
|
|
459
|
-
syntheticPageRestore = false
|
|
460
|
-
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
461
|
-
prepareAdjacentPages()
|
|
462
|
-
attachPrimaryScrollObserver()
|
|
463
|
-
}
|
|
464
|
-
|
|
465
389
|
fun selectPage(position: Int) {
|
|
466
|
-
offsetDiag("select-begin-target=$position", observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView)
|
|
467
390
|
pendingRefreshPull = null
|
|
468
391
|
if (adapter.itemCount == 0) return
|
|
469
|
-
if (nativeSmoothHeaderScrollEnabled && headerHeightPx > 0) {
|
|
470
|
-
if (transitionHeaderOffsets != null) return
|
|
471
|
-
beginPageTransition(position)
|
|
472
|
-
finishPageTransition(position)
|
|
473
|
-
return
|
|
474
|
-
}
|
|
475
392
|
detachPrimaryScrollObserver()
|
|
476
393
|
releaseRefreshForeground()
|
|
477
394
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
478
395
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
479
396
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
480
397
|
?: 0
|
|
481
|
-
|
|
482
|
-
if (savedOffset > 0) {
|
|
483
|
-
offsetDiag("select-force-collapse-before")
|
|
484
|
-
setHeaderOffset(headerHeightPx)
|
|
485
|
-
offsetDiag("select-force-collapse-after")
|
|
486
|
-
}
|
|
398
|
+
if (savedOffset > 0) setHeaderOffset(headerHeightPx)
|
|
487
399
|
postForCurrentAttachment {
|
|
488
400
|
prepareAdjacentPages()
|
|
489
401
|
attachPrimaryScrollObserver()
|
|
@@ -625,14 +537,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
625
537
|
|
|
626
538
|
private fun setHeaderOffset(offset: Int) {
|
|
627
539
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
628
|
-
val
|
|
629
|
-
val maximum = if (transitionHeaderOffsets != null || pending) headerHeightPx else maximumHeaderOffset()
|
|
630
|
-
val next = offset.coerceIn(0, maximum)
|
|
540
|
+
val next = offset.coerceIn(0, maximumHeaderOffset())
|
|
631
541
|
if (headerOffsetPx == next) return
|
|
632
542
|
headerOffsetPx = next
|
|
633
|
-
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
634
|
-
pageHeaderOffsets[currentPageKey()] = next
|
|
635
|
-
}
|
|
636
543
|
applyHeaderOffset()
|
|
637
544
|
}
|
|
638
545
|
|
|
@@ -749,17 +656,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
749
656
|
}
|
|
750
657
|
|
|
751
658
|
private fun attachPrimaryScrollObserver() {
|
|
752
|
-
if (transitionHeaderOffsets != null) return
|
|
753
659
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
754
660
|
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
755
661
|
if (observedPageRemoved || previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
756
|
-
offsetDiag("primary-replacement-before-removed=$observedPageRemoved", previous)
|
|
757
662
|
observedPageRemoved = false
|
|
758
|
-
detachPrimaryScrollObserver(
|
|
663
|
+
detachPrimaryScrollObserver()
|
|
759
664
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
760
|
-
|
|
761
|
-
if (!nativeSmoothHeaderScrollEnabled) pageOffsets[currentPageKey()] = 0
|
|
762
|
-
offsetDiag("primary-replacement-cache-after", previous)
|
|
665
|
+
pageOffsets[currentPageKey()] = 0
|
|
763
666
|
}
|
|
764
667
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
765
668
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
@@ -769,33 +672,24 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
769
672
|
}
|
|
770
673
|
|
|
771
674
|
private fun attachRecyclerObserver(recycler: RecyclerView) {
|
|
772
|
-
if (observedRecyclerView === recycler
|
|
675
|
+
if (observedRecyclerView === recycler) {
|
|
773
676
|
applyRecyclerInsets(recycler, selectedPage)
|
|
774
677
|
publishConsumedOffset(recycler)
|
|
775
678
|
return
|
|
776
679
|
}
|
|
777
680
|
|
|
778
681
|
detachPrimaryScrollObserver()
|
|
779
|
-
offsetDiag("recycler-attach-before", recycler)
|
|
780
682
|
observedRecyclerView = recycler
|
|
781
|
-
observedRecyclerKey = currentPageKey()
|
|
782
683
|
applyRecyclerInsets(recycler, selectedPage)
|
|
783
684
|
publishConsumedOffset(recycler)
|
|
784
|
-
val key = currentPageKey()
|
|
785
685
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
786
686
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
787
|
-
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
788
|
-
key != currentPageKey() || !isAttachedToWindow ||
|
|
789
|
-
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores) return
|
|
790
687
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
791
|
-
|
|
792
|
-
pageOffsets[key] = contentOffset
|
|
688
|
+
pageOffsets[currentPageKey()] = contentOffset
|
|
793
689
|
// Nested touch scrolling moves list content only after the header has
|
|
794
690
|
// collapsed. Programmatic list jumps bypass those parent callbacks.
|
|
795
691
|
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
796
|
-
offsetDiag("recycler-positive-collapse-before", recyclerView)
|
|
797
692
|
setHeaderOffset(headerHeightPx)
|
|
798
|
-
offsetDiag("recycler-positive-collapse-after", recyclerView)
|
|
799
693
|
}
|
|
800
694
|
}
|
|
801
695
|
}
|
|
@@ -811,11 +705,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
811
705
|
val recycler = observedRecyclerView
|
|
812
706
|
val listener = observedScrollListener
|
|
813
707
|
if (recycler != null) {
|
|
814
|
-
|
|
815
|
-
if (recordOffset && transitionPageOffsets == null) observedRecyclerKey?.let {
|
|
816
|
-
if (it !in pendingPageRestores) pageOffsets[it] = recycler.computeVerticalScrollOffset()
|
|
817
|
-
}
|
|
818
|
-
offsetDiag("recycler-detach-save-after", recycler)
|
|
708
|
+
if (recordOffset) pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
|
|
819
709
|
publishConsumedOffset(recycler, remove = true)
|
|
820
710
|
}
|
|
821
711
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
@@ -826,37 +716,28 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
826
716
|
)
|
|
827
717
|
}
|
|
828
718
|
observedRecyclerView = null
|
|
829
|
-
observedRecyclerKey = null
|
|
830
719
|
observedScrollListener = null
|
|
831
720
|
}
|
|
832
721
|
|
|
833
722
|
private fun attachNativeScrollerObserver(scroller: ScrollView) {
|
|
834
|
-
if (observedNativeScroller === scroller
|
|
723
|
+
if (observedNativeScroller === scroller) {
|
|
835
724
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
836
725
|
return
|
|
837
726
|
}
|
|
838
727
|
|
|
839
728
|
detachPrimaryScrollObserver()
|
|
840
|
-
offsetDiag("scrollview-attach-before", scroller)
|
|
841
729
|
observedNativeScroller = scroller
|
|
842
|
-
observedNativeScrollerKey = currentPageKey()
|
|
843
730
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
844
|
-
val key = currentPageKey()
|
|
845
731
|
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
846
732
|
override fun onScrollChanged() {
|
|
847
733
|
if (observedNativeScrollerListener !== this) {
|
|
848
734
|
removeScrollChangedListener(null, scroller, this)
|
|
849
735
|
return
|
|
850
736
|
}
|
|
851
|
-
if (key != currentPageKey() || !isAttachedToWindow ||
|
|
852
|
-
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores) return
|
|
853
737
|
val contentOffset = scroller.scrollY
|
|
854
|
-
|
|
855
|
-
pageOffsets[key] = contentOffset
|
|
738
|
+
pageOffsets[currentPageKey()] = contentOffset
|
|
856
739
|
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
857
|
-
offsetDiag("scrollview-positive-collapse-before", scroller)
|
|
858
740
|
setHeaderOffset(headerHeightPx)
|
|
859
|
-
offsetDiag("scrollview-positive-collapse-after", scroller)
|
|
860
741
|
}
|
|
861
742
|
}
|
|
862
743
|
}
|
|
@@ -873,11 +754,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
873
754
|
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
874
755
|
val scroller = observedNativeScroller
|
|
875
756
|
val listener = observedNativeScrollerListener
|
|
876
|
-
|
|
877
|
-
if (scroller != null && recordOffset && transitionPageOffsets == null) observedNativeScrollerKey?.let {
|
|
878
|
-
if (it !in pendingPageRestores) pageOffsets[it] = scroller.scrollY
|
|
879
|
-
}
|
|
880
|
-
offsetDiag("scrollview-detach-save-after", scroller)
|
|
757
|
+
if (scroller != null && recordOffset) pageOffsets[currentPageKey()] = scroller.scrollY
|
|
881
758
|
if (scroller != null && listener != null) {
|
|
882
759
|
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
883
760
|
}
|
|
@@ -888,7 +765,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
888
765
|
)
|
|
889
766
|
}
|
|
890
767
|
observedNativeScroller = null
|
|
891
|
-
observedNativeScrollerKey = null
|
|
892
768
|
observedNativeScrollerListener = null
|
|
893
769
|
observedNativeScrollerTreeObserver = null
|
|
894
770
|
}
|
|
@@ -917,23 +793,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
917
793
|
val key = pageKey(pageIndex)
|
|
918
794
|
if (ownedScrollerKeys[scroller] == key) return
|
|
919
795
|
ownedScrollerKeys[scroller] = key
|
|
920
|
-
if (nativeSmoothHeaderScrollEnabled) {
|
|
921
|
-
pendingPageRestores.add(key)
|
|
922
|
-
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to null
|
|
923
|
-
}
|
|
924
|
-
val generation = attachmentGeneration
|
|
925
796
|
postForCurrentAttachment {
|
|
926
|
-
if (ownedScrollerKeys[scroller] == key &&
|
|
927
|
-
scroller.
|
|
928
|
-
(key in pendingPageRestores && ownedScrollerRestoreRequests[key]?.first?.get() === scroller))) {
|
|
929
|
-
offsetDiag("owned-restore-before", scroller, key)
|
|
930
|
-
val saved = transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
931
|
-
scroller.host.restoreScrollOffset(saved)
|
|
932
|
-
if (nativeSmoothHeaderScrollEnabled) {
|
|
933
|
-
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to generation
|
|
934
|
-
recordOwnedScrollerOffset(scroller, key)
|
|
935
|
-
}
|
|
936
|
-
offsetDiag("owned-restore-after", scroller, key)
|
|
797
|
+
if (ownedScrollerKeys[scroller] == key && scroller.isAttachedToWindow) {
|
|
798
|
+
scroller.host.restoreScrollOffset(pageOffsets[key] ?: 0)
|
|
937
799
|
}
|
|
938
800
|
}
|
|
939
801
|
}
|
|
@@ -941,9 +803,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
941
803
|
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
942
804
|
scroller.stopMotion()
|
|
943
805
|
scroller.host.cancelPendingScrollRestore()
|
|
944
|
-
ownedScrollerKeys.remove(scroller)
|
|
945
|
-
if (ownedScrollerRestoreRequests[key]?.first?.get() === scroller) ownedScrollerRestoreRequests.remove(key)
|
|
946
|
-
}
|
|
806
|
+
ownedScrollerKeys.remove(scroller)
|
|
947
807
|
publishConsumedOffset(scroller, remove = true)
|
|
948
808
|
scroller.host.setPagerInset(this, null)
|
|
949
809
|
}
|
|
@@ -951,11 +811,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
951
811
|
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
952
812
|
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
953
813
|
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
954
|
-
if (nativeSmoothHeaderScrollEnabled) recordOwnedScrollerOffset(scroller, currentPageKey())
|
|
955
814
|
return
|
|
956
815
|
}
|
|
957
816
|
detachPrimaryScrollObserver()
|
|
958
|
-
offsetDiag("owned-attach-before", scroller)
|
|
959
817
|
observedOwnedScroller = scroller
|
|
960
818
|
observedOwnedScrollerKey = currentPageKey()
|
|
961
819
|
val key = currentPageKey()
|
|
@@ -966,7 +824,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
966
824
|
removeScrollChangedListener(null, scroller, this)
|
|
967
825
|
return
|
|
968
826
|
}
|
|
969
|
-
|
|
827
|
+
val contentOffset = scroller.scrollY
|
|
828
|
+
pageOffsets[key] = contentOffset
|
|
829
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
970
830
|
}
|
|
971
831
|
}
|
|
972
832
|
observedOwnedScrollerListener = listener
|
|
@@ -975,37 +835,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
975
835
|
}
|
|
976
836
|
}
|
|
977
837
|
|
|
978
|
-
private fun recordOwnedScrollerOffset(scroller: NativeScrollerViewport, key: String) {
|
|
979
|
-
if (ownedScrollerKeys[scroller] != key || !isAttachedToWindow || !scroller.isAttachedToWindow) return
|
|
980
|
-
if (key in pendingPageRestores) {
|
|
981
|
-
// A false native pending flag before our posted request executes is not cancellation.
|
|
982
|
-
val request = ownedScrollerRestoreRequests[key]
|
|
983
|
-
if (request == null || request.first.get() !== scroller || request.second != attachmentGeneration ||
|
|
984
|
-
scroller.width <= 0 || scroller.height <= 0 || scroller.host.hasPendingScrollRestore()) return
|
|
985
|
-
pendingPageRestores.remove(key)
|
|
986
|
-
ownedScrollerRestoreRequests.remove(key)
|
|
987
|
-
}
|
|
988
|
-
if (observedOwnedScroller !== scroller || observedOwnedScrollerKey != key ||
|
|
989
|
-
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore) return
|
|
990
|
-
val contentOffset = scroller.scrollY
|
|
991
|
-
if (pageOffsets[key] != contentOffset) offsetDiag("owned-cache-before-new=$contentOffset", scroller, key)
|
|
992
|
-
pageOffsets[key] = contentOffset
|
|
993
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
994
|
-
offsetDiag("owned-positive-collapse-before", scroller, key)
|
|
995
|
-
setHeaderOffset(headerHeightPx)
|
|
996
|
-
offsetDiag("owned-positive-collapse-after", scroller, key)
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
838
|
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
1001
839
|
val scroller = observedOwnedScroller ?: return
|
|
1002
840
|
scroller.stopMotion()
|
|
1003
841
|
publishConsumedOffset(scroller, remove = true)
|
|
1004
|
-
|
|
1005
|
-
if (recordOffset && transitionPageOffsets == null) observedOwnedScrollerKey?.let {
|
|
1006
|
-
if (it !in pendingPageRestores) pageOffsets[it] = scroller.scrollY
|
|
1007
|
-
}
|
|
1008
|
-
offsetDiag("owned-detach-save-after", scroller)
|
|
842
|
+
if (recordOffset) observedOwnedScrollerKey?.let { pageOffsets[it] = scroller.scrollY }
|
|
1009
843
|
observedOwnedScrollerListener?.let {
|
|
1010
844
|
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
1011
845
|
}
|
|
@@ -1059,7 +893,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1059
893
|
val key = pageKey(pageIndex)
|
|
1060
894
|
if (restoredRecyclerKeys[recycler] != key) {
|
|
1061
895
|
restoredRecyclerKeys[recycler] = key
|
|
1062
|
-
if (nativeSmoothHeaderScrollEnabled) pendingPageRestores.add(key)
|
|
1063
896
|
val generation = attachmentGeneration
|
|
1064
897
|
recycler.post {
|
|
1065
898
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
@@ -1068,16 +901,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1068
901
|
if (restoredRecyclerKeys[recycler] == key) restoredRecyclerKeys.remove(recycler)
|
|
1069
902
|
return@post
|
|
1070
903
|
}
|
|
1071
|
-
val saved =
|
|
904
|
+
val saved = pageOffsets[key] ?: 0
|
|
1072
905
|
val current = recycler.computeVerticalScrollOffset()
|
|
1073
|
-
offsetDiag("recycler-restore-before-saved=$saved", recycler, key)
|
|
1074
|
-
if (nativeSmoothHeaderScrollEnabled && (recycler.height <= 0 || recycler.childCount == 0)) {
|
|
1075
|
-
restoredRecyclerKeys.remove(recycler)
|
|
1076
|
-
return@post
|
|
1077
|
-
}
|
|
1078
906
|
if (saved != current) recycler.scrollBy(0, saved - current)
|
|
1079
|
-
pendingPageRestores.remove(key)
|
|
1080
|
-
offsetDiag("recycler-restore-after", recycler, key)
|
|
1081
907
|
}
|
|
1082
908
|
}
|
|
1083
909
|
}
|
|
@@ -1123,7 +949,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1123
949
|
val key = pageKey(pageIndex)
|
|
1124
950
|
if (restoredScrollerKeys[scroller] != key) {
|
|
1125
951
|
restoredScrollerKeys[scroller] = key
|
|
1126
|
-
if (nativeSmoothHeaderScrollEnabled) pendingPageRestores.add(key)
|
|
1127
952
|
val generation = attachmentGeneration
|
|
1128
953
|
scroller.post {
|
|
1129
954
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
@@ -1132,16 +957,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1132
957
|
if (restoredScrollerKeys[scroller] == key) restoredScrollerKeys.remove(scroller)
|
|
1133
958
|
return@post
|
|
1134
959
|
}
|
|
1135
|
-
val saved =
|
|
1136
|
-
offsetDiag("scrollview-restore-before-saved=$saved", scroller, key)
|
|
1137
|
-
if (nativeSmoothHeaderScrollEnabled &&
|
|
1138
|
-
(scroller.height <= 0 || (scroller.getChildAt(0)?.height ?: 0) <= 0)) {
|
|
1139
|
-
restoredScrollerKeys.remove(scroller)
|
|
1140
|
-
return@post
|
|
1141
|
-
}
|
|
960
|
+
val saved = pageOffsets[key] ?: 0
|
|
1142
961
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1143
|
-
pendingPageRestores.remove(key)
|
|
1144
|
-
offsetDiag("scrollview-restore-after", scroller, key)
|
|
1145
962
|
}
|
|
1146
963
|
}
|
|
1147
964
|
}
|
|
@@ -1582,10 +1399,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1582
1399
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1583
1400
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1584
1401
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1585
|
-
if (type == ViewCompat.TYPE_TOUCH) {
|
|
1586
|
-
pendingPageRestores.remove(currentPageKey())
|
|
1587
|
-
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
1588
|
-
}
|
|
1589
1402
|
}
|
|
1590
1403
|
|
|
1591
1404
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
@@ -1679,13 +1492,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1679
1492
|
restoredRecyclerKeys.clear()
|
|
1680
1493
|
restoredScrollerKeys.clear()
|
|
1681
1494
|
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
1682
|
-
resumeTransitionSource?.let { source ->
|
|
1683
|
-
resumeTransitionSource = null
|
|
1684
|
-
if (source in 0 until adapter.itemCount) {
|
|
1685
|
-
pager.setCurrentItem(source, false)
|
|
1686
|
-
selectPage(source)
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
1495
|
log(
|
|
1690
1496
|
"host-attach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1691
1497
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1705,15 +1511,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1705
1511
|
nestedNativeGestureStarted = false
|
|
1706
1512
|
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
1707
1513
|
detachPrimaryScrollObserver()
|
|
1708
|
-
transitionHeaderOffsets?.let { headers ->
|
|
1709
|
-
resumeTransitionSource = transitionSourcePage
|
|
1710
|
-
syntheticPageRestore = true
|
|
1711
|
-
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: 0)
|
|
1712
|
-
syntheticPageRestore = false
|
|
1713
|
-
}
|
|
1714
|
-
transitionHeaderOffsets = null
|
|
1715
|
-
transitionPageOffsets = null
|
|
1716
|
-
transitionTargetPage = null
|
|
1717
1514
|
// Snapshot live keys before restoration removes their padding records.
|
|
1718
1515
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
1719
1516
|
restoreRecyclerInsets(recycler)
|
|
@@ -43,7 +43,6 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
43
43
|
)
|
|
44
44
|
host.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
|
45
45
|
override fun onPageScrolled(position: Int, offset: Float, offsetPixels: Int) {
|
|
46
|
-
host.onPagerScrolled(position, offset)
|
|
47
46
|
dispatch(reactContext, host, PageScrollEvent(host.id, position, offset))
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -56,7 +55,6 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
override fun onPageScrollStateChanged(state: Int) {
|
|
59
|
-
host.onPagerScrollStateChanged(state)
|
|
60
58
|
val value = when (state) {
|
|
61
59
|
ViewPager2.SCROLL_STATE_IDLE -> "idle"
|
|
62
60
|
ViewPager2.SCROLL_STATE_DRAGGING -> "dragging"
|
|
@@ -326,19 +324,13 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
326
324
|
|
|
327
325
|
override fun setPage(view: CollapsiblePagerHost?, selectedPage: Int) {
|
|
328
326
|
if (view != null && selectedPage in 0 until view.adapter.itemCount) {
|
|
329
|
-
view.beginPageTransition(selectedPage)
|
|
330
327
|
view.pager.setCurrentItem(selectedPage, true)
|
|
331
|
-
if (view.pager.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
|
|
332
|
-
view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
|
|
333
|
-
}
|
|
334
328
|
}
|
|
335
329
|
}
|
|
336
330
|
|
|
337
331
|
override fun setPageWithoutAnimation(view: CollapsiblePagerHost?, selectedPage: Int) {
|
|
338
332
|
if (view != null && selectedPage in 0 until view.adapter.itemCount) {
|
|
339
|
-
view.beginPageTransition(selectedPage)
|
|
340
333
|
view.pager.setCurrentItem(selectedPage, false)
|
|
341
|
-
view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
|
|
342
334
|
}
|
|
343
335
|
}
|
|
344
336
|
|
|
@@ -255,8 +255,6 @@ class NativeScrollerView(context: Context) : FrameLayout(context) {
|
|
|
255
255
|
|
|
256
256
|
internal fun cancelPendingScrollRestore() = pendingRestore.cancel()
|
|
257
257
|
|
|
258
|
-
internal fun hasPendingScrollRestore(): Boolean = pendingRestore.target != null
|
|
259
|
-
|
|
260
258
|
internal fun setRefreshOffset(value: Int) {
|
|
261
259
|
refreshOffset = value
|
|
262
260
|
applyRefreshOffset()
|
|
@@ -27,16 +27,5 @@ fun main() {
|
|
|
27
27
|
restore.cancel()
|
|
28
28
|
check(restore.target == null)
|
|
29
29
|
check(restore.anchor(120) == 120)
|
|
30
|
-
// Cancellation clears the old target without requiring the reached offset to change.
|
|
31
|
-
restore.request(target = 640, reached = 120)
|
|
32
|
-
check(restore.anchor(120) == 640)
|
|
33
|
-
restore.cancel()
|
|
34
|
-
check(restore.target == null)
|
|
35
|
-
check(restore.anchor(120) == 120)
|
|
36
|
-
// A fresh request owns its target after cancellation.
|
|
37
|
-
restore.request(target = 320, reached = 120)
|
|
38
|
-
check(restore.target == 320)
|
|
39
|
-
restore.settle(reached = 320)
|
|
40
|
-
check(restore.target == null)
|
|
41
30
|
println("Native scroller pending restore: clamp retry, partial growth, settle and cancel passed")
|
|
42
31
|
}
|
package/docs/SPEC.md
CHANGED
|
@@ -3,42 +3,6 @@
|
|
|
3
3
|
Existing PagerView and CollapsiblePagerView APIs are documented in README.md.
|
|
4
4
|
The sections below define native coordination boundaries without new React props.
|
|
5
5
|
|
|
6
|
-
## Smooth per-page screen restoration
|
|
7
|
-
|
|
8
|
-
When nativeSmoothHeaderScrollEnabled is enabled with a collapsible header, each
|
|
9
|
-
page owns its header collapse and body offset. For a settled position outside
|
|
10
|
-
pull overscroll, Q = H + L, H = min(Q, C), and L = max(Q - C, 0), where C is
|
|
11
|
-
the header-collapse distance. These are semantic coordinates, not new props.
|
|
12
|
-
An unvisited page starts expanded
|
|
13
|
-
at body start. Leaving a page preserves its expanded, partial or collapsed header
|
|
14
|
-
and its body position; tap, swipe and programmatic selection restore the same
|
|
15
|
-
complete screen state. Legacy mode and pagers without a collapsible header retain
|
|
16
|
-
their existing behavior.
|
|
17
|
-
|
|
18
|
-
Horizontal progress interpolates the shared header between immutable source and
|
|
19
|
-
target snapshots. Native body geometry follows that header without obscured rows
|
|
20
|
-
or blank gaps. Temporary transition offsets MUST NOT overwrite page snapshots.
|
|
21
|
-
Non-adjacent commands use the original source and destination, not intermediate
|
|
22
|
-
pages, as the header interpolation endpoints. Cancellation restores the source
|
|
23
|
-
and all temporarily compensated pages before observation resumes.
|
|
24
|
-
|
|
25
|
-
A scroll observer owns one page identity and generation independently of mutable
|
|
26
|
-
selection. Detach, replacement and late notifications MUST NOT save another
|
|
27
|
-
page's offset. Zero layout and unresolved replacement identity preserve pending
|
|
28
|
-
restoration until an actual usable layout; settled content shrink still clamps to
|
|
29
|
-
the legal range. This state is native UI-thread state, not persistent wallet data
|
|
30
|
-
or per-frame JavaScript synchronization. Vertical scrolling, refresh, snapping
|
|
31
|
-
and repeated-current-page selection retain their existing contracts.
|
|
32
|
-
|
|
33
|
-
Android owned NativeScroller restoration keeps its intent while a posted request
|
|
34
|
-
has not executed or native content is still too short to reach the saved offset.
|
|
35
|
-
Only the same viewport, page key and attachment generation may reconcile an
|
|
36
|
-
issued request. Once native restoration settles or is cancelled by touch, an
|
|
37
|
-
imperative command or content replacement, the host resumes recording its live
|
|
38
|
-
body offset. Existing pre-draw observation also reconciles a same-position command
|
|
39
|
-
that emits no scroll notification. Zero-sized geometry retains the pending intent;
|
|
40
|
-
no timeout infers that delayed content is permanently short.
|
|
41
|
-
|
|
42
6
|
## Independent NativeScroller migration
|
|
43
7
|
|
|
44
8
|
Status: implemented in source. Both native builds pass. Android focused emulator
|
|
@@ -490,8 +454,7 @@ When restoring a retained page's cached vertical offset, the pager limits only
|
|
|
490
454
|
the upper bound to the current content height, viewport and applied insets. A
|
|
491
455
|
cache captured before rows were removed MUST NOT restore an offset below the new
|
|
492
456
|
content bottom, including when removal occurs during a page transition. Valid
|
|
493
|
-
deep offsets and
|
|
494
|
-
follows the per-page reading-position contract above.
|
|
457
|
+
deep offsets and the existing lower-bound/header alignment rules are unchanged.
|
|
495
458
|
This check runs only on explicit restoration, not on scroll frames; UIKit pull
|
|
496
459
|
overscroll and refresh settlement retain their existing ownership. An unmeasured
|
|
497
460
|
zero-height viewport defers this upper-bound limit to a later restoration.
|