@onekeyfe/react-native-pager-view 3.0.157-alpha.257 → 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 +237 -43
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +15 -11
- 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 +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 +267 -34
- 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
|
|
@@ -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,10 +122,19 @@ 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
|
|
127
138
|
private var observedNativeScroller: ScrollView? = null
|
|
128
139
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
129
140
|
private var attachmentGeneration = 0
|
|
@@ -139,6 +150,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
139
150
|
private var headerDownEvent: MotionEvent? = null
|
|
140
151
|
private var headerHasHorizontalChild = false
|
|
141
152
|
private var forwardedScrollable: View? = null
|
|
153
|
+
private var pendingRefreshPull: WeakReference<SwipeRefreshLayout>? = null
|
|
142
154
|
private var nativeGestureStarted = false
|
|
143
155
|
private var pressCancelled = false
|
|
144
156
|
private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
|
|
@@ -314,6 +326,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
314
326
|
}
|
|
315
327
|
|
|
316
328
|
fun removeReactChild(child: View) {
|
|
329
|
+
releaseRefreshForeground()
|
|
317
330
|
val index = logicalChildren.indexOf(child)
|
|
318
331
|
if (index < 0) return
|
|
319
332
|
logicalChildren.removeAt(index)
|
|
@@ -331,6 +344,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
331
344
|
findExplicitNativeScrollerInView(child) ?: findVerticalScrollableView(child)) {
|
|
332
345
|
is RecyclerView -> restoreRecyclerInsets(scrollable)
|
|
333
346
|
is ScrollView -> restoreNativeScrollerInsets(scrollable)
|
|
347
|
+
is NativeScrollerViewport -> restoreOwnedScrollerInsets(scrollable)
|
|
334
348
|
}
|
|
335
349
|
adapter.removePage(child)
|
|
336
350
|
log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
@@ -339,6 +353,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
339
353
|
}
|
|
340
354
|
|
|
341
355
|
fun removeAllReactChildren() {
|
|
356
|
+
releaseRefreshForeground()
|
|
342
357
|
detachPrimaryScrollObserver()
|
|
343
358
|
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
344
359
|
restoreRecyclerInsets(recycler)
|
|
@@ -346,6 +361,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
346
361
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
347
362
|
restoreNativeScrollerInsets(scroller)
|
|
348
363
|
}
|
|
364
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
349
365
|
headerView?.let { view -> super.removeView(view) }
|
|
350
366
|
stickyHeaderView?.let { view -> super.removeView(view) }
|
|
351
367
|
headerView = null
|
|
@@ -361,8 +377,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
361
377
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
362
378
|
|
|
363
379
|
fun selectPage(position: Int) {
|
|
380
|
+
pendingRefreshPull = null
|
|
364
381
|
if (adapter.itemCount == 0) return
|
|
365
382
|
detachPrimaryScrollObserver()
|
|
383
|
+
releaseRefreshForeground()
|
|
366
384
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
367
385
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
368
386
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
@@ -396,7 +414,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
396
414
|
fun attachedPageCount(): Int = (pager.getChildAt(0) as? RecyclerView)?.childCount ?: 0
|
|
397
415
|
|
|
398
416
|
fun observedScrollableCount(): Int =
|
|
399
|
-
originalRecyclerPadding.size + originalScrollerPadding.size
|
|
417
|
+
originalRecyclerPadding.size + originalScrollerPadding.size + ownedScrollerKeys.size
|
|
400
418
|
|
|
401
419
|
private fun layoutPagerIfRequested() {
|
|
402
420
|
val recycler = pager.getChildAt(0) as? RecyclerView ?: return
|
|
@@ -493,6 +511,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
493
511
|
|
|
494
512
|
private fun maximumHeaderOffset(): Int {
|
|
495
513
|
val scrollable = observedRecyclerView
|
|
514
|
+
?: observedOwnedScroller
|
|
496
515
|
?: observedNativeScroller
|
|
497
516
|
?: primaryScrollableForPage(selectedPage)
|
|
498
517
|
?: return headerHeightPx
|
|
@@ -514,7 +533,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
514
533
|
applyHeaderOffset()
|
|
515
534
|
}
|
|
516
535
|
|
|
517
|
-
private fun publishConsumedOffset(recycler:
|
|
536
|
+
private fun publishConsumedOffset(recycler: View, remove: Boolean = false) {
|
|
518
537
|
val key = R.id.onekey_native_scroll_coordinator_consumed_offsets
|
|
519
538
|
val existing = recycler.getTag(key) as? NativeScrollCoordinatorContributions
|
|
520
539
|
val contributions = existing ?: if (remove) return else NativeScrollCoordinatorContributions().also {
|
|
@@ -535,6 +554,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
535
554
|
nativeTabBarView.translationY = -offset
|
|
536
555
|
nativeSubHeaderView.translationY = -offset
|
|
537
556
|
observedRecyclerView?.let { publishConsumedOffset(it) }
|
|
557
|
+
observedOwnedScroller?.let { publishConsumedOffset(it) }
|
|
538
558
|
}
|
|
539
559
|
|
|
540
560
|
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
@@ -561,6 +581,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
561
581
|
}
|
|
562
582
|
|
|
563
583
|
private fun findExplicitNativeScrollerInView(view: View): View? {
|
|
584
|
+
if (view is NativeScrollerView) return view.viewport
|
|
564
585
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
565
586
|
return view.currentPrimaryScrollableForParent()
|
|
566
587
|
}
|
|
@@ -577,6 +598,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
577
598
|
}
|
|
578
599
|
|
|
579
600
|
private fun findVerticalScrollableView(view: View): View? {
|
|
601
|
+
if (view is NativeScrollerView) return view.viewport
|
|
580
602
|
if (view !== this && view is CollapsiblePagerHost) {
|
|
581
603
|
return view.currentPrimaryScrollableForParent()
|
|
582
604
|
}
|
|
@@ -605,29 +627,26 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
605
627
|
private fun verticalScrollOffset(view: View): Int = when (view) {
|
|
606
628
|
is RecyclerView -> view.computeVerticalScrollOffset()
|
|
607
629
|
is ScrollView -> view.scrollY
|
|
630
|
+
is NativeScrollerViewport -> view.scrollY
|
|
608
631
|
else -> 0
|
|
609
632
|
}
|
|
610
633
|
|
|
611
634
|
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
635
|
val first = max(0, selectedPage - 1)
|
|
619
636
|
val last = min(adapter.itemCount - 1, selectedPage + 1)
|
|
620
637
|
for (index in first..last) {
|
|
621
638
|
when (val scrollable = primaryScrollableForPage(index)) {
|
|
622
639
|
is RecyclerView -> applyRecyclerInsets(scrollable, index)
|
|
623
640
|
is ScrollView -> applyNativeScrollerInsets(scrollable, index)
|
|
641
|
+
is NativeScrollerViewport -> applyOwnedScrollerInsets(scrollable, index)
|
|
624
642
|
}
|
|
625
643
|
}
|
|
644
|
+
updateRefreshForeground()
|
|
626
645
|
}
|
|
627
646
|
|
|
628
647
|
private fun attachPrimaryScrollObserver() {
|
|
629
648
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
630
|
-
val previous = observedRecyclerView ?: observedNativeScroller
|
|
649
|
+
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
631
650
|
if (previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
632
651
|
detachPrimaryScrollObserver()
|
|
633
652
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
@@ -636,6 +655,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
636
655
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
637
656
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
638
657
|
is ScrollView -> attachNativeScrollerObserver(scrollable)
|
|
658
|
+
is NativeScrollerViewport -> attachOwnedScrollerObserver(scrollable)
|
|
639
659
|
}
|
|
640
660
|
}
|
|
641
661
|
|
|
@@ -729,10 +749,63 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
729
749
|
}
|
|
730
750
|
|
|
731
751
|
private fun detachPrimaryScrollObserver() {
|
|
752
|
+
detachOwnedScrollerObserver()
|
|
732
753
|
detachRecyclerObserver()
|
|
733
754
|
detachNativeScrollerObserver()
|
|
734
755
|
}
|
|
735
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
|
+
|
|
736
809
|
private fun applyRecyclerInsets(recycler: RecyclerView, pageIndex: Int) {
|
|
737
810
|
val original = originalRecyclerPadding.getOrPut(recycler) {
|
|
738
811
|
RecyclerPadding(
|
|
@@ -805,7 +878,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
805
878
|
updateScrollerPaddingOwnership(scroller, original)
|
|
806
879
|
val topInset = headerHeightPx + stickyHeaderHeightPx
|
|
807
880
|
scroller.clipToPadding = false
|
|
808
|
-
applyNativeScrollerRefreshIndicator(scroller, topInset)
|
|
809
881
|
if (scroller is ReactScrollView) {
|
|
810
882
|
// Fabric owns the content child's layout, so ScrollView padding alone
|
|
811
883
|
// cannot move it. RN's native ScrollAway API also updates Fabric culling.
|
|
@@ -908,7 +980,6 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
908
980
|
}
|
|
909
981
|
|
|
910
982
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
911
|
-
restoreNativeScrollerRefreshIndicator(scroller)
|
|
912
983
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
913
984
|
updateScrollerPaddingOwnership(scroller, original)
|
|
914
985
|
restoredScrollerKeys.remove(scroller)
|
|
@@ -923,12 +994,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
923
994
|
scroller.setPadding(original.left, original.top, original.right, original.bottom)
|
|
924
995
|
}
|
|
925
996
|
|
|
926
|
-
private fun
|
|
927
|
-
if (!
|
|
928
|
-
var ancestor =
|
|
929
|
-
while (ancestor != null && ancestor
|
|
930
|
-
|
|
931
|
-
|
|
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? {
|
|
932
1005
|
for (index in 0 until refresh.childCount) {
|
|
933
1006
|
val child = refresh.getChildAt(index)
|
|
934
1007
|
if (child is ImageView && child.drawable is CircularProgressDrawable) return child
|
|
@@ -936,29 +1009,119 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
936
1009
|
return null
|
|
937
1010
|
}
|
|
938
1011
|
|
|
939
|
-
private fun
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
943
1040
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
+
}
|
|
947
1057
|
}
|
|
948
|
-
|
|
949
|
-
|
|
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()
|
|
950
1085
|
}
|
|
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
1086
|
}
|
|
956
1087
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
indicator
|
|
961
|
-
|
|
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
|
+
}
|
|
962
1125
|
}
|
|
963
1126
|
|
|
964
1127
|
private fun headerRegionAt(y: Float): String? {
|
|
@@ -1185,10 +1348,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1185
1348
|
axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 && isCurrentPageTarget(target)
|
|
1186
1349
|
|
|
1187
1350
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1351
|
+
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1188
1352
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1189
1353
|
}
|
|
1190
1354
|
|
|
1191
1355
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
1356
|
+
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
1192
1357
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
1193
1358
|
onHeaderOffsetChanged?.invoke()
|
|
1194
1359
|
}
|
|
@@ -1203,10 +1368,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1203
1368
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
1204
1369
|
setHeaderOffset(headerOffsetPx + used)
|
|
1205
1370
|
consumed[1] += used
|
|
1206
|
-
} else if (dy < 0 && headerOffsetPx > 0
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
+
}
|
|
1210
1383
|
}
|
|
1211
1384
|
}
|
|
1212
1385
|
|
|
@@ -1219,10 +1392,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1219
1392
|
type: Int,
|
|
1220
1393
|
consumed: IntArray,
|
|
1221
1394
|
) {
|
|
1222
|
-
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0
|
|
1395
|
+
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0) return
|
|
1223
1396
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
1224
1397
|
setHeaderOffset(headerOffsetPx + used)
|
|
1225
1398
|
consumed[1] += used
|
|
1399
|
+
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
1400
|
+
target is SwipeRefreshLayout && target.isEnabled) {
|
|
1401
|
+
pendingRefreshPull = WeakReference(target)
|
|
1402
|
+
}
|
|
1226
1403
|
}
|
|
1227
1404
|
|
|
1228
1405
|
override fun onNestedScroll(
|
|
@@ -1244,6 +1421,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1244
1421
|
)
|
|
1245
1422
|
}
|
|
1246
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
|
+
|
|
1247
1438
|
override fun getNestedScrollAxes() = nestedScrollingParentHelper.nestedScrollAxes
|
|
1248
1439
|
|
|
1249
1440
|
override fun onAttachedToWindow() {
|
|
@@ -1259,6 +1450,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1259
1450
|
}
|
|
1260
1451
|
|
|
1261
1452
|
override fun onDetachedFromWindow() {
|
|
1453
|
+
pendingRefreshPull = null
|
|
1454
|
+
releaseRefreshForeground()
|
|
1262
1455
|
attachmentGeneration += 1
|
|
1263
1456
|
headerDownEvent?.recycle()
|
|
1264
1457
|
headerDownEvent = null
|
|
@@ -1275,6 +1468,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1275
1468
|
for (scroller in originalScrollerPadding.keys.toList()) {
|
|
1276
1469
|
restoreNativeScrollerInsets(scroller)
|
|
1277
1470
|
}
|
|
1471
|
+
for (scroller in ownedScrollerKeys.keys.toList()) restoreOwnedScrollerInsets(scroller)
|
|
1278
1472
|
log(
|
|
1279
1473
|
"host-detach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1280
1474
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1283,7 +1477,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1283
1477
|
}
|
|
1284
1478
|
|
|
1285
1479
|
companion object {
|
|
1286
|
-
private val
|
|
1480
|
+
private val refreshIndicatorAlphas = WeakHashMap<ImageView, NativeRefreshForegroundAlpha>()
|
|
1287
1481
|
const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
|
|
1288
1482
|
const val PAGE_SLOT_OFFSET = 2
|
|
1289
1483
|
}
|
|
@@ -18,20 +18,24 @@ internal fun nativeScrollerPaddingWasResetByScrollAway(
|
|
|
18
18
|
): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
|
|
19
19
|
bottom == scrollAwayTop + scrollAwayBottom
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
29
|
|
|
26
30
|
val isEmpty: Boolean
|
|
27
|
-
get() =
|
|
31
|
+
get() = owners.isEmpty()
|
|
32
|
+
val drawingAlpha: Float
|
|
33
|
+
get() = baseline
|
|
28
34
|
|
|
29
|
-
fun update(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
if (inset == null) contributions.remove(owner) else contributions[owner] = inset
|
|
34
|
-
lastApplied = baseline + contributions.values.sum()
|
|
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
|
|
35
39
|
return lastApplied
|
|
36
40
|
}
|
|
37
41
|
}
|