@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.
@@ -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 (savedOffset > 0) setHeaderOffset(headerHeightPx)
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 next = offset.coerceIn(0, maximumHeaderOffset())
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
- pageOffsets[currentPageKey()] = 0
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()] = contentOffset
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
- if (recordOffset) pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
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()] = contentOffset
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
- if (scroller != null && recordOffset) pageOffsets[currentPageKey()] = scroller.scrollY
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 && scroller.isAttachedToWindow) {
798
- scroller.host.restoreScrollOffset(pageOffsets[key] ?: 0)
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
- val contentOffset = scroller.scrollY
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
- if (recordOffset) observedOwnedScrollerKey?.let { pageOffsets[it] = scroller.scrollY }
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
  }