@onekeyfe/react-native-pager-view 3.0.170 → 3.0.172
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 +283 -21
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +8 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +2 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +11 -0
- package/docs/SPEC.md +50 -1
- package/ios/RNCCollapsiblePagerViewComponentView.mm +275 -25
- package/ios/RNCNativeScrollerComponentView.h +1 -0
- package/ios/RNCNativeScrollerComponentView.mm +42 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +257 -0
- package/package.json +2 -2
|
@@ -123,6 +123,7 @@ 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?>>()
|
|
126
127
|
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
127
128
|
private var refreshForegroundWasVisible = false
|
|
128
129
|
private val refreshForegroundMatrix = Matrix()
|
|
@@ -130,13 +131,51 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
130
131
|
private val refreshForegroundAncestors = ArrayList<View>()
|
|
131
132
|
private var refreshForegroundAncestorAlpha = 1f
|
|
132
133
|
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
|
+
private class PageRestoreRequest(view: View, val pageIndex: Int, val generation: Int) {
|
|
143
|
+
val view = WeakReference(view)
|
|
144
|
+
}
|
|
145
|
+
private val pageRestoreRequests = HashMap<String, PageRestoreRequest>()
|
|
146
|
+
|
|
147
|
+
private fun registerPageRestore(key: String, view: View, pageIndex: Int): PageRestoreRequest? {
|
|
148
|
+
if (!nativeSmoothHeaderScrollEnabled) return null
|
|
149
|
+
val previous = pageRestoreRequests[key]?.view?.get()
|
|
150
|
+
if (previous is NativeScrollerViewport && previous !== view) previous.host.cancelPendingScrollRestore()
|
|
151
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
152
|
+
return PageRestoreRequest(view, pageIndex, attachmentGeneration).also {
|
|
153
|
+
pageRestoreRequests[key] = it
|
|
154
|
+
pendingPageRestores.add(key)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun isCurrentPageRestore(key: String, request: PageRestoreRequest?): Boolean =
|
|
159
|
+
!nativeSmoothHeaderScrollEnabled || (request != null && pageRestoreRequests[key] === request &&
|
|
160
|
+
key in pendingPageRestores && request.generation == attachmentGeneration &&
|
|
161
|
+
pageKey(request.pageIndex) == key)
|
|
162
|
+
|
|
163
|
+
private fun clearPageRestoreForView(key: String, view: View, request: PageRestoreRequest? = null) {
|
|
164
|
+
if (request != null && pageRestoreRequests[key] !== request) return
|
|
165
|
+
if (pageRestoreRequests[key]?.view?.get() !== view) return
|
|
166
|
+
pageRestoreRequests.remove(key)
|
|
167
|
+
pendingPageRestores.remove(key)
|
|
168
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
169
|
+
}
|
|
133
170
|
private var observedRecyclerView: RecyclerView? = null
|
|
171
|
+
private var observedRecyclerKey: String? = null
|
|
134
172
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
135
173
|
private var observedOwnedScroller: NativeScrollerViewport? = null
|
|
136
174
|
private var observedOwnedScrollerKey: String? = null
|
|
137
175
|
private var observedOwnedScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
138
176
|
private var observedOwnedScrollerTreeObserver: ViewTreeObserver? = null
|
|
139
177
|
private var observedNativeScroller: ScrollView? = null
|
|
178
|
+
private var observedNativeScrollerKey: String? = null
|
|
140
179
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
141
180
|
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
142
181
|
private var observedPageRemoved = false
|
|
@@ -189,6 +228,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
189
228
|
var headerOffsetPx = 0
|
|
190
229
|
private set
|
|
191
230
|
var pageKeys: List<String> = emptyList()
|
|
231
|
+
set(value) {
|
|
232
|
+
if (field == value) return
|
|
233
|
+
pageRestoreRequests.entries.toList().forEach { (key, request) ->
|
|
234
|
+
if ((value.getOrNull(request.pageIndex) ?: "page-${request.pageIndex}") != key) {
|
|
235
|
+
(request.view.get() as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
236
|
+
pageRestoreRequests.remove(key)
|
|
237
|
+
pendingPageRestores.remove(key)
|
|
238
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
field = value
|
|
242
|
+
}
|
|
192
243
|
var retainedPages: String = "[]"
|
|
193
244
|
var onHeaderOffsetChanged: (() -> Unit)? = null
|
|
194
245
|
var nativeSmoothHeaderScrollEnabled = false
|
|
@@ -230,6 +281,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
230
281
|
OneKeyLog.debug("CollapsiblePager", "host=$hostIdentity $message")
|
|
231
282
|
}
|
|
232
283
|
|
|
284
|
+
private var offsetDiagSequence = 0L
|
|
285
|
+
|
|
286
|
+
private fun offsetDiag(phase: String, view: View? = null, key: String = currentPageKey()) {
|
|
287
|
+
if ((context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) == 0) return
|
|
288
|
+
val safeKey = if (key in setOf("portfolio", "perps", "defi", "nft", "history")) key else "other"
|
|
289
|
+
val observed = observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView
|
|
290
|
+
OneKeyLog.debug("CollapsiblePager", "[HOME-OFFSET-DIAG] phase=$phase seq=${++offsetDiagSequence} " +
|
|
291
|
+
"wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} host=$hostIdentity " +
|
|
292
|
+
"generation=$attachmentGeneration page=$selectedPage key=$safeKey header=$headerOffsetPx headerHeight=$headerHeightPx " +
|
|
293
|
+
"view=${view?.let { System.identityHashCode(it) }} tag=${view?.id} observed=${observed?.let { System.identityHashCode(it) }} " +
|
|
294
|
+
"offset=${view?.let(::verticalScrollOffset)} cache=${pageOffsets[key]} boundsHeight=${view?.height}")
|
|
295
|
+
}
|
|
296
|
+
|
|
233
297
|
private fun outerPagerIndex(): Int {
|
|
234
298
|
var ancestor = parent as? View
|
|
235
299
|
while (ancestor != null) {
|
|
@@ -364,6 +428,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
364
428
|
fun removeAllReactChildren() {
|
|
365
429
|
releaseRefreshForeground()
|
|
366
430
|
detachPrimaryScrollObserver()
|
|
431
|
+
pageRestoreRequests.clear()
|
|
432
|
+
pendingPageRestores.clear()
|
|
433
|
+
ownedScrollerRestoreRequests.clear()
|
|
367
434
|
// Weak keys may disappear after size is read; Collection.toArray tolerates this.
|
|
368
435
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
369
436
|
restoreRecyclerInsets(recycler)
|
|
@@ -386,16 +453,80 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
386
453
|
|
|
387
454
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
388
455
|
|
|
456
|
+
internal fun beginPageTransition(target: Int? = null) {
|
|
457
|
+
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
458
|
+
if (transitionHeaderOffsets == null) {
|
|
459
|
+
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
460
|
+
detachPrimaryScrollObserver()
|
|
461
|
+
transitionSourcePage = selectedPage
|
|
462
|
+
transitionHeaderOffsets = pageHeaderOffsets.toMap()
|
|
463
|
+
transitionPageOffsets = pageOffsets.toMap()
|
|
464
|
+
}
|
|
465
|
+
if (target != null) transitionTargetPage = target
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
internal fun onPagerScrolled(position: Int, offset: Float) {
|
|
469
|
+
val headers = transitionHeaderOffsets ?: return
|
|
470
|
+
val target = transitionTargetPage
|
|
471
|
+
val first = if (target != null) transitionSourcePage else position
|
|
472
|
+
val second = target ?: (position + 1).coerceAtMost(adapter.itemCount - 1)
|
|
473
|
+
val fraction = if (target != null && target != first) {
|
|
474
|
+
((position + offset - first) / (target - first)).coerceIn(0f, 1f)
|
|
475
|
+
} else offset
|
|
476
|
+
val firstH = headers[pageKey(first)] ?: 0
|
|
477
|
+
val secondH = headers[pageKey(second)] ?: 0
|
|
478
|
+
syntheticPageRestore = true
|
|
479
|
+
setHeaderOffset((firstH + (secondH - firstH) * fraction).roundToInt())
|
|
480
|
+
syntheticPageRestore = false
|
|
481
|
+
updateNativeTabProgress(position, offset)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
internal fun onPagerScrollStateChanged(state: Int) {
|
|
485
|
+
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
486
|
+
beginPageTransition()
|
|
487
|
+
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
488
|
+
finishPageTransition(pager.currentItem)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun finishPageTransition(position: Int) {
|
|
493
|
+
val headers = transitionHeaderOffsets ?: return
|
|
494
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
495
|
+
releaseRefreshForeground()
|
|
496
|
+
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
497
|
+
syntheticPageRestore = true
|
|
498
|
+
setHeaderOffset(headers[currentPageKey()] ?: 0)
|
|
499
|
+
transitionHeaderOffsets = null
|
|
500
|
+
transitionPageOffsets = null
|
|
501
|
+
transitionTargetPage = null
|
|
502
|
+
syntheticPageRestore = false
|
|
503
|
+
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
504
|
+
prepareAdjacentPages()
|
|
505
|
+
attachPrimaryScrollObserver()
|
|
506
|
+
}
|
|
507
|
+
|
|
389
508
|
fun selectPage(position: Int) {
|
|
509
|
+
offsetDiag("select-begin-target=$position", observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView)
|
|
390
510
|
pendingRefreshPull = null
|
|
391
511
|
if (adapter.itemCount == 0) return
|
|
512
|
+
if (nativeSmoothHeaderScrollEnabled && headerHeightPx > 0) {
|
|
513
|
+
if (transitionHeaderOffsets != null) return
|
|
514
|
+
beginPageTransition(position)
|
|
515
|
+
finishPageTransition(position)
|
|
516
|
+
return
|
|
517
|
+
}
|
|
392
518
|
detachPrimaryScrollObserver()
|
|
393
519
|
releaseRefreshForeground()
|
|
394
520
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
395
521
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
396
522
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
397
523
|
?: 0
|
|
398
|
-
if (
|
|
524
|
+
offsetDiag("select-offset=$savedOffset-source=${if (pageOffsets[currentPageKey()] != null) "saved" else "live"}")
|
|
525
|
+
if (savedOffset > 0) {
|
|
526
|
+
offsetDiag("select-force-collapse-before")
|
|
527
|
+
setHeaderOffset(headerHeightPx)
|
|
528
|
+
offsetDiag("select-force-collapse-after")
|
|
529
|
+
}
|
|
399
530
|
postForCurrentAttachment {
|
|
400
531
|
prepareAdjacentPages()
|
|
401
532
|
attachPrimaryScrollObserver()
|
|
@@ -537,9 +668,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
537
668
|
|
|
538
669
|
private fun setHeaderOffset(offset: Int) {
|
|
539
670
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
540
|
-
val
|
|
671
|
+
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
672
|
+
val maximum = if (transitionHeaderOffsets != null || pending) headerHeightPx else maximumHeaderOffset()
|
|
673
|
+
val next = offset.coerceIn(0, maximum)
|
|
541
674
|
if (headerOffsetPx == next) return
|
|
542
675
|
headerOffsetPx = next
|
|
676
|
+
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
677
|
+
pageHeaderOffsets[currentPageKey()] = next
|
|
678
|
+
}
|
|
543
679
|
applyHeaderOffset()
|
|
544
680
|
}
|
|
545
681
|
|
|
@@ -656,13 +792,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
656
792
|
}
|
|
657
793
|
|
|
658
794
|
private fun attachPrimaryScrollObserver() {
|
|
795
|
+
if (transitionHeaderOffsets != null) return
|
|
659
796
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
660
797
|
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
661
798
|
if (observedPageRemoved || previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
799
|
+
offsetDiag("primary-replacement-before-removed=$observedPageRemoved", previous)
|
|
662
800
|
observedPageRemoved = false
|
|
663
|
-
detachPrimaryScrollObserver()
|
|
801
|
+
detachPrimaryScrollObserver(recordOffset = !nativeSmoothHeaderScrollEnabled)
|
|
664
802
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
665
|
-
|
|
803
|
+
offsetDiag("primary-replacement-cache-before", previous)
|
|
804
|
+
if (!nativeSmoothHeaderScrollEnabled) pageOffsets[currentPageKey()] = 0
|
|
805
|
+
offsetDiag("primary-replacement-cache-after", previous)
|
|
666
806
|
}
|
|
667
807
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
668
808
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
@@ -672,24 +812,33 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
672
812
|
}
|
|
673
813
|
|
|
674
814
|
private fun attachRecyclerObserver(recycler: RecyclerView) {
|
|
675
|
-
if (observedRecyclerView === recycler) {
|
|
815
|
+
if (observedRecyclerView === recycler && observedRecyclerKey == currentPageKey()) {
|
|
676
816
|
applyRecyclerInsets(recycler, selectedPage)
|
|
677
817
|
publishConsumedOffset(recycler)
|
|
678
818
|
return
|
|
679
819
|
}
|
|
680
820
|
|
|
681
821
|
detachPrimaryScrollObserver()
|
|
822
|
+
offsetDiag("recycler-attach-before", recycler)
|
|
682
823
|
observedRecyclerView = recycler
|
|
824
|
+
observedRecyclerKey = currentPageKey()
|
|
683
825
|
applyRecyclerInsets(recycler, selectedPage)
|
|
684
826
|
publishConsumedOffset(recycler)
|
|
827
|
+
val key = currentPageKey()
|
|
685
828
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
686
829
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
830
|
+
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
831
|
+
key != currentPageKey() || !isAttachedToWindow ||
|
|
832
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores) return
|
|
687
833
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
688
|
-
pageOffsets[currentPageKey()]
|
|
834
|
+
if (pageOffsets[currentPageKey()] != contentOffset) offsetDiag("recycler-cache-before-new=$contentOffset", recyclerView)
|
|
835
|
+
pageOffsets[key] = contentOffset
|
|
689
836
|
// Nested touch scrolling moves list content only after the header has
|
|
690
837
|
// collapsed. Programmatic list jumps bypass those parent callbacks.
|
|
691
838
|
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
839
|
+
offsetDiag("recycler-positive-collapse-before", recyclerView)
|
|
692
840
|
setHeaderOffset(headerHeightPx)
|
|
841
|
+
offsetDiag("recycler-positive-collapse-after", recyclerView)
|
|
693
842
|
}
|
|
694
843
|
}
|
|
695
844
|
}
|
|
@@ -705,7 +854,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
705
854
|
val recycler = observedRecyclerView
|
|
706
855
|
val listener = observedScrollListener
|
|
707
856
|
if (recycler != null) {
|
|
708
|
-
|
|
857
|
+
offsetDiag("recycler-detach-save-before-record=$recordOffset", recycler)
|
|
858
|
+
if (recordOffset && transitionPageOffsets == null) observedRecyclerKey?.let {
|
|
859
|
+
if (it !in pendingPageRestores) pageOffsets[it] = recycler.computeVerticalScrollOffset()
|
|
860
|
+
}
|
|
861
|
+
offsetDiag("recycler-detach-save-after", recycler)
|
|
709
862
|
publishConsumedOffset(recycler, remove = true)
|
|
710
863
|
}
|
|
711
864
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
@@ -716,28 +869,37 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
716
869
|
)
|
|
717
870
|
}
|
|
718
871
|
observedRecyclerView = null
|
|
872
|
+
observedRecyclerKey = null
|
|
719
873
|
observedScrollListener = null
|
|
720
874
|
}
|
|
721
875
|
|
|
722
876
|
private fun attachNativeScrollerObserver(scroller: ScrollView) {
|
|
723
|
-
if (observedNativeScroller === scroller) {
|
|
877
|
+
if (observedNativeScroller === scroller && observedNativeScrollerKey == currentPageKey()) {
|
|
724
878
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
725
879
|
return
|
|
726
880
|
}
|
|
727
881
|
|
|
728
882
|
detachPrimaryScrollObserver()
|
|
883
|
+
offsetDiag("scrollview-attach-before", scroller)
|
|
729
884
|
observedNativeScroller = scroller
|
|
885
|
+
observedNativeScrollerKey = currentPageKey()
|
|
730
886
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
887
|
+
val key = currentPageKey()
|
|
731
888
|
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
732
889
|
override fun onScrollChanged() {
|
|
733
890
|
if (observedNativeScrollerListener !== this) {
|
|
734
891
|
removeScrollChangedListener(null, scroller, this)
|
|
735
892
|
return
|
|
736
893
|
}
|
|
894
|
+
if (key != currentPageKey() || !isAttachedToWindow ||
|
|
895
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores) return
|
|
737
896
|
val contentOffset = scroller.scrollY
|
|
738
|
-
pageOffsets[currentPageKey()]
|
|
897
|
+
if (pageOffsets[currentPageKey()] != contentOffset) offsetDiag("scrollview-cache-before-new=$contentOffset", scroller)
|
|
898
|
+
pageOffsets[key] = contentOffset
|
|
739
899
|
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
900
|
+
offsetDiag("scrollview-positive-collapse-before", scroller)
|
|
740
901
|
setHeaderOffset(headerHeightPx)
|
|
902
|
+
offsetDiag("scrollview-positive-collapse-after", scroller)
|
|
741
903
|
}
|
|
742
904
|
}
|
|
743
905
|
}
|
|
@@ -754,7 +916,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
754
916
|
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
755
917
|
val scroller = observedNativeScroller
|
|
756
918
|
val listener = observedNativeScrollerListener
|
|
757
|
-
|
|
919
|
+
offsetDiag("scrollview-detach-save-before-record=$recordOffset", scroller)
|
|
920
|
+
if (scroller != null && recordOffset && transitionPageOffsets == null) observedNativeScrollerKey?.let {
|
|
921
|
+
if (it !in pendingPageRestores) pageOffsets[it] = scroller.scrollY
|
|
922
|
+
}
|
|
923
|
+
offsetDiag("scrollview-detach-save-after", scroller)
|
|
758
924
|
if (scroller != null && listener != null) {
|
|
759
925
|
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
760
926
|
}
|
|
@@ -765,6 +931,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
765
931
|
)
|
|
766
932
|
}
|
|
767
933
|
observedNativeScroller = null
|
|
934
|
+
observedNativeScrollerKey = null
|
|
768
935
|
observedNativeScrollerListener = null
|
|
769
936
|
observedNativeScrollerTreeObserver = null
|
|
770
937
|
}
|
|
@@ -793,9 +960,21 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
793
960
|
val key = pageKey(pageIndex)
|
|
794
961
|
if (ownedScrollerKeys[scroller] == key) return
|
|
795
962
|
ownedScrollerKeys[scroller] = key
|
|
963
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
964
|
+
if (nativeSmoothHeaderScrollEnabled) ownedScrollerRestoreRequests[key] = WeakReference(scroller) to null
|
|
965
|
+
val generation = attachmentGeneration
|
|
796
966
|
postForCurrentAttachment {
|
|
797
|
-
if (ownedScrollerKeys[scroller] == key &&
|
|
798
|
-
scroller.
|
|
967
|
+
if (isCurrentPageRestore(key, restoreRequest) && ownedScrollerKeys[scroller] == key && generation == attachmentGeneration &&
|
|
968
|
+
scroller.isAttachedToWindow && (!nativeSmoothHeaderScrollEnabled ||
|
|
969
|
+
(key in pendingPageRestores && ownedScrollerRestoreRequests[key]?.first?.get() === scroller))) {
|
|
970
|
+
offsetDiag("owned-restore-before", scroller, key)
|
|
971
|
+
val saved = transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
972
|
+
scroller.host.restoreScrollOffset(saved)
|
|
973
|
+
if (nativeSmoothHeaderScrollEnabled) {
|
|
974
|
+
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to generation
|
|
975
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
976
|
+
}
|
|
977
|
+
offsetDiag("owned-restore-after", scroller, key)
|
|
799
978
|
}
|
|
800
979
|
}
|
|
801
980
|
}
|
|
@@ -803,7 +982,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
803
982
|
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
804
983
|
scroller.stopMotion()
|
|
805
984
|
scroller.host.cancelPendingScrollRestore()
|
|
806
|
-
ownedScrollerKeys.remove(scroller)
|
|
985
|
+
ownedScrollerKeys.remove(scroller)?.let { key ->
|
|
986
|
+
clearPageRestoreForView(key, scroller)
|
|
987
|
+
if (ownedScrollerRestoreRequests[key]?.first?.get() === scroller) ownedScrollerRestoreRequests.remove(key)
|
|
988
|
+
}
|
|
807
989
|
publishConsumedOffset(scroller, remove = true)
|
|
808
990
|
scroller.host.setPagerInset(this, null)
|
|
809
991
|
}
|
|
@@ -811,9 +993,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
811
993
|
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
812
994
|
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
813
995
|
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
996
|
+
if (nativeSmoothHeaderScrollEnabled) recordOwnedScrollerOffset(scroller, currentPageKey())
|
|
814
997
|
return
|
|
815
998
|
}
|
|
816
999
|
detachPrimaryScrollObserver()
|
|
1000
|
+
offsetDiag("owned-attach-before", scroller)
|
|
817
1001
|
observedOwnedScroller = scroller
|
|
818
1002
|
observedOwnedScrollerKey = currentPageKey()
|
|
819
1003
|
val key = currentPageKey()
|
|
@@ -824,9 +1008,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
824
1008
|
removeScrollChangedListener(null, scroller, this)
|
|
825
1009
|
return
|
|
826
1010
|
}
|
|
827
|
-
|
|
828
|
-
pageOffsets[key] = contentOffset
|
|
829
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
1011
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
830
1012
|
}
|
|
831
1013
|
}
|
|
832
1014
|
observedOwnedScrollerListener = listener
|
|
@@ -835,11 +1017,36 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
835
1017
|
}
|
|
836
1018
|
}
|
|
837
1019
|
|
|
1020
|
+
private fun recordOwnedScrollerOffset(scroller: NativeScrollerViewport, key: String) {
|
|
1021
|
+
if (ownedScrollerKeys[scroller] != key || !isAttachedToWindow || !scroller.isAttachedToWindow) return
|
|
1022
|
+
if (key in pendingPageRestores) {
|
|
1023
|
+
// A false native pending flag before our posted request executes is not cancellation.
|
|
1024
|
+
val request = ownedScrollerRestoreRequests[key]
|
|
1025
|
+
if (request == null || request.first.get() !== scroller || request.second != attachmentGeneration ||
|
|
1026
|
+
scroller.width <= 0 || scroller.height <= 0 || scroller.host.hasPendingScrollRestore()) return
|
|
1027
|
+
clearPageRestoreForView(key, scroller)
|
|
1028
|
+
}
|
|
1029
|
+
if (observedOwnedScroller !== scroller || observedOwnedScrollerKey != key ||
|
|
1030
|
+
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore) return
|
|
1031
|
+
val contentOffset = scroller.scrollY
|
|
1032
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("owned-cache-before-new=$contentOffset", scroller, key)
|
|
1033
|
+
pageOffsets[key] = contentOffset
|
|
1034
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1035
|
+
offsetDiag("owned-positive-collapse-before", scroller, key)
|
|
1036
|
+
setHeaderOffset(headerHeightPx)
|
|
1037
|
+
offsetDiag("owned-positive-collapse-after", scroller, key)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
838
1041
|
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
839
1042
|
val scroller = observedOwnedScroller ?: return
|
|
840
1043
|
scroller.stopMotion()
|
|
841
1044
|
publishConsumedOffset(scroller, remove = true)
|
|
842
|
-
|
|
1045
|
+
offsetDiag("owned-detach-save-before-record=$recordOffset", scroller)
|
|
1046
|
+
if (recordOffset && transitionPageOffsets == null) observedOwnedScrollerKey?.let {
|
|
1047
|
+
if (it !in pendingPageRestores) pageOffsets[it] = scroller.scrollY
|
|
1048
|
+
}
|
|
1049
|
+
offsetDiag("owned-detach-save-after", scroller)
|
|
843
1050
|
observedOwnedScrollerListener?.let {
|
|
844
1051
|
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
845
1052
|
}
|
|
@@ -893,17 +1100,32 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
893
1100
|
val key = pageKey(pageIndex)
|
|
894
1101
|
if (restoredRecyclerKeys[recycler] != key) {
|
|
895
1102
|
restoredRecyclerKeys[recycler] = key
|
|
1103
|
+
val restoreRequest = registerPageRestore(key, recycler, pageIndex)
|
|
896
1104
|
val generation = attachmentGeneration
|
|
897
1105
|
recycler.post {
|
|
1106
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1107
|
+
offsetDiag("post-skip-cancelled-page=$pageIndex", recycler, key)
|
|
1108
|
+
return@post
|
|
1109
|
+
}
|
|
898
1110
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
899
1111
|
restoredRecyclerKeys[recycler] != key ||
|
|
900
1112
|
originalRecyclerPadding[recycler] !== original) {
|
|
901
1113
|
if (restoredRecyclerKeys[recycler] == key) restoredRecyclerKeys.remove(recycler)
|
|
1114
|
+
clearPageRestoreForView(key, recycler, restoreRequest)
|
|
902
1115
|
return@post
|
|
903
1116
|
}
|
|
904
|
-
val saved = pageOffsets[key] ?: 0
|
|
1117
|
+
val saved = transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
905
1118
|
val current = recycler.computeVerticalScrollOffset()
|
|
1119
|
+
offsetDiag("recycler-restore-before-saved=$saved", recycler, key)
|
|
1120
|
+
if (nativeSmoothHeaderScrollEnabled && (recycler.height <= 0 || recycler.childCount == 0)) {
|
|
1121
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1122
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1123
|
+
return@post
|
|
1124
|
+
}
|
|
906
1125
|
if (saved != current) recycler.scrollBy(0, saved - current)
|
|
1126
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, recycler, restoreRequest)
|
|
1127
|
+
else pendingPageRestores.remove(key)
|
|
1128
|
+
offsetDiag("recycler-restore-after", recycler, key)
|
|
907
1129
|
}
|
|
908
1130
|
}
|
|
909
1131
|
}
|
|
@@ -949,16 +1171,32 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
949
1171
|
val key = pageKey(pageIndex)
|
|
950
1172
|
if (restoredScrollerKeys[scroller] != key) {
|
|
951
1173
|
restoredScrollerKeys[scroller] = key
|
|
1174
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
952
1175
|
val generation = attachmentGeneration
|
|
953
1176
|
scroller.post {
|
|
1177
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1178
|
+
offsetDiag("post-skip-cancelled-page=$pageIndex", scroller, key)
|
|
1179
|
+
return@post
|
|
1180
|
+
}
|
|
954
1181
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
955
1182
|
restoredScrollerKeys[scroller] != key ||
|
|
956
1183
|
originalScrollerPadding[scroller] !== original) {
|
|
957
1184
|
if (restoredScrollerKeys[scroller] == key) restoredScrollerKeys.remove(scroller)
|
|
1185
|
+
clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1186
|
+
return@post
|
|
1187
|
+
}
|
|
1188
|
+
val saved = transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
1189
|
+
offsetDiag("scrollview-restore-before-saved=$saved", scroller, key)
|
|
1190
|
+
if (nativeSmoothHeaderScrollEnabled &&
|
|
1191
|
+
(scroller.height <= 0 || (scroller.getChildAt(0)?.height ?: 0) <= 0)) {
|
|
1192
|
+
restoredScrollerKeys.remove(scroller)
|
|
1193
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
958
1194
|
return@post
|
|
959
1195
|
}
|
|
960
|
-
val saved = pageOffsets[key] ?: 0
|
|
961
1196
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1197
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1198
|
+
else pendingPageRestores.remove(key)
|
|
1199
|
+
offsetDiag("scrollview-restore-after", scroller, key)
|
|
962
1200
|
}
|
|
963
1201
|
}
|
|
964
1202
|
}
|
|
@@ -1017,7 +1255,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1017
1255
|
publishConsumedOffset(recycler, remove = true)
|
|
1018
1256
|
val original = originalRecyclerPadding.remove(recycler) ?: return
|
|
1019
1257
|
updateRecyclerPaddingOwnership(recycler, original)
|
|
1020
|
-
restoredRecyclerKeys.remove(recycler)
|
|
1258
|
+
restoredRecyclerKeys.remove(recycler)?.let { clearPageRestoreForView(it, recycler) }
|
|
1021
1259
|
recycler.clipToPadding = original.clipToPadding
|
|
1022
1260
|
recycler.setPadding(original.left, original.top, original.right, original.bottom)
|
|
1023
1261
|
}
|
|
@@ -1025,7 +1263,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1025
1263
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
1026
1264
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
1027
1265
|
updateScrollerPaddingOwnership(scroller, original)
|
|
1028
|
-
restoredScrollerKeys.remove(scroller)
|
|
1266
|
+
restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
|
|
1029
1267
|
if (scroller is ReactScrollView) {
|
|
1030
1268
|
val state = scroller.reactScrollViewScrollState
|
|
1031
1269
|
scroller.setScrollAwayPaddingEnabledUnstable(
|
|
@@ -1399,6 +1637,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1399
1637
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1400
1638
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1401
1639
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1640
|
+
if (type == ViewCompat.TYPE_TOUCH) {
|
|
1641
|
+
pageRestoreRequests.remove(currentPageKey())
|
|
1642
|
+
pendingPageRestores.remove(currentPageKey())
|
|
1643
|
+
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
1644
|
+
}
|
|
1402
1645
|
}
|
|
1403
1646
|
|
|
1404
1647
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
@@ -1492,6 +1735,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1492
1735
|
restoredRecyclerKeys.clear()
|
|
1493
1736
|
restoredScrollerKeys.clear()
|
|
1494
1737
|
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
1738
|
+
resumeTransitionSource?.let { source ->
|
|
1739
|
+
resumeTransitionSource = null
|
|
1740
|
+
if (source in 0 until adapter.itemCount) {
|
|
1741
|
+
pager.setCurrentItem(source, false)
|
|
1742
|
+
selectPage(source)
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1495
1745
|
log(
|
|
1496
1746
|
"host-attach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1497
1747
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1511,6 +1761,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1511
1761
|
nestedNativeGestureStarted = false
|
|
1512
1762
|
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
1513
1763
|
detachPrimaryScrollObserver()
|
|
1764
|
+
pageRestoreRequests.clear()
|
|
1765
|
+
pendingPageRestores.clear()
|
|
1766
|
+
ownedScrollerRestoreRequests.clear()
|
|
1767
|
+
transitionHeaderOffsets?.let { headers ->
|
|
1768
|
+
resumeTransitionSource = transitionSourcePage
|
|
1769
|
+
syntheticPageRestore = true
|
|
1770
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: 0)
|
|
1771
|
+
syntheticPageRestore = false
|
|
1772
|
+
}
|
|
1773
|
+
transitionHeaderOffsets = null
|
|
1774
|
+
transitionPageOffsets = null
|
|
1775
|
+
transitionTargetPage = null
|
|
1514
1776
|
// Snapshot live keys before restoration removes their padding records.
|
|
1515
1777
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
1516
1778
|
restoreRecyclerInsets(recycler)
|
|
@@ -43,6 +43,7 @@ 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)
|
|
46
47
|
dispatch(reactContext, host, PageScrollEvent(host.id, position, offset))
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -55,6 +56,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
override fun onPageScrollStateChanged(state: Int) {
|
|
59
|
+
host.onPagerScrollStateChanged(state)
|
|
58
60
|
val value = when (state) {
|
|
59
61
|
ViewPager2.SCROLL_STATE_IDLE -> "idle"
|
|
60
62
|
ViewPager2.SCROLL_STATE_DRAGGING -> "dragging"
|
|
@@ -324,13 +326,19 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
324
326
|
|
|
325
327
|
override fun setPage(view: CollapsiblePagerHost?, selectedPage: Int) {
|
|
326
328
|
if (view != null && selectedPage in 0 until view.adapter.itemCount) {
|
|
329
|
+
view.beginPageTransition(selectedPage)
|
|
327
330
|
view.pager.setCurrentItem(selectedPage, true)
|
|
331
|
+
if (view.pager.scrollState == ViewPager2.SCROLL_STATE_IDLE) {
|
|
332
|
+
view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
|
|
333
|
+
}
|
|
328
334
|
}
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
override fun setPageWithoutAnimation(view: CollapsiblePagerHost?, selectedPage: Int) {
|
|
332
338
|
if (view != null && selectedPage in 0 until view.adapter.itemCount) {
|
|
339
|
+
view.beginPageTransition(selectedPage)
|
|
333
340
|
view.pager.setCurrentItem(selectedPage, false)
|
|
341
|
+
view.onPagerScrollStateChanged(ViewPager2.SCROLL_STATE_IDLE)
|
|
334
342
|
}
|
|
335
343
|
}
|
|
336
344
|
|
|
@@ -255,6 +255,8 @@ 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
|
+
|
|
258
260
|
internal fun setRefreshOffset(value: Int) {
|
|
259
261
|
refreshOffset = value
|
|
260
262
|
applyRefreshOffset()
|
|
@@ -27,5 +27,16 @@ 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)
|
|
30
41
|
println("Native scroller pending restore: clamp retry, partial growth, settle and cancel passed")
|
|
31
42
|
}
|