@onekeyfe/react-native-pager-view 3.0.157-alpha.257 → 3.0.157-alpha.262
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/README.md +55 -4
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +295 -59
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +15 -11
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/jni/CMakeLists.txt +19 -0
- package/android/src/main/jni/pagerview.h +11 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +1 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +32 -52
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
- package/docs/SPEC.md +282 -34
- package/ios/RNCCollapsiblePagerViewComponentView.mm +187 -1
- package/ios/RNCNativeScrollerComponentView.h +17 -0
- package/ios/RNCNativeScrollerComponentView.mm +479 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +759 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
- package/ios/tests/RNCNativeScrollerTests.mm +252 -0
- package/lib/module/CollapsiblePagerView.js +10 -1
- package/lib/module/NativeScroller.js +171 -11
- package/lib/module/NativeScrollerNativeComponent.ts +101 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
- package/lib/typescript/src/NativeScroller.d.ts +15 -7
- package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
- package/package.json +7 -4
- package/react-native-pager-view.podspec +2 -1
- package/react-native.config.js +15 -0
- package/src/CollapsiblePagerView.tsx +14 -4
- package/src/NativeScroller.tsx +291 -14
- package/src/NativeScrollerNativeComponent.ts +101 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package com.reactnativepagerview
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Matrix
|
|
4
6
|
import android.graphics.Rect
|
|
5
7
|
import android.os.SystemClock
|
|
6
8
|
import android.view.MotionEvent
|
|
@@ -19,11 +21,11 @@ import androidx.core.view.ViewCompat
|
|
|
19
21
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
20
22
|
import androidx.recyclerview.widget.RecyclerView
|
|
21
23
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|
24
|
+
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
22
25
|
import androidx.viewpager2.widget.ViewPager2
|
|
23
26
|
import com.facebook.react.R as ReactR
|
|
24
27
|
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
25
28
|
import com.facebook.react.views.scroll.ReactScrollView
|
|
26
|
-
import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
|
|
27
29
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
28
30
|
import java.lang.ref.WeakReference
|
|
29
31
|
import java.util.WeakHashMap
|
|
@@ -120,12 +122,24 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
120
122
|
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
121
123
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
122
124
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
123
|
-
private val
|
|
125
|
+
private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
|
|
126
|
+
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
127
|
+
private var refreshForegroundWasVisible = false
|
|
128
|
+
private val refreshForegroundMatrix = Matrix()
|
|
129
|
+
private val refreshForegroundOrigin = FloatArray(2)
|
|
130
|
+
private val refreshForegroundAncestors = ArrayList<View>()
|
|
131
|
+
private var refreshForegroundAncestorAlpha = 1f
|
|
124
132
|
private val pageOffsets = HashMap<String, Int>()
|
|
125
133
|
private var observedRecyclerView: RecyclerView? = null
|
|
126
134
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
135
|
+
private var observedOwnedScroller: NativeScrollerViewport? = null
|
|
136
|
+
private var observedOwnedScrollerKey: String? = null
|
|
137
|
+
private var observedOwnedScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
138
|
+
private var observedOwnedScrollerTreeObserver: ViewTreeObserver? = null
|
|
127
139
|
private var observedNativeScroller: ScrollView? = null
|
|
128
140
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
141
|
+
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
142
|
+
private var observedPageRemoved = false
|
|
129
143
|
private var attachmentGeneration = 0
|
|
130
144
|
private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
|
|
131
145
|
private val hostIdentity = System.identityHashCode(this)
|
|
@@ -139,6 +153,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
139
153
|
private var headerDownEvent: MotionEvent? = null
|
|
140
154
|
private var headerHasHorizontalChild = false
|
|
141
155
|
private var forwardedScrollable: View? = null
|
|
156
|
+
private var pendingRefreshPull: WeakReference<SwipeRefreshLayout>? = null
|
|
142
157
|
private var nativeGestureStarted = false
|
|
143
158
|
private var pressCancelled = false
|
|
144
159
|
private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
|
|
@@ -314,6 +329,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
314
329
|
}
|
|
315
330
|
|
|
316
331
|
fun removeReactChild(child: View) {
|
|
332
|
+
releaseRefreshForeground()
|
|
317
333
|
val index = logicalChildren.indexOf(child)
|
|
318
334
|
if (index < 0) return
|
|
319
335
|
logicalChildren.removeAt(index)
|
|
@@ -327,10 +343,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
327
343
|
stickyHeaderView = null
|
|
328
344
|
}
|
|
329
345
|
else -> {
|
|
346
|
+
val observed = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
347
|
+
if (observed != null && isDescendant(observed, child)) {
|
|
348
|
+
// The page detaches only later; its scroller must not keep writing this key.
|
|
349
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
350
|
+
observedPageRemoved = true
|
|
351
|
+
}
|
|
330
352
|
when (val scrollable =
|
|
331
353
|
findExplicitNativeScrollerInView(child) ?: findVerticalScrollableView(child)) {
|
|
332
354
|
is RecyclerView -> restoreRecyclerInsets(scrollable)
|
|
333
355
|
is ScrollView -> restoreNativeScrollerInsets(scrollable)
|
|
356
|
+
is NativeScrollerViewport -> restoreOwnedScrollerInsets(scrollable)
|
|
334
357
|
}
|
|
335
358
|
adapter.removePage(child)
|
|
336
359
|
log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
@@ -339,6 +362,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
339
362
|
}
|
|
340
363
|
|
|
341
364
|
fun removeAllReactChildren() {
|
|
365
|
+
releaseRefreshForeground()
|
|
342
366
|
detachPrimaryScrollObserver()
|
|
343
367
|
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
344
368
|
restoreRecyclerInsets(recycler)
|
|
@@ -346,6 +370,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
346
370
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
347
371
|
restoreNativeScrollerInsets(scroller)
|
|
348
372
|
}
|
|
373
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
349
374
|
headerView?.let { view -> super.removeView(view) }
|
|
350
375
|
stickyHeaderView?.let { view -> super.removeView(view) }
|
|
351
376
|
headerView = null
|
|
@@ -361,8 +386,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
361
386
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
362
387
|
|
|
363
388
|
fun selectPage(position: Int) {
|
|
389
|
+
pendingRefreshPull = null
|
|
364
390
|
if (adapter.itemCount == 0) return
|
|
365
391
|
detachPrimaryScrollObserver()
|
|
392
|
+
releaseRefreshForeground()
|
|
366
393
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
367
394
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
368
395
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
@@ -396,7 +423,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
396
423
|
fun attachedPageCount(): Int = (pager.getChildAt(0) as? RecyclerView)?.childCount ?: 0
|
|
397
424
|
|
|
398
425
|
fun observedScrollableCount(): Int =
|
|
399
|
-
originalRecyclerPadding.size + originalScrollerPadding.size
|
|
426
|
+
originalRecyclerPadding.size + originalScrollerPadding.size + ownedScrollerKeys.size
|
|
400
427
|
|
|
401
428
|
private fun layoutPagerIfRequested() {
|
|
402
429
|
val recycler = pager.getChildAt(0) as? RecyclerView ?: return
|
|
@@ -493,6 +520,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
493
520
|
|
|
494
521
|
private fun maximumHeaderOffset(): Int {
|
|
495
522
|
val scrollable = observedRecyclerView
|
|
523
|
+
?: observedOwnedScroller
|
|
496
524
|
?: observedNativeScroller
|
|
497
525
|
?: primaryScrollableForPage(selectedPage)
|
|
498
526
|
?: return headerHeightPx
|
|
@@ -514,7 +542,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
514
542
|
applyHeaderOffset()
|
|
515
543
|
}
|
|
516
544
|
|
|
517
|
-
private fun publishConsumedOffset(recycler:
|
|
545
|
+
private fun publishConsumedOffset(recycler: View, remove: Boolean = false) {
|
|
518
546
|
val key = R.id.onekey_native_scroll_coordinator_consumed_offsets
|
|
519
547
|
val existing = recycler.getTag(key) as? NativeScrollCoordinatorContributions
|
|
520
548
|
val contributions = existing ?: if (remove) return else NativeScrollCoordinatorContributions().also {
|
|
@@ -535,6 +563,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
535
563
|
nativeTabBarView.translationY = -offset
|
|
536
564
|
nativeSubHeaderView.translationY = -offset
|
|
537
565
|
observedRecyclerView?.let { publishConsumedOffset(it) }
|
|
566
|
+
// Adjacent owned pages are translated too; unchanged values are not republished.
|
|
567
|
+
for (scroller in ownedScrollerKeys.keys) publishConsumedOffset(scroller)
|
|
538
568
|
}
|
|
539
569
|
|
|
540
570
|
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
@@ -561,6 +591,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
561
591
|
}
|
|
562
592
|
|
|
563
593
|
private fun findExplicitNativeScrollerInView(view: View): View? {
|
|
594
|
+
if (view is NativeScrollerView) return view.viewport
|
|
564
595
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
565
596
|
return view.currentPrimaryScrollableForParent()
|
|
566
597
|
}
|
|
@@ -577,6 +608,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
577
608
|
}
|
|
578
609
|
|
|
579
610
|
private fun findVerticalScrollableView(view: View): View? {
|
|
611
|
+
if (view is NativeScrollerView) return view.viewport
|
|
580
612
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
581
613
|
return view.currentPrimaryScrollableForParent()
|
|
582
614
|
}
|
|
@@ -605,30 +637,28 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
605
637
|
private fun verticalScrollOffset(view: View): Int = when (view) {
|
|
606
638
|
is RecyclerView -> view.computeVerticalScrollOffset()
|
|
607
639
|
is ScrollView -> view.scrollY
|
|
640
|
+
is NativeScrollerViewport -> view.scrollY
|
|
608
641
|
else -> 0
|
|
609
642
|
}
|
|
610
643
|
|
|
611
644
|
private fun prepareAdjacentPages() {
|
|
612
|
-
// Retained pages and replaced controls may leave the attached view tree.
|
|
613
|
-
for ((scroller, indicator) in scrollerRefreshIndicators.toMap()) {
|
|
614
|
-
if (nativeScrollerRefreshIndicator(scroller) !== indicator.get()) {
|
|
615
|
-
restoreNativeScrollerRefreshIndicator(scroller)
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
645
|
val first = max(0, selectedPage - 1)
|
|
619
646
|
val last = min(adapter.itemCount - 1, selectedPage + 1)
|
|
620
647
|
for (index in first..last) {
|
|
621
648
|
when (val scrollable = primaryScrollableForPage(index)) {
|
|
622
649
|
is RecyclerView -> applyRecyclerInsets(scrollable, index)
|
|
623
650
|
is ScrollView -> applyNativeScrollerInsets(scrollable, index)
|
|
651
|
+
is NativeScrollerViewport -> applyOwnedScrollerInsets(scrollable, index)
|
|
624
652
|
}
|
|
625
653
|
}
|
|
654
|
+
updateRefreshForeground()
|
|
626
655
|
}
|
|
627
656
|
|
|
628
657
|
private fun attachPrimaryScrollObserver() {
|
|
629
658
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
630
|
-
val previous = observedRecyclerView ?: observedNativeScroller
|
|
631
|
-
if (previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
659
|
+
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
660
|
+
if (observedPageRemoved || previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
661
|
+
observedPageRemoved = false
|
|
632
662
|
detachPrimaryScrollObserver()
|
|
633
663
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
634
664
|
pageOffsets[currentPageKey()] = 0
|
|
@@ -636,6 +666,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
636
666
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
637
667
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
638
668
|
is ScrollView -> attachNativeScrollerObserver(scrollable)
|
|
669
|
+
is NativeScrollerViewport -> attachOwnedScrollerObserver(scrollable)
|
|
639
670
|
}
|
|
640
671
|
}
|
|
641
672
|
|
|
@@ -669,11 +700,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
669
700
|
)
|
|
670
701
|
}
|
|
671
702
|
|
|
672
|
-
private fun detachRecyclerObserver() {
|
|
703
|
+
private fun detachRecyclerObserver(recordOffset: Boolean) {
|
|
673
704
|
val recycler = observedRecyclerView
|
|
674
705
|
val listener = observedScrollListener
|
|
675
706
|
if (recycler != null) {
|
|
676
|
-
pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
|
|
707
|
+
if (recordOffset) pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
|
|
677
708
|
publishConsumedOffset(recycler, remove = true)
|
|
678
709
|
}
|
|
679
710
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
@@ -696,27 +727,35 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
696
727
|
detachPrimaryScrollObserver()
|
|
697
728
|
observedNativeScroller = scroller
|
|
698
729
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
699
|
-
val listener = ViewTreeObserver.OnScrollChangedListener {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
730
|
+
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
731
|
+
override fun onScrollChanged() {
|
|
732
|
+
if (observedNativeScrollerListener !== this) {
|
|
733
|
+
removeScrollChangedListener(null, scroller, this)
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
val contentOffset = scroller.scrollY
|
|
737
|
+
pageOffsets[currentPageKey()] = contentOffset
|
|
738
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
739
|
+
setHeaderOffset(headerHeightPx)
|
|
740
|
+
}
|
|
704
741
|
}
|
|
705
742
|
}
|
|
706
743
|
observedNativeScrollerListener = listener
|
|
707
|
-
scroller.viewTreeObserver.
|
|
744
|
+
observedNativeScrollerTreeObserver = scroller.viewTreeObserver.also {
|
|
745
|
+
it.addOnScrollChangedListener(listener)
|
|
746
|
+
}
|
|
708
747
|
log(
|
|
709
748
|
"scroller-observer-attach inner=$selectedPage key=${currentPageKey()} " +
|
|
710
749
|
"scroller=${System.identityHashCode(scroller)}",
|
|
711
750
|
)
|
|
712
751
|
}
|
|
713
752
|
|
|
714
|
-
private fun detachNativeScrollerObserver() {
|
|
753
|
+
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
715
754
|
val scroller = observedNativeScroller
|
|
716
755
|
val listener = observedNativeScrollerListener
|
|
717
|
-
if (scroller != null) pageOffsets[currentPageKey()] = scroller.scrollY
|
|
718
|
-
if (scroller != null && listener != null
|
|
719
|
-
scroller
|
|
756
|
+
if (scroller != null && recordOffset) pageOffsets[currentPageKey()] = scroller.scrollY
|
|
757
|
+
if (scroller != null && listener != null) {
|
|
758
|
+
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
720
759
|
}
|
|
721
760
|
if (scroller != null) {
|
|
722
761
|
log(
|
|
@@ -726,11 +765,87 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
726
765
|
}
|
|
727
766
|
observedNativeScroller = null
|
|
728
767
|
observedNativeScrollerListener = null
|
|
768
|
+
observedNativeScrollerTreeObserver = null
|
|
729
769
|
}
|
|
730
770
|
|
|
731
|
-
private fun detachPrimaryScrollObserver() {
|
|
732
|
-
|
|
733
|
-
|
|
771
|
+
private fun detachPrimaryScrollObserver(recordOffset: Boolean = true) {
|
|
772
|
+
detachOwnedScrollerObserver(recordOffset)
|
|
773
|
+
detachRecyclerObserver(recordOffset)
|
|
774
|
+
detachNativeScrollerObserver(recordOffset)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private fun removeScrollChangedListener(
|
|
778
|
+
registered: ViewTreeObserver?,
|
|
779
|
+
scroller: View,
|
|
780
|
+
listener: ViewTreeObserver.OnScrollChangedListener,
|
|
781
|
+
) {
|
|
782
|
+
// A detached scroller returns a new floating observer, while the listener is
|
|
783
|
+
// still registered on the window observer. Try every instance that can own it.
|
|
784
|
+
for (observer in arrayOf(registered, scroller.viewTreeObserver, viewTreeObserver)) {
|
|
785
|
+
if (observer != null && observer.isAlive) observer.removeOnScrollChangedListener(listener)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private fun applyOwnedScrollerInsets(scroller: NativeScrollerViewport, pageIndex: Int) {
|
|
790
|
+
scroller.host.setPagerInset(this, headerHeightPx + stickyHeaderHeightPx)
|
|
791
|
+
publishConsumedOffset(scroller)
|
|
792
|
+
val key = pageKey(pageIndex)
|
|
793
|
+
if (ownedScrollerKeys[scroller] == key) return
|
|
794
|
+
ownedScrollerKeys[scroller] = key
|
|
795
|
+
postForCurrentAttachment {
|
|
796
|
+
if (ownedScrollerKeys[scroller] == key && scroller.isAttachedToWindow) {
|
|
797
|
+
scroller.host.restoreScrollOffset(pageOffsets[key] ?: 0)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
803
|
+
scroller.stopMotion()
|
|
804
|
+
scroller.host.cancelPendingScrollRestore()
|
|
805
|
+
ownedScrollerKeys.remove(scroller)
|
|
806
|
+
publishConsumedOffset(scroller, remove = true)
|
|
807
|
+
scroller.host.setPagerInset(this, null)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
811
|
+
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
812
|
+
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
detachPrimaryScrollObserver()
|
|
816
|
+
observedOwnedScroller = scroller
|
|
817
|
+
observedOwnedScrollerKey = currentPageKey()
|
|
818
|
+
val key = currentPageKey()
|
|
819
|
+
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
820
|
+
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
821
|
+
override fun onScrollChanged() {
|
|
822
|
+
if (observedOwnedScrollerListener !== this) {
|
|
823
|
+
removeScrollChangedListener(null, scroller, this)
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
val contentOffset = scroller.scrollY
|
|
827
|
+
pageOffsets[key] = contentOffset
|
|
828
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
observedOwnedScrollerListener = listener
|
|
832
|
+
observedOwnedScrollerTreeObserver = scroller.viewTreeObserver.also {
|
|
833
|
+
it.addOnScrollChangedListener(listener)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
838
|
+
val scroller = observedOwnedScroller ?: return
|
|
839
|
+
scroller.stopMotion()
|
|
840
|
+
publishConsumedOffset(scroller, remove = true)
|
|
841
|
+
if (recordOffset) observedOwnedScrollerKey?.let { pageOffsets[it] = scroller.scrollY }
|
|
842
|
+
observedOwnedScrollerListener?.let {
|
|
843
|
+
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
844
|
+
}
|
|
845
|
+
observedOwnedScroller = null
|
|
846
|
+
observedOwnedScrollerKey = null
|
|
847
|
+
observedOwnedScrollerListener = null
|
|
848
|
+
observedOwnedScrollerTreeObserver = null
|
|
734
849
|
}
|
|
735
850
|
|
|
736
851
|
private fun applyRecyclerInsets(recycler: RecyclerView, pageIndex: Int) {
|
|
@@ -805,7 +920,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
805
920
|
updateScrollerPaddingOwnership(scroller, original)
|
|
806
921
|
val topInset = headerHeightPx + stickyHeaderHeightPx
|
|
807
922
|
scroller.clipToPadding = false
|
|
808
|
-
applyNativeScrollerRefreshIndicator(scroller, topInset)
|
|
809
923
|
if (scroller is ReactScrollView) {
|
|
810
924
|
// Fabric owns the content child's layout, so ScrollView padding alone
|
|
811
925
|
// cannot move it. RN's native ScrollAway API also updates Fabric culling.
|
|
@@ -908,7 +1022,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
908
1022
|
}
|
|
909
1023
|
|
|
910
1024
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
911
|
-
restoreNativeScrollerRefreshIndicator(scroller)
|
|
912
1025
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
913
1026
|
updateScrollerPaddingOwnership(scroller, original)
|
|
914
1027
|
restoredScrollerKeys.remove(scroller)
|
|
@@ -923,12 +1036,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
923
1036
|
scroller.setPadding(original.left, original.top, original.right, original.bottom)
|
|
924
1037
|
}
|
|
925
1038
|
|
|
926
|
-
private fun
|
|
927
|
-
if (!
|
|
928
|
-
var ancestor =
|
|
929
|
-
while (ancestor != null && ancestor
|
|
930
|
-
|
|
931
|
-
|
|
1039
|
+
private fun refreshLayoutFor(scrollable: View): SwipeRefreshLayout? {
|
|
1040
|
+
if (scrollable !is ScrollView && scrollable !is RecyclerView && scrollable !is NativeScrollerViewport) return null
|
|
1041
|
+
var ancestor = scrollable.parent
|
|
1042
|
+
while (ancestor != null && ancestor !is SwipeRefreshLayout) ancestor = ancestor.parent
|
|
1043
|
+
return ancestor as? SwipeRefreshLayout
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private fun refreshIndicatorFor(refresh: SwipeRefreshLayout): ImageView? {
|
|
932
1047
|
for (index in 0 until refresh.childCount) {
|
|
933
1048
|
val child = refresh.getChildAt(index)
|
|
934
1049
|
if (child is ImageView && child.drawable is CircularProgressDrawable) return child
|
|
@@ -936,29 +1051,119 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
936
1051
|
return null
|
|
937
1052
|
}
|
|
938
1053
|
|
|
939
|
-
private fun
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1054
|
+
private fun refreshForegroundOwner(scrollable: View): CollapsiblePagerHost? {
|
|
1055
|
+
var owner: CollapsiblePagerHost? = null
|
|
1056
|
+
var ancestor = scrollable.parent
|
|
1057
|
+
while (ancestor != null) {
|
|
1058
|
+
if (ancestor is CollapsiblePagerHost && ancestor.nativeSmoothHeaderScrollEnabled &&
|
|
1059
|
+
ancestor.isAttachedToWindow && ancestor.isShown &&
|
|
1060
|
+
ancestor.primaryScrollableForPage(ancestor.selectedPage) === scrollable) {
|
|
1061
|
+
owner = ancestor
|
|
1062
|
+
}
|
|
1063
|
+
ancestor = ancestor.parent
|
|
1064
|
+
}
|
|
1065
|
+
return owner
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private fun updateRefreshForeground() {
|
|
1069
|
+
val scrollable = primaryScrollableForPage(selectedPage)
|
|
1070
|
+
val refresh = scrollable?.takeIf { refreshForegroundOwner(it) === this }?.let(::refreshLayoutFor)
|
|
1071
|
+
val indicator = refresh?.let(::refreshIndicatorFor)
|
|
1072
|
+
if (foregroundRefreshIndicator?.get() !== indicator) releaseRefreshForeground()
|
|
1073
|
+
if (indicator != null) {
|
|
1074
|
+
if (foregroundRefreshIndicator?.get() !== indicator) {
|
|
1075
|
+
foregroundRefreshIndicator = WeakReference(indicator)
|
|
1076
|
+
}
|
|
1077
|
+
val state = refreshIndicatorAlphas.getOrPut(indicator) {
|
|
1078
|
+
NativeRefreshForegroundAlpha(indicator.alpha)
|
|
1079
|
+
}
|
|
1080
|
+
val alpha = state.update(indicator.alpha, this, acquire = true)
|
|
1081
|
+
if (indicator.alpha != alpha) indicator.alpha = alpha
|
|
943
1082
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1083
|
+
val visible = indicator?.isShown == true &&
|
|
1084
|
+
(refreshIndicatorAlphas[indicator]?.drawingAlpha ?: 0f) > 0f
|
|
1085
|
+
// The foreground lies outside the original child's invalidation rectangle.
|
|
1086
|
+
// Repaint once on disappearance as well as throughout the native animation.
|
|
1087
|
+
if (visible || refreshForegroundWasVisible) postInvalidateOnAnimation()
|
|
1088
|
+
refreshForegroundWasVisible = visible
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private fun releaseRefreshForeground() {
|
|
1092
|
+
val indicator = foregroundRefreshIndicator?.get()
|
|
1093
|
+
foregroundRefreshIndicator = null
|
|
1094
|
+
if (indicator != null) {
|
|
1095
|
+
refreshIndicatorAlphas[indicator]?.let { state ->
|
|
1096
|
+
indicator.alpha = state.update(indicator.alpha, this, acquire = false)
|
|
1097
|
+
if (state.isEmpty) refreshIndicatorAlphas.remove(indicator)
|
|
1098
|
+
}
|
|
947
1099
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1100
|
+
if (indicator != null || refreshForegroundWasVisible) invalidate()
|
|
1101
|
+
refreshForegroundWasVisible = false
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private fun refreshMatrixInHost(refresh: View): Boolean {
|
|
1105
|
+
refreshForegroundMatrix.reset()
|
|
1106
|
+
refreshForegroundAncestorAlpha = 1f
|
|
1107
|
+
refreshForegroundAncestors.clear()
|
|
1108
|
+
try {
|
|
1109
|
+
var current: View? = refresh
|
|
1110
|
+
while (current != null && current !== this) {
|
|
1111
|
+
refreshForegroundAncestors.add(current)
|
|
1112
|
+
current = current.parent as? View
|
|
1113
|
+
}
|
|
1114
|
+
if (current !== this) return false
|
|
1115
|
+
for (index in refreshForegroundAncestors.lastIndex downTo 0) {
|
|
1116
|
+
val child = refreshForegroundAncestors[index]
|
|
1117
|
+
val parent = child.parent as? View ?: return false
|
|
1118
|
+
refreshForegroundMatrix.preTranslate(
|
|
1119
|
+
(child.left - parent.scrollX).toFloat(), (child.top - parent.scrollY).toFloat(),
|
|
1120
|
+
)
|
|
1121
|
+
refreshForegroundMatrix.preConcat(child.matrix)
|
|
1122
|
+
refreshForegroundAncestorAlpha *= child.alpha
|
|
1123
|
+
}
|
|
1124
|
+
return true
|
|
1125
|
+
} finally {
|
|
1126
|
+
refreshForegroundAncestors.clear()
|
|
950
1127
|
}
|
|
951
|
-
// Moving only the drawing preserves RN's offsets and AndroidX's active pull/refresh animation.
|
|
952
|
-
// setProgressViewOffset would reset that animation and clear isRefreshing.
|
|
953
|
-
val next = translation.update(indicator.translationY, this, inset.toFloat())
|
|
954
|
-
if (indicator.translationY != next) indicator.translationY = next
|
|
955
1128
|
}
|
|
956
1129
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
indicator
|
|
961
|
-
|
|
1130
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
1131
|
+
// The original parent advances native refresh animations and their listeners.
|
|
1132
|
+
super.dispatchDraw(canvas)
|
|
1133
|
+
val indicator = foregroundRefreshIndicator?.get() ?: return
|
|
1134
|
+
val scrollable = primaryScrollableForPage(selectedPage) ?: return
|
|
1135
|
+
if (refreshForegroundOwner(scrollable) !== this) return
|
|
1136
|
+
val refresh = refreshLayoutFor(scrollable) ?: return
|
|
1137
|
+
if (refreshIndicatorFor(refresh) !== indicator || !indicator.isShown ||
|
|
1138
|
+
indicator.width <= 0 || indicator.height <= 0 || !refreshMatrixInHost(refresh)) return
|
|
1139
|
+
val alpha = (refreshIndicatorAlphas[indicator]?.drawingAlpha ?: return) *
|
|
1140
|
+
refreshForegroundAncestorAlpha
|
|
1141
|
+
if (alpha <= 0f) return
|
|
1142
|
+
refreshForegroundOrigin[0] = 0f
|
|
1143
|
+
refreshForegroundOrigin[1] = 0f
|
|
1144
|
+
refreshForegroundMatrix.mapPoints(refreshForegroundOrigin)
|
|
1145
|
+
val appliedInset = (refresh.getTag(R.id.onekey_native_scroll_coordinator_refresh_inset) as? Number)
|
|
1146
|
+
?.toFloat() ?: 0f
|
|
1147
|
+
val save = canvas.save()
|
|
1148
|
+
try {
|
|
1149
|
+
// Pin the refresh container's origin to this Host top while retaining its
|
|
1150
|
+
// horizontal page position, native scale/pivot, and caller translation.
|
|
1151
|
+
canvas.translate(0f, -refreshForegroundOrigin[1])
|
|
1152
|
+
canvas.concat(refreshForegroundMatrix)
|
|
1153
|
+
canvas.translate(indicator.left.toFloat(), nativeRefreshForegroundTop(indicator.top, appliedInset))
|
|
1154
|
+
canvas.concat(indicator.matrix)
|
|
1155
|
+
if (alpha < 1f) {
|
|
1156
|
+
canvas.saveLayerAlpha(
|
|
1157
|
+
0f, 0f, indicator.width.toFloat(), indicator.height.toFloat(),
|
|
1158
|
+
(alpha * 255f).roundToInt().coerceIn(0, 255),
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
// Direct draw deliberately bypasses the alpha used to hide the original
|
|
1162
|
+
// RenderNode. Do not advance Animation.getTransformation a second time.
|
|
1163
|
+
indicator.draw(canvas)
|
|
1164
|
+
} finally {
|
|
1165
|
+
canvas.restoreToCount(save)
|
|
1166
|
+
}
|
|
962
1167
|
}
|
|
963
1168
|
|
|
964
1169
|
private fun headerRegionAt(y: Float): String? {
|
|
@@ -1185,10 +1390,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1185
1390
|
axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 && isCurrentPageTarget(target)
|
|
1186
1391
|
|
|
1187
1392
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1393
|
+
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1188
1394
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1189
1395
|
}
|
|
1190
1396
|
|
|
1191
1397
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
1398
|
+
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
1192
1399
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
1193
1400
|
onHeaderOffsetChanged?.invoke()
|
|
1194
1401
|
}
|
|
@@ -1203,10 +1410,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1203
1410
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
1204
1411
|
setHeaderOffset(headerOffsetPx + used)
|
|
1205
1412
|
consumed[1] += used
|
|
1206
|
-
} else if (dy < 0 && headerOffsetPx > 0
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1413
|
+
} else if (dy < 0 && headerOffsetPx > 0) {
|
|
1414
|
+
// Refresh wrappers forward nested scrolls but do not scroll themselves.
|
|
1415
|
+
val canScrollUp = if (target is SwipeRefreshLayout) {
|
|
1416
|
+
target.canChildScrollUp()
|
|
1417
|
+
} else {
|
|
1418
|
+
target.canScrollVertically(-1)
|
|
1419
|
+
}
|
|
1420
|
+
if (!canScrollUp) {
|
|
1421
|
+
val used = max(dy, -headerOffsetPx)
|
|
1422
|
+
setHeaderOffset(headerOffsetPx + used)
|
|
1423
|
+
consumed[1] += used
|
|
1424
|
+
}
|
|
1210
1425
|
}
|
|
1211
1426
|
}
|
|
1212
1427
|
|
|
@@ -1219,10 +1434,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1219
1434
|
type: Int,
|
|
1220
1435
|
consumed: IntArray,
|
|
1221
1436
|
) {
|
|
1222
|
-
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0
|
|
1437
|
+
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0) return
|
|
1223
1438
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
1224
1439
|
setHeaderOffset(headerOffsetPx + used)
|
|
1225
1440
|
consumed[1] += used
|
|
1441
|
+
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
1442
|
+
target is SwipeRefreshLayout && target.isEnabled) {
|
|
1443
|
+
pendingRefreshPull = WeakReference(target)
|
|
1444
|
+
}
|
|
1226
1445
|
}
|
|
1227
1446
|
|
|
1228
1447
|
override fun onNestedScroll(
|
|
@@ -1244,6 +1463,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1244
1463
|
)
|
|
1245
1464
|
}
|
|
1246
1465
|
|
|
1466
|
+
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
|
|
1467
|
+
val refresh = pendingRefreshPull?.get()
|
|
1468
|
+
val scrollable = primaryScrollableForPage(selectedPage)
|
|
1469
|
+
if (nativeSmoothHeaderScrollEnabled && refresh != null && refresh === target && isCurrentPageTarget(target) &&
|
|
1470
|
+
headerOffsetPx == 0 && scrollable != null && !scrollable.canScrollVertically(-1) &&
|
|
1471
|
+
refresh.isEnabled && !refresh.isRefreshing &&
|
|
1472
|
+
refreshIndicatorFor(refresh)?.visibility == View.VISIBLE) {
|
|
1473
|
+
// A small release reversal must settle the current refresh pull, not fling
|
|
1474
|
+
// content upward and make its first positive offset collapse the header.
|
|
1475
|
+
return true
|
|
1476
|
+
}
|
|
1477
|
+
return super.onNestedPreFling(target, velocityX, velocityY)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1247
1480
|
override fun getNestedScrollAxes() = nestedScrollingParentHelper.nestedScrollAxes
|
|
1248
1481
|
|
|
1249
1482
|
override fun onAttachedToWindow() {
|
|
@@ -1259,6 +1492,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1259
1492
|
}
|
|
1260
1493
|
|
|
1261
1494
|
override fun onDetachedFromWindow() {
|
|
1495
|
+
pendingRefreshPull = null
|
|
1496
|
+
releaseRefreshForeground()
|
|
1262
1497
|
attachmentGeneration += 1
|
|
1263
1498
|
headerDownEvent?.recycle()
|
|
1264
1499
|
headerDownEvent = null
|
|
@@ -1275,6 +1510,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1275
1510
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
1276
1511
|
restoreNativeScrollerInsets(scroller)
|
|
1277
1512
|
}
|
|
1513
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
1278
1514
|
log(
|
|
1279
1515
|
"host-detach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1280
1516
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1283,7 +1519,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1283
1519
|
}
|
|
1284
1520
|
|
|
1285
1521
|
companion object {
|
|
1286
|
-
private val
|
|
1522
|
+
private val refreshIndicatorAlphas = WeakHashMap<ImageView, NativeRefreshForegroundAlpha>()
|
|
1287
1523
|
const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
|
|
1288
1524
|
const val PAGE_SLOT_OFFSET = 2
|
|
1289
1525
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A pager-owned offset restoration that NestedScrollView clamped because the
|
|
5
|
+
* React content height had not reached native yet. It is reapplied on content
|
|
6
|
+
* layout until reached or cancelled by user/command/content replacement.
|
|
7
|
+
*/
|
|
8
|
+
internal class NativeScrollerPendingRestore {
|
|
9
|
+
var target: Int? = null
|
|
10
|
+
private set
|
|
11
|
+
|
|
12
|
+
fun request(target: Int, reached: Int) {
|
|
13
|
+
this.target = if (reached == target) null else target
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The offset to reapply after a content layout pass. */
|
|
17
|
+
fun anchor(current: Int): Int = target ?: current
|
|
18
|
+
|
|
19
|
+
fun settle(reached: Int) {
|
|
20
|
+
if (target == reached) target = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun cancel() {
|
|
24
|
+
target = null
|
|
25
|
+
}
|
|
26
|
+
}
|