@onekeyfe/react-native-pager-view 3.0.157-alpha.256 → 3.0.157-alpha.258
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 +51 -4
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +254 -8
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +24 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +573 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +130 -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/NativeScrollerScrollAwayInsetsTest.kt +38 -0
- 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 +268 -1
- package/ios/RNCCollapsiblePagerViewComponentView.mm +187 -1
- package/ios/RNCNativeScrollerComponentView.h +17 -0
- package/ios/RNCNativeScrollerComponentView.mm +479 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +346 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
- package/ios/tests/RNCNativeScrollerTests.mm +252 -0
- package/lib/module/CollapsiblePagerView.js +1 -1
- package/lib/module/NativeScroller.js +171 -11
- package/lib/module/NativeScrollerNativeComponent.ts +101 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +2 -3
- package/lib/typescript/src/NativeScroller.d.ts +15 -7
- package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
- package/package.json +6 -3
- package/react-native-pager-view.podspec +2 -1
- package/react-native.config.js +15 -0
- package/src/CollapsiblePagerView.tsx +4 -4
- package/src/NativeScroller.tsx +291 -14
- package/src/NativeScrollerNativeComponent.ts +101 -0
package/README.md
CHANGED
|
@@ -68,12 +68,59 @@ Web scroll child can opt in by exposing a DOM element with
|
|
|
68
68
|
movement, so React is not updated on each vertical scroll frame.
|
|
69
69
|
|
|
70
70
|
On iOS and Android, `nativeTabBar` renders the tab bar natively and
|
|
71
|
-
`onNativeTabPress` reports presses.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
`onNativeTabPress` reports presses. By default, adjacent tab presses animate and
|
|
72
|
+
non-adjacent presses jump directly to the target without passing intermediate
|
|
73
|
+
pages. Set `nativeTabPressAnimationEnabled={true}` to always animate, or `false`
|
|
74
|
+
to always jump. Swipe gestures and imperative page commands are unchanged.
|
|
75
75
|
|
|
76
76
|
Thank you again to Callstack and everyone who contributes to
|
|
77
77
|
`react-native-pager-view` 💙
|
|
78
78
|
|
|
79
79
|
The native ancestor-scroll contribution protocol is specified in [docs/SPEC.md](docs/SPEC.md).
|
|
80
|
+
|
|
81
|
+
## NativeScroller compatibility
|
|
82
|
+
|
|
83
|
+
The independent iOS/Android `NativeScroller` has passing native builds and
|
|
84
|
+
focused simulator checks. The complete acceptance matrix remains open; see
|
|
85
|
+
[docs/SPEC.md](docs/SPEC.md) for the verified cases and limits. It is a
|
|
86
|
+
non-virtualized vertical scroller with one internal React content View. Fabric/Yoga lays out React children and content padding; size
|
|
87
|
+
changes are sent to native through layout props, not a JavaScript scroll loop.
|
|
88
|
+
Shared Fabric native state tracks local scroll displacement and ancestor header
|
|
89
|
+
travel separately, without per-frame JavaScript coordination. Descendant hit
|
|
90
|
+
testing and `measureInWindow` use that state; device acceptance must compare the
|
|
91
|
+
reported coordinates with visible React content after scrolling and page return.
|
|
92
|
+
The automatic flexGrow minimum uses a native-reported usable content height,
|
|
93
|
+
with static pager reservations accounted for once. This internal size event is
|
|
94
|
+
not public API and does not run on scroll frames. Before it arrives the automatic
|
|
95
|
+
minimum is zero; a larger explicit numeric minHeight remains authoritative.
|
|
96
|
+
Refresh insets and content-dependent bottom fill do not change this minimum.
|
|
97
|
+
Web continues using its existing ScrollView implementation.
|
|
98
|
+
|
|
99
|
+
The supported ScrollView subset is `contentContainerStyle` (including padding,
|
|
100
|
+
height, numeric minHeight and flexGrow), `onContentSizeChange`, `scrollEnabled`,
|
|
101
|
+
`nestedScrollEnabled`, indicators, `bounces`, `alwaysBounceVertical`,
|
|
102
|
+
`contentInset`, keyboard dismissal/persist-tap modes, `scrollEventThrottle`, and
|
|
103
|
+
the scroll/drag/momentum callbacks. Standard View props remain available.
|
|
104
|
+
`decelerationRate` applies on iOS; Android uses AndroidX's native fling physics.
|
|
105
|
+
`bounces`/`alwaysBounceVertical` and refresh tint/title are iOS controls; Android
|
|
106
|
+
uses refresh colors/background/size and does not expose a bounce policy here.
|
|
107
|
+
Tap-to-dismiss uses RN 0.86's JavaScript TextInput registry to preserve taps on
|
|
108
|
+
another input and child-handled presses; keyboard behavior requires device checks.
|
|
109
|
+
Horizontal scrolling, sticky rows, snapping, zoom, automatic keyboard insets and
|
|
110
|
+
the full ScrollView responder API are not provided.
|
|
111
|
+
|
|
112
|
+
Pass a standard `<RefreshControl refreshing={value} onRefresh={callback} />` as
|
|
113
|
+
`refreshControl`. Its enabled, tint/colors, background, offset, title/title color
|
|
114
|
+
and size props map to the component's native refresh controller; the RN refresh
|
|
115
|
+
element is not mounted. Refreshing remains controlled even if the callback does
|
|
116
|
+
not change the value. Arbitrary custom refresh components are not supported.
|
|
117
|
+
|
|
118
|
+
`NativeScrollerRef` exposes `scrollTo`, `scrollToEnd`, `flashScrollIndicators`,
|
|
119
|
+
host measurement/focus methods, `getNativeScrollRef` and `getScrollableNode`.
|
|
120
|
+
`pagerScrollKey` remains the stable primary-scroller identity. Native command
|
|
121
|
+
events use the ScrollView event shape; UIKit/AndroidX continue owning gestures
|
|
122
|
+
and momentum. Animated commands whose clamped target differs from the current
|
|
123
|
+
position emit a momentum begin/end pair; a no-op emits no new pair. A new command,
|
|
124
|
+
new touch, content-root replacement or detach ends the previous active sequence.
|
|
125
|
+
The focused JS tests verify wrapper routing, not native scrolling,
|
|
126
|
+
Yoga rendering, keyboard interaction or refresh placement on a device.
|
|
@@ -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
|
|
@@ -9,6 +11,7 @@ import android.view.ViewConfiguration
|
|
|
9
11
|
import android.view.ViewGroup
|
|
10
12
|
import android.view.ViewTreeObserver
|
|
11
13
|
import android.widget.HorizontalScrollView
|
|
14
|
+
import android.widget.ImageView
|
|
12
15
|
import android.widget.ScrollView
|
|
13
16
|
// OneKey patch: FrameLayout is inherited through NestedScrollableHost.
|
|
14
17
|
// import android.widget.FrameLayout
|
|
@@ -17,11 +20,14 @@ import androidx.core.view.NestedScrollingParentHelper
|
|
|
17
20
|
import androidx.core.view.ViewCompat
|
|
18
21
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
19
22
|
import androidx.recyclerview.widget.RecyclerView
|
|
23
|
+
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|
24
|
+
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
20
25
|
import androidx.viewpager2.widget.ViewPager2
|
|
21
26
|
import com.facebook.react.R as ReactR
|
|
22
27
|
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
23
28
|
import com.facebook.react.views.scroll.ReactScrollView
|
|
24
29
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
30
|
+
import java.lang.ref.WeakReference
|
|
25
31
|
import java.util.WeakHashMap
|
|
26
32
|
import kotlin.math.max
|
|
27
33
|
import kotlin.math.min
|
|
@@ -116,9 +122,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
116
122
|
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
117
123
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
118
124
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
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
|
|
119
132
|
private val pageOffsets = HashMap<String, Int>()
|
|
120
133
|
private var observedRecyclerView: RecyclerView? = null
|
|
121
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
|
|
122
138
|
private var observedNativeScroller: ScrollView? = null
|
|
123
139
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
124
140
|
private var attachmentGeneration = 0
|
|
@@ -134,6 +150,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
134
150
|
private var headerDownEvent: MotionEvent? = null
|
|
135
151
|
private var headerHasHorizontalChild = false
|
|
136
152
|
private var forwardedScrollable: View? = null
|
|
153
|
+
private var pendingRefreshPull: WeakReference<SwipeRefreshLayout>? = null
|
|
137
154
|
private var nativeGestureStarted = false
|
|
138
155
|
private var pressCancelled = false
|
|
139
156
|
private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
|
|
@@ -309,6 +326,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
309
326
|
}
|
|
310
327
|
|
|
311
328
|
fun removeReactChild(child: View) {
|
|
329
|
+
releaseRefreshForeground()
|
|
312
330
|
val index = logicalChildren.indexOf(child)
|
|
313
331
|
if (index < 0) return
|
|
314
332
|
logicalChildren.removeAt(index)
|
|
@@ -326,6 +344,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
326
344
|
findExplicitNativeScrollerInView(child) ?: findVerticalScrollableView(child)) {
|
|
327
345
|
is RecyclerView -> restoreRecyclerInsets(scrollable)
|
|
328
346
|
is ScrollView -> restoreNativeScrollerInsets(scrollable)
|
|
347
|
+
is NativeScrollerViewport -> restoreOwnedScrollerInsets(scrollable)
|
|
329
348
|
}
|
|
330
349
|
adapter.removePage(child)
|
|
331
350
|
log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
@@ -334,6 +353,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
334
353
|
}
|
|
335
354
|
|
|
336
355
|
fun removeAllReactChildren() {
|
|
356
|
+
releaseRefreshForeground()
|
|
337
357
|
detachPrimaryScrollObserver()
|
|
338
358
|
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
339
359
|
restoreRecyclerInsets(recycler)
|
|
@@ -341,6 +361,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
341
361
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
342
362
|
restoreNativeScrollerInsets(scroller)
|
|
343
363
|
}
|
|
364
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
344
365
|
headerView?.let { view -> super.removeView(view) }
|
|
345
366
|
stickyHeaderView?.let { view -> super.removeView(view) }
|
|
346
367
|
headerView = null
|
|
@@ -356,8 +377,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
356
377
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
357
378
|
|
|
358
379
|
fun selectPage(position: Int) {
|
|
380
|
+
pendingRefreshPull = null
|
|
359
381
|
if (adapter.itemCount == 0) return
|
|
360
382
|
detachPrimaryScrollObserver()
|
|
383
|
+
releaseRefreshForeground()
|
|
361
384
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
362
385
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
363
386
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
@@ -391,7 +414,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
391
414
|
fun attachedPageCount(): Int = (pager.getChildAt(0) as? RecyclerView)?.childCount ?: 0
|
|
392
415
|
|
|
393
416
|
fun observedScrollableCount(): Int =
|
|
394
|
-
originalRecyclerPadding.size + originalScrollerPadding.size
|
|
417
|
+
originalRecyclerPadding.size + originalScrollerPadding.size + ownedScrollerKeys.size
|
|
395
418
|
|
|
396
419
|
private fun layoutPagerIfRequested() {
|
|
397
420
|
val recycler = pager.getChildAt(0) as? RecyclerView ?: return
|
|
@@ -488,6 +511,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
488
511
|
|
|
489
512
|
private fun maximumHeaderOffset(): Int {
|
|
490
513
|
val scrollable = observedRecyclerView
|
|
514
|
+
?: observedOwnedScroller
|
|
491
515
|
?: observedNativeScroller
|
|
492
516
|
?: primaryScrollableForPage(selectedPage)
|
|
493
517
|
?: return headerHeightPx
|
|
@@ -509,7 +533,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
509
533
|
applyHeaderOffset()
|
|
510
534
|
}
|
|
511
535
|
|
|
512
|
-
private fun publishConsumedOffset(recycler:
|
|
536
|
+
private fun publishConsumedOffset(recycler: View, remove: Boolean = false) {
|
|
513
537
|
val key = R.id.onekey_native_scroll_coordinator_consumed_offsets
|
|
514
538
|
val existing = recycler.getTag(key) as? NativeScrollCoordinatorContributions
|
|
515
539
|
val contributions = existing ?: if (remove) return else NativeScrollCoordinatorContributions().also {
|
|
@@ -530,6 +554,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
530
554
|
nativeTabBarView.translationY = -offset
|
|
531
555
|
nativeSubHeaderView.translationY = -offset
|
|
532
556
|
observedRecyclerView?.let { publishConsumedOffset(it) }
|
|
557
|
+
observedOwnedScroller?.let { publishConsumedOffset(it) }
|
|
533
558
|
}
|
|
534
559
|
|
|
535
560
|
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
@@ -556,6 +581,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
556
581
|
}
|
|
557
582
|
|
|
558
583
|
private fun findExplicitNativeScrollerInView(view: View): View? {
|
|
584
|
+
if (view is NativeScrollerView) return view.viewport
|
|
559
585
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
560
586
|
return view.currentPrimaryScrollableForParent()
|
|
561
587
|
}
|
|
@@ -572,6 +598,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
572
598
|
}
|
|
573
599
|
|
|
574
600
|
private fun findVerticalScrollableView(view: View): View? {
|
|
601
|
+
if (view is NativeScrollerView) return view.viewport
|
|
575
602
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
576
603
|
return view.currentPrimaryScrollableForParent()
|
|
577
604
|
}
|
|
@@ -600,6 +627,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
600
627
|
private fun verticalScrollOffset(view: View): Int = when (view) {
|
|
601
628
|
is RecyclerView -> view.computeVerticalScrollOffset()
|
|
602
629
|
is ScrollView -> view.scrollY
|
|
630
|
+
is NativeScrollerViewport -> view.scrollY
|
|
603
631
|
else -> 0
|
|
604
632
|
}
|
|
605
633
|
|
|
@@ -610,13 +638,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
610
638
|
when (val scrollable = primaryScrollableForPage(index)) {
|
|
611
639
|
is RecyclerView -> applyRecyclerInsets(scrollable, index)
|
|
612
640
|
is ScrollView -> applyNativeScrollerInsets(scrollable, index)
|
|
641
|
+
is NativeScrollerViewport -> applyOwnedScrollerInsets(scrollable, index)
|
|
613
642
|
}
|
|
614
643
|
}
|
|
644
|
+
updateRefreshForeground()
|
|
615
645
|
}
|
|
616
646
|
|
|
617
647
|
private fun attachPrimaryScrollObserver() {
|
|
618
648
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
619
|
-
val previous = observedRecyclerView ?: observedNativeScroller
|
|
649
|
+
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
620
650
|
if (previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
621
651
|
detachPrimaryScrollObserver()
|
|
622
652
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
@@ -625,6 +655,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
625
655
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
626
656
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
627
657
|
is ScrollView -> attachNativeScrollerObserver(scrollable)
|
|
658
|
+
is NativeScrollerViewport -> attachOwnedScrollerObserver(scrollable)
|
|
628
659
|
}
|
|
629
660
|
}
|
|
630
661
|
|
|
@@ -718,10 +749,63 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
718
749
|
}
|
|
719
750
|
|
|
720
751
|
private fun detachPrimaryScrollObserver() {
|
|
752
|
+
detachOwnedScrollerObserver()
|
|
721
753
|
detachRecyclerObserver()
|
|
722
754
|
detachNativeScrollerObserver()
|
|
723
755
|
}
|
|
724
756
|
|
|
757
|
+
private fun applyOwnedScrollerInsets(scroller: NativeScrollerViewport, pageIndex: Int) {
|
|
758
|
+
scroller.host.setPagerInset(this, headerHeightPx + stickyHeaderHeightPx)
|
|
759
|
+
publishConsumedOffset(scroller)
|
|
760
|
+
val key = pageKey(pageIndex)
|
|
761
|
+
if (ownedScrollerKeys[scroller] == key) return
|
|
762
|
+
ownedScrollerKeys[scroller] = key
|
|
763
|
+
postForCurrentAttachment {
|
|
764
|
+
if (ownedScrollerKeys[scroller] == key && scroller.isAttachedToWindow) {
|
|
765
|
+
scroller.scrollTo(0, pageOffsets[key] ?: 0)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
771
|
+
scroller.stopMotion()
|
|
772
|
+
ownedScrollerKeys.remove(scroller)
|
|
773
|
+
publishConsumedOffset(scroller, remove = true)
|
|
774
|
+
scroller.host.setPagerInset(this, null)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
778
|
+
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
779
|
+
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
detachPrimaryScrollObserver()
|
|
783
|
+
observedOwnedScroller = scroller
|
|
784
|
+
observedOwnedScrollerKey = currentPageKey()
|
|
785
|
+
val key = currentPageKey()
|
|
786
|
+
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
787
|
+
val listener = ViewTreeObserver.OnScrollChangedListener {
|
|
788
|
+
val contentOffset = scroller.scrollY
|
|
789
|
+
pageOffsets[key] = contentOffset
|
|
790
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
791
|
+
}
|
|
792
|
+
observedOwnedScrollerListener = listener
|
|
793
|
+
scroller.viewTreeObserver.addOnScrollChangedListener(listener)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private fun detachOwnedScrollerObserver() {
|
|
797
|
+
val scroller = observedOwnedScroller ?: return
|
|
798
|
+
scroller.stopMotion()
|
|
799
|
+
publishConsumedOffset(scroller, remove = true)
|
|
800
|
+
observedOwnedScrollerKey?.let { pageOffsets[it] = scroller.scrollY }
|
|
801
|
+
observedOwnedScrollerListener?.let {
|
|
802
|
+
if (scroller.viewTreeObserver.isAlive) scroller.viewTreeObserver.removeOnScrollChangedListener(it)
|
|
803
|
+
}
|
|
804
|
+
observedOwnedScroller = null
|
|
805
|
+
observedOwnedScrollerKey = null
|
|
806
|
+
observedOwnedScrollerListener = null
|
|
807
|
+
}
|
|
808
|
+
|
|
725
809
|
private fun applyRecyclerInsets(recycler: RecyclerView, pageIndex: Int) {
|
|
726
810
|
val original = originalRecyclerPadding.getOrPut(recycler) {
|
|
727
811
|
RecyclerPadding(
|
|
@@ -910,6 +994,136 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
910
994
|
scroller.setPadding(original.left, original.top, original.right, original.bottom)
|
|
911
995
|
}
|
|
912
996
|
|
|
997
|
+
private fun refreshLayoutFor(scrollable: View): SwipeRefreshLayout? {
|
|
998
|
+
if (scrollable !is ScrollView && scrollable !is RecyclerView && scrollable !is NativeScrollerViewport) return null
|
|
999
|
+
var ancestor = scrollable.parent
|
|
1000
|
+
while (ancestor != null && ancestor !is SwipeRefreshLayout) ancestor = ancestor.parent
|
|
1001
|
+
return ancestor as? SwipeRefreshLayout
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private fun refreshIndicatorFor(refresh: SwipeRefreshLayout): ImageView? {
|
|
1005
|
+
for (index in 0 until refresh.childCount) {
|
|
1006
|
+
val child = refresh.getChildAt(index)
|
|
1007
|
+
if (child is ImageView && child.drawable is CircularProgressDrawable) return child
|
|
1008
|
+
}
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private fun refreshForegroundOwner(scrollable: View): CollapsiblePagerHost? {
|
|
1013
|
+
var owner: CollapsiblePagerHost? = null
|
|
1014
|
+
var ancestor = scrollable.parent
|
|
1015
|
+
while (ancestor != null) {
|
|
1016
|
+
if (ancestor is CollapsiblePagerHost && ancestor.nativeSmoothHeaderScrollEnabled &&
|
|
1017
|
+
ancestor.isAttachedToWindow && ancestor.isShown &&
|
|
1018
|
+
ancestor.primaryScrollableForPage(ancestor.selectedPage) === scrollable) {
|
|
1019
|
+
owner = ancestor
|
|
1020
|
+
}
|
|
1021
|
+
ancestor = ancestor.parent
|
|
1022
|
+
}
|
|
1023
|
+
return owner
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private fun updateRefreshForeground() {
|
|
1027
|
+
val scrollable = primaryScrollableForPage(selectedPage)
|
|
1028
|
+
val refresh = scrollable?.takeIf { refreshForegroundOwner(it) === this }?.let(::refreshLayoutFor)
|
|
1029
|
+
val indicator = refresh?.let(::refreshIndicatorFor)
|
|
1030
|
+
if (foregroundRefreshIndicator?.get() !== indicator) releaseRefreshForeground()
|
|
1031
|
+
if (indicator != null) {
|
|
1032
|
+
if (foregroundRefreshIndicator?.get() !== indicator) {
|
|
1033
|
+
foregroundRefreshIndicator = WeakReference(indicator)
|
|
1034
|
+
}
|
|
1035
|
+
val state = refreshIndicatorAlphas.getOrPut(indicator) {
|
|
1036
|
+
NativeRefreshForegroundAlpha(indicator.alpha)
|
|
1037
|
+
}
|
|
1038
|
+
val alpha = state.update(indicator.alpha, this, acquire = true)
|
|
1039
|
+
if (indicator.alpha != alpha) indicator.alpha = alpha
|
|
1040
|
+
}
|
|
1041
|
+
val visible = indicator?.isShown == true &&
|
|
1042
|
+
(refreshIndicatorAlphas[indicator]?.drawingAlpha ?: 0f) > 0f
|
|
1043
|
+
// The foreground lies outside the original child's invalidation rectangle.
|
|
1044
|
+
// Repaint once on disappearance as well as throughout the native animation.
|
|
1045
|
+
if (visible || refreshForegroundWasVisible) postInvalidateOnAnimation()
|
|
1046
|
+
refreshForegroundWasVisible = visible
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private fun releaseRefreshForeground() {
|
|
1050
|
+
val indicator = foregroundRefreshIndicator?.get()
|
|
1051
|
+
foregroundRefreshIndicator = null
|
|
1052
|
+
if (indicator != null) {
|
|
1053
|
+
refreshIndicatorAlphas[indicator]?.let { state ->
|
|
1054
|
+
indicator.alpha = state.update(indicator.alpha, this, acquire = false)
|
|
1055
|
+
if (state.isEmpty) refreshIndicatorAlphas.remove(indicator)
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (indicator != null || refreshForegroundWasVisible) invalidate()
|
|
1059
|
+
refreshForegroundWasVisible = false
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private fun refreshMatrixInHost(refresh: View): Boolean {
|
|
1063
|
+
refreshForegroundMatrix.reset()
|
|
1064
|
+
refreshForegroundAncestorAlpha = 1f
|
|
1065
|
+
refreshForegroundAncestors.clear()
|
|
1066
|
+
try {
|
|
1067
|
+
var current: View? = refresh
|
|
1068
|
+
while (current != null && current !== this) {
|
|
1069
|
+
refreshForegroundAncestors.add(current)
|
|
1070
|
+
current = current.parent as? View
|
|
1071
|
+
}
|
|
1072
|
+
if (current !== this) return false
|
|
1073
|
+
for (index in refreshForegroundAncestors.lastIndex downTo 0) {
|
|
1074
|
+
val child = refreshForegroundAncestors[index]
|
|
1075
|
+
val parent = child.parent as? View ?: return false
|
|
1076
|
+
refreshForegroundMatrix.preTranslate(
|
|
1077
|
+
(child.left - parent.scrollX).toFloat(), (child.top - parent.scrollY).toFloat(),
|
|
1078
|
+
)
|
|
1079
|
+
refreshForegroundMatrix.preConcat(child.matrix)
|
|
1080
|
+
refreshForegroundAncestorAlpha *= child.alpha
|
|
1081
|
+
}
|
|
1082
|
+
return true
|
|
1083
|
+
} finally {
|
|
1084
|
+
refreshForegroundAncestors.clear()
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
1089
|
+
// The original parent advances native refresh animations and their listeners.
|
|
1090
|
+
super.dispatchDraw(canvas)
|
|
1091
|
+
val indicator = foregroundRefreshIndicator?.get() ?: return
|
|
1092
|
+
val scrollable = primaryScrollableForPage(selectedPage) ?: return
|
|
1093
|
+
if (refreshForegroundOwner(scrollable) !== this) return
|
|
1094
|
+
val refresh = refreshLayoutFor(scrollable) ?: return
|
|
1095
|
+
if (refreshIndicatorFor(refresh) !== indicator || !indicator.isShown ||
|
|
1096
|
+
indicator.width <= 0 || indicator.height <= 0 || !refreshMatrixInHost(refresh)) return
|
|
1097
|
+
val alpha = (refreshIndicatorAlphas[indicator]?.drawingAlpha ?: return) *
|
|
1098
|
+
refreshForegroundAncestorAlpha
|
|
1099
|
+
if (alpha <= 0f) return
|
|
1100
|
+
refreshForegroundOrigin[0] = 0f
|
|
1101
|
+
refreshForegroundOrigin[1] = 0f
|
|
1102
|
+
refreshForegroundMatrix.mapPoints(refreshForegroundOrigin)
|
|
1103
|
+
val appliedInset = (refresh.getTag(R.id.onekey_native_scroll_coordinator_refresh_inset) as? Number)
|
|
1104
|
+
?.toFloat() ?: 0f
|
|
1105
|
+
val save = canvas.save()
|
|
1106
|
+
try {
|
|
1107
|
+
// Pin the refresh container's origin to this Host top while retaining its
|
|
1108
|
+
// horizontal page position, native scale/pivot, and caller translation.
|
|
1109
|
+
canvas.translate(0f, -refreshForegroundOrigin[1])
|
|
1110
|
+
canvas.concat(refreshForegroundMatrix)
|
|
1111
|
+
canvas.translate(indicator.left.toFloat(), nativeRefreshForegroundTop(indicator.top, appliedInset))
|
|
1112
|
+
canvas.concat(indicator.matrix)
|
|
1113
|
+
if (alpha < 1f) {
|
|
1114
|
+
canvas.saveLayerAlpha(
|
|
1115
|
+
0f, 0f, indicator.width.toFloat(), indicator.height.toFloat(),
|
|
1116
|
+
(alpha * 255f).roundToInt().coerceIn(0, 255),
|
|
1117
|
+
)
|
|
1118
|
+
}
|
|
1119
|
+
// Direct draw deliberately bypasses the alpha used to hide the original
|
|
1120
|
+
// RenderNode. Do not advance Animation.getTransformation a second time.
|
|
1121
|
+
indicator.draw(canvas)
|
|
1122
|
+
} finally {
|
|
1123
|
+
canvas.restoreToCount(save)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
913
1127
|
private fun headerRegionAt(y: Float): String? {
|
|
914
1128
|
val headerBottom = headerHeightPx - headerOffsetPx
|
|
915
1129
|
val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
|
|
@@ -1134,10 +1348,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1134
1348
|
axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 && isCurrentPageTarget(target)
|
|
1135
1349
|
|
|
1136
1350
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1351
|
+
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1137
1352
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1138
1353
|
}
|
|
1139
1354
|
|
|
1140
1355
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
1356
|
+
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
1141
1357
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
1142
1358
|
onHeaderOffsetChanged?.invoke()
|
|
1143
1359
|
}
|
|
@@ -1152,10 +1368,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1152
1368
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
1153
1369
|
setHeaderOffset(headerOffsetPx + used)
|
|
1154
1370
|
consumed[1] += used
|
|
1155
|
-
} else if (dy < 0 && headerOffsetPx > 0
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1371
|
+
} else if (dy < 0 && headerOffsetPx > 0) {
|
|
1372
|
+
// Refresh wrappers forward nested scrolls but do not scroll themselves.
|
|
1373
|
+
val canScrollUp = if (target is SwipeRefreshLayout) {
|
|
1374
|
+
target.canChildScrollUp()
|
|
1375
|
+
} else {
|
|
1376
|
+
target.canScrollVertically(-1)
|
|
1377
|
+
}
|
|
1378
|
+
if (!canScrollUp) {
|
|
1379
|
+
val used = max(dy, -headerOffsetPx)
|
|
1380
|
+
setHeaderOffset(headerOffsetPx + used)
|
|
1381
|
+
consumed[1] += used
|
|
1382
|
+
}
|
|
1159
1383
|
}
|
|
1160
1384
|
}
|
|
1161
1385
|
|
|
@@ -1168,10 +1392,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1168
1392
|
type: Int,
|
|
1169
1393
|
consumed: IntArray,
|
|
1170
1394
|
) {
|
|
1171
|
-
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0
|
|
1395
|
+
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0) return
|
|
1172
1396
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
1173
1397
|
setHeaderOffset(headerOffsetPx + used)
|
|
1174
1398
|
consumed[1] += used
|
|
1399
|
+
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
1400
|
+
target is SwipeRefreshLayout && target.isEnabled) {
|
|
1401
|
+
pendingRefreshPull = WeakReference(target)
|
|
1402
|
+
}
|
|
1175
1403
|
}
|
|
1176
1404
|
|
|
1177
1405
|
override fun onNestedScroll(
|
|
@@ -1193,6 +1421,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1193
1421
|
)
|
|
1194
1422
|
}
|
|
1195
1423
|
|
|
1424
|
+
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
|
|
1425
|
+
val refresh = pendingRefreshPull?.get()
|
|
1426
|
+
val scrollable = primaryScrollableForPage(selectedPage)
|
|
1427
|
+
if (nativeSmoothHeaderScrollEnabled && refresh != null && refresh === target && isCurrentPageTarget(target) &&
|
|
1428
|
+
headerOffsetPx == 0 && scrollable != null && !scrollable.canScrollVertically(-1) &&
|
|
1429
|
+
refresh.isEnabled && !refresh.isRefreshing &&
|
|
1430
|
+
refreshIndicatorFor(refresh)?.visibility == View.VISIBLE) {
|
|
1431
|
+
// A small release reversal must settle the current refresh pull, not fling
|
|
1432
|
+
// content upward and make its first positive offset collapse the header.
|
|
1433
|
+
return true
|
|
1434
|
+
}
|
|
1435
|
+
return super.onNestedPreFling(target, velocityX, velocityY)
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1196
1438
|
override fun getNestedScrollAxes() = nestedScrollingParentHelper.nestedScrollAxes
|
|
1197
1439
|
|
|
1198
1440
|
override fun onAttachedToWindow() {
|
|
@@ -1208,6 +1450,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1208
1450
|
}
|
|
1209
1451
|
|
|
1210
1452
|
override fun onDetachedFromWindow() {
|
|
1453
|
+
pendingRefreshPull = null
|
|
1454
|
+
releaseRefreshForeground()
|
|
1211
1455
|
attachmentGeneration += 1
|
|
1212
1456
|
headerDownEvent?.recycle()
|
|
1213
1457
|
headerDownEvent = null
|
|
@@ -1224,6 +1468,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1224
1468
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
1225
1469
|
restoreNativeScrollerInsets(scroller)
|
|
1226
1470
|
}
|
|
1471
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
1227
1472
|
log(
|
|
1228
1473
|
"host-detach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1229
1474
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1232,6 +1477,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1232
1477
|
}
|
|
1233
1478
|
|
|
1234
1479
|
companion object {
|
|
1480
|
+
private val refreshIndicatorAlphas = WeakHashMap<ImageView, NativeRefreshForegroundAlpha>()
|
|
1235
1481
|
const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
|
|
1236
1482
|
const val PAGE_SLOT_OFFSET = 2
|
|
1237
1483
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.reactnativepagerview
|
|
2
2
|
|
|
3
|
+
import java.util.WeakHashMap
|
|
4
|
+
|
|
3
5
|
// Replace only this pager's contribution; another native owner may share the scroller.
|
|
4
6
|
internal fun nativeScrollerScrollAwayTop(currentTop: Int, previousInset: Int, nextInset: Int): Int =
|
|
5
7
|
(currentTop - previousInset).coerceAtLeast(0) + nextInset
|
|
@@ -15,3 +17,25 @@ internal fun nativeScrollerPaddingWasResetByScrollAway(
|
|
|
15
17
|
scrollAwayBottom: Int,
|
|
16
18
|
): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
|
|
17
19
|
bottom == scrollAwayTop + scrollAwayBottom
|
|
20
|
+
|
|
21
|
+
// The refresh controller publishes its actual combined ancestor compensation.
|
|
22
|
+
internal fun nativeRefreshForegroundTop(indicatorTop: Int, appliedInset: Float): Float =
|
|
23
|
+
indicatorTop - appliedInset
|
|
24
|
+
|
|
25
|
+
internal class NativeRefreshForegroundAlpha(initialAlpha: Float) {
|
|
26
|
+
private val owners = WeakHashMap<Any, Unit>()
|
|
27
|
+
private var baseline = initialAlpha
|
|
28
|
+
private var lastApplied = initialAlpha
|
|
29
|
+
|
|
30
|
+
val isEmpty: Boolean
|
|
31
|
+
get() = owners.isEmpty()
|
|
32
|
+
val drawingAlpha: Float
|
|
33
|
+
get() = baseline
|
|
34
|
+
|
|
35
|
+
fun update(currentAlpha: Float, owner: Any, acquire: Boolean): Float {
|
|
36
|
+
if (currentAlpha != lastApplied) baseline = currentAlpha
|
|
37
|
+
if (acquire) owners[owner] = Unit else owners.remove(owner)
|
|
38
|
+
lastApplied = if (owners.isEmpty()) baseline else 0f
|
|
39
|
+
return lastApplied
|
|
40
|
+
}
|
|
41
|
+
}
|