@onekeyfe/react-native-pager-view 3.0.111 → 3.0.113
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 +40 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +503 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +215 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +39 -0
- package/ios/RNCCollapsiblePagerViewComponentView.h +9 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +738 -0
- package/lib/module/CollapsiblePagerView.js +152 -0
- package/lib/module/CollapsiblePagerView.web.js +364 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +85 -0
- package/lib/module/PagerView.web.js +177 -0
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +6 -0
- package/lib/module/usePagerView.js +1 -1
- package/lib/typescript/src/CollapsiblePagerView.d.ts +58 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +93 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +50 -0
- package/lib/typescript/src/PagerView.web.d.ts +55 -0
- package/lib/typescript/src/index.d.ts +5 -4
- package/lib/typescript/src/index.web.d.ts +5 -0
- package/package.json +5 -2
- package/src/CollapsiblePagerView.tsx +258 -0
- package/src/CollapsiblePagerView.web.tsx +559 -0
- package/src/CollapsiblePagerViewNativeComponent.ts +85 -0
- package/src/PagerView.web.tsx +280 -0
- package/src/index.tsx +5 -4
- package/src/index.web.tsx +12 -0
package/README.md
CHANGED
|
@@ -27,5 +27,45 @@ development and delivery stable.
|
|
|
27
27
|
- If you are looking for original behavior and full documentation, please refer
|
|
28
28
|
to the upstream repository.
|
|
29
29
|
|
|
30
|
+
## Collapsible pager
|
|
31
|
+
|
|
32
|
+
`CollapsiblePagerView` is an opt-in container. The default `PagerView` export
|
|
33
|
+
keeps its existing page lifetime and native implementation.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<CollapsiblePagerView
|
|
37
|
+
header={<Header />}
|
|
38
|
+
stickyHeader={<Tabs />}
|
|
39
|
+
headerHeight={measuredHeaderHeight}
|
|
40
|
+
stickyHeaderHeight={measuredTabsHeight}
|
|
41
|
+
pageRetentionDistance={1}
|
|
42
|
+
onMountedPagesChanged={({ mountedPages }) => {
|
|
43
|
+
// Selected page plus one neighbour on each side by default.
|
|
44
|
+
}}
|
|
45
|
+
onCollapsibleStateChanged={({ nativeEvent }) => {
|
|
46
|
+
// Settled diagnostics: headerOffset, native/attached page counts, etc.
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{pages.map((page) => (
|
|
50
|
+
<View key={page.key}>{page.content}</View>
|
|
51
|
+
))}
|
|
52
|
+
</CollapsiblePagerView>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Page keys must remain stable. The selected page and configured neighbours stay
|
|
56
|
+
mounted for preheating; distant page slots remain in the pager while their
|
|
57
|
+
React/native heavy subtrees are unmounted. iOS and Android retain a per-key
|
|
58
|
+
scroll offset and preserve the shared header collapse when pages change. A
|
|
59
|
+
NativeList can additionally restore its own stable item-key anchor when it is
|
|
60
|
+
remounted; the pager does not replace that list-level anchor protocol or its
|
|
61
|
+
cell reuse.
|
|
62
|
+
|
|
63
|
+
On iOS the component observes the current `UIScrollView` content offset without
|
|
64
|
+
replacing its delegate. Android coordinates nested scroll with the active
|
|
65
|
+
`RecyclerView`. Web uses the active NativeList viewport automatically; another
|
|
66
|
+
Web scroll child can opt in by exposing a DOM element with
|
|
67
|
+
`data-collapsible-pager-scroll`. CSS scroll timelines drive header/sticky
|
|
68
|
+
movement, so React is not updated on each vertical scroll frame.
|
|
69
|
+
|
|
30
70
|
Thank you again to Callstack and everyone who contributes to
|
|
31
71
|
`react-native-pager-view` 💙
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Rect
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.ViewGroup
|
|
7
|
+
import android.view.ViewTreeObserver
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import androidx.core.view.NestedScrollingParent3
|
|
10
|
+
import androidx.core.view.NestedScrollingParentHelper
|
|
11
|
+
import androidx.core.view.ViewCompat
|
|
12
|
+
import androidx.recyclerview.widget.LinearLayoutManager
|
|
13
|
+
import androidx.recyclerview.widget.RecyclerView
|
|
14
|
+
import androidx.viewpager2.widget.ViewPager2
|
|
15
|
+
import java.util.WeakHashMap
|
|
16
|
+
import kotlin.math.max
|
|
17
|
+
import kotlin.math.min
|
|
18
|
+
|
|
19
|
+
internal class CollapsiblePagerAdapter : RecyclerView.Adapter<ViewPagerViewHolder>() {
|
|
20
|
+
private val pages = ArrayList<View>()
|
|
21
|
+
|
|
22
|
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
|
23
|
+
ViewPagerViewHolder.create(parent)
|
|
24
|
+
|
|
25
|
+
override fun onBindViewHolder(holder: ViewPagerViewHolder, position: Int) {
|
|
26
|
+
val page = pages[position]
|
|
27
|
+
val container = holder.container
|
|
28
|
+
container.removeAllViews()
|
|
29
|
+
(page.parent as? ViewGroup)?.removeView(page)
|
|
30
|
+
container.addView(page)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun onViewRecycled(holder: ViewPagerViewHolder) {
|
|
34
|
+
holder.container.removeAllViews()
|
|
35
|
+
super.onViewRecycled(holder)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun getItemCount() = pages.size
|
|
39
|
+
|
|
40
|
+
fun addPage(page: View, index: Int) {
|
|
41
|
+
val safeIndex = index.coerceIn(0, pages.size)
|
|
42
|
+
pages.add(safeIndex, page)
|
|
43
|
+
notifyItemInserted(safeIndex)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun removePage(page: View) {
|
|
47
|
+
val index = pages.indexOf(page)
|
|
48
|
+
if (index >= 0) {
|
|
49
|
+
pages.removeAt(index)
|
|
50
|
+
notifyItemRemoved(index)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun pageAt(index: Int) = pages[index]
|
|
55
|
+
|
|
56
|
+
fun clear() {
|
|
57
|
+
val count = pages.size
|
|
58
|
+
pages.forEach { page -> (page.parent as? ViewGroup)?.removeView(page) }
|
|
59
|
+
pages.clear()
|
|
60
|
+
if (count > 0) notifyItemRangeRemoved(0, count)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Native vertical coordinator for the optional collapsible PagerView variant.
|
|
66
|
+
* It consumes only the header part of vertical nested scroll. The active page's
|
|
67
|
+
* vertical scroll view remains the content scroll source and horizontal gestures
|
|
68
|
+
* remain owned by ViewPager2.
|
|
69
|
+
*/
|
|
70
|
+
class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrollingParent3 {
|
|
71
|
+
private data class RecyclerPadding(
|
|
72
|
+
val left: Int,
|
|
73
|
+
val top: Int,
|
|
74
|
+
val right: Int,
|
|
75
|
+
val bottom: Int,
|
|
76
|
+
val clipToPadding: Boolean,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
val pager = ViewPager2(context)
|
|
80
|
+
internal val adapter = CollapsiblePagerAdapter()
|
|
81
|
+
private val logicalChildren = ArrayList<View>()
|
|
82
|
+
private val nestedScrollingParentHelper = NestedScrollingParentHelper(this)
|
|
83
|
+
private val originalRecyclerPadding = WeakHashMap<RecyclerView, RecyclerPadding>()
|
|
84
|
+
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
85
|
+
private val pageOffsets = HashMap<String, Int>()
|
|
86
|
+
private var observedRecyclerView: RecyclerView? = null
|
|
87
|
+
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
88
|
+
private var headerView: View? = null
|
|
89
|
+
private var stickyHeaderView: View? = null
|
|
90
|
+
private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
|
|
91
|
+
// Fabric can mount a retained list without another Android layout pass.
|
|
92
|
+
// Apply its header insets before that list first draws.
|
|
93
|
+
prepareAdjacentPages()
|
|
94
|
+
attachRecyclerObserver()
|
|
95
|
+
true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var selectedPage = 0
|
|
99
|
+
private set
|
|
100
|
+
var pendingInitialPage = 0
|
|
101
|
+
var headerHeightPx = 0
|
|
102
|
+
set(value) {
|
|
103
|
+
if (field == value) return
|
|
104
|
+
field = max(0, value)
|
|
105
|
+
headerOffsetPx = min(headerOffsetPx, field)
|
|
106
|
+
reapplyRecyclerInsets()
|
|
107
|
+
requestLayout()
|
|
108
|
+
}
|
|
109
|
+
var stickyHeaderHeightPx = 0
|
|
110
|
+
set(value) {
|
|
111
|
+
if (field == value) return
|
|
112
|
+
field = max(0, value)
|
|
113
|
+
reapplyRecyclerInsets()
|
|
114
|
+
requestLayout()
|
|
115
|
+
}
|
|
116
|
+
var headerOffsetPx = 0
|
|
117
|
+
private set
|
|
118
|
+
var pageKeys: List<String> = emptyList()
|
|
119
|
+
var retainedPages: String = "[]"
|
|
120
|
+
var onHeaderOffsetChanged: (() -> Unit)? = null
|
|
121
|
+
|
|
122
|
+
init {
|
|
123
|
+
isSaveEnabled = false
|
|
124
|
+
clipChildren = true
|
|
125
|
+
pager.id = View.generateViewId()
|
|
126
|
+
pager.adapter = adapter
|
|
127
|
+
pager.isSaveEnabled = false
|
|
128
|
+
super.addView(
|
|
129
|
+
pager,
|
|
130
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fun addReactChild(child: View, index: Int) {
|
|
135
|
+
val safeIndex = index.coerceIn(0, logicalChildren.size)
|
|
136
|
+
logicalChildren.add(safeIndex, child)
|
|
137
|
+
when (safeIndex) {
|
|
138
|
+
0 -> {
|
|
139
|
+
headerView = child
|
|
140
|
+
(child.parent as? ViewGroup)?.removeView(child)
|
|
141
|
+
super.addView(child)
|
|
142
|
+
}
|
|
143
|
+
1 -> {
|
|
144
|
+
stickyHeaderView = child
|
|
145
|
+
(child.parent as? ViewGroup)?.removeView(child)
|
|
146
|
+
super.addView(child)
|
|
147
|
+
}
|
|
148
|
+
else -> adapter.addPage(child, safeIndex - PAGE_SLOT_OFFSET)
|
|
149
|
+
}
|
|
150
|
+
applyPendingInitialPage()
|
|
151
|
+
headerView?.bringToFront()
|
|
152
|
+
stickyHeaderView?.bringToFront()
|
|
153
|
+
requestLayout()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fun removeReactChild(child: View) {
|
|
157
|
+
val index = logicalChildren.indexOf(child)
|
|
158
|
+
if (index < 0) return
|
|
159
|
+
logicalChildren.removeAt(index)
|
|
160
|
+
when (index) {
|
|
161
|
+
0 -> {
|
|
162
|
+
super.removeView(child)
|
|
163
|
+
headerView = null
|
|
164
|
+
}
|
|
165
|
+
1 -> {
|
|
166
|
+
super.removeView(child)
|
|
167
|
+
stickyHeaderView = null
|
|
168
|
+
}
|
|
169
|
+
else -> {
|
|
170
|
+
findVerticalRecyclerView(child)?.let(::restoreRecyclerInsets)
|
|
171
|
+
adapter.removePage(child)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun removeAllReactChildren() {
|
|
177
|
+
detachRecyclerObserver()
|
|
178
|
+
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
179
|
+
restoreRecyclerInsets(recycler)
|
|
180
|
+
}
|
|
181
|
+
headerView?.let { view -> super.removeView(view) }
|
|
182
|
+
stickyHeaderView?.let { view -> super.removeView(view) }
|
|
183
|
+
headerView = null
|
|
184
|
+
stickyHeaderView = null
|
|
185
|
+
logicalChildren.clear()
|
|
186
|
+
adapter.clear()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fun reactChildCount() = logicalChildren.size
|
|
190
|
+
|
|
191
|
+
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
192
|
+
|
|
193
|
+
fun selectPage(position: Int) {
|
|
194
|
+
if (adapter.itemCount == 0) return
|
|
195
|
+
detachRecyclerObserver()
|
|
196
|
+
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
197
|
+
post {
|
|
198
|
+
prepareAdjacentPages()
|
|
199
|
+
attachRecyclerObserver()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fun applyPendingInitialPage() {
|
|
204
|
+
if (pendingInitialPage !in 0 until adapter.itemCount) return
|
|
205
|
+
if (pager.currentItem != pendingInitialPage) {
|
|
206
|
+
pager.setCurrentItem(pendingInitialPage, false)
|
|
207
|
+
}
|
|
208
|
+
selectPage(pendingInitialPage)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fun attachedPageCount(): Int = (pager.getChildAt(0) as? RecyclerView)?.childCount ?: 0
|
|
212
|
+
|
|
213
|
+
fun observedScrollableCount(): Int = originalRecyclerPadding.size
|
|
214
|
+
|
|
215
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
216
|
+
val width = MeasureSpec.getSize(widthMeasureSpec)
|
|
217
|
+
val height = MeasureSpec.getSize(heightMeasureSpec)
|
|
218
|
+
val pagerHeight = height + headerHeightPx
|
|
219
|
+
pager.measure(
|
|
220
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
221
|
+
MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.EXACTLY),
|
|
222
|
+
)
|
|
223
|
+
headerView?.measure(
|
|
224
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
225
|
+
MeasureSpec.makeMeasureSpec(headerHeightPx, MeasureSpec.EXACTLY),
|
|
226
|
+
)
|
|
227
|
+
stickyHeaderView?.measure(
|
|
228
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
229
|
+
MeasureSpec.makeMeasureSpec(stickyHeaderHeightPx, MeasureSpec.EXACTLY),
|
|
230
|
+
)
|
|
231
|
+
setMeasuredDimension(width, height)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
235
|
+
val width = right - left
|
|
236
|
+
val height = bottom - top
|
|
237
|
+
// RN ancestors allow overflow; keep translated header content inside this viewport.
|
|
238
|
+
clipBounds = Rect(0, 0, width, height)
|
|
239
|
+
pager.layout(0, 0, width, height + headerHeightPx)
|
|
240
|
+
headerView?.layout(0, 0, width, headerHeightPx)
|
|
241
|
+
stickyHeaderView?.layout(
|
|
242
|
+
0,
|
|
243
|
+
headerHeightPx,
|
|
244
|
+
width,
|
|
245
|
+
headerHeightPx + stickyHeaderHeightPx,
|
|
246
|
+
)
|
|
247
|
+
applyHeaderOffset()
|
|
248
|
+
post {
|
|
249
|
+
prepareAdjacentPages()
|
|
250
|
+
attachRecyclerObserver()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun setHeaderOffset(offset: Int) {
|
|
255
|
+
val next = offset.coerceIn(0, headerHeightPx)
|
|
256
|
+
if (headerOffsetPx == next) return
|
|
257
|
+
headerOffsetPx = next
|
|
258
|
+
applyHeaderOffset()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun applyHeaderOffset() {
|
|
262
|
+
val offset = headerOffsetPx.toFloat()
|
|
263
|
+
pager.translationY = -offset
|
|
264
|
+
headerView?.translationY = -offset
|
|
265
|
+
stickyHeaderView?.translationY = -offset
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
269
|
+
|
|
270
|
+
private fun currentPageKey(): String = pageKey(selectedPage)
|
|
271
|
+
|
|
272
|
+
private fun isDescendant(view: View, ancestor: View): Boolean {
|
|
273
|
+
var current: View? = view
|
|
274
|
+
while (current != null) {
|
|
275
|
+
if (current === ancestor) return true
|
|
276
|
+
current = current.parent as? View
|
|
277
|
+
}
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private fun findVerticalRecyclerView(view: View): RecyclerView? {
|
|
282
|
+
if (view is RecyclerView) {
|
|
283
|
+
val manager = view.layoutManager
|
|
284
|
+
if (manager !is LinearLayoutManager || manager.orientation == RecyclerView.VERTICAL) {
|
|
285
|
+
return view
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (view is ViewGroup) {
|
|
289
|
+
for (index in 0 until view.childCount) {
|
|
290
|
+
findVerticalRecyclerView(view.getChildAt(index))?.let { return it }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private fun recyclerViewForPage(index: Int): RecyclerView? {
|
|
297
|
+
if (index !in 0 until adapter.itemCount) return null
|
|
298
|
+
return findVerticalRecyclerView(adapter.pageAt(index))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private fun prepareAdjacentPages() {
|
|
302
|
+
val first = max(0, selectedPage - 1)
|
|
303
|
+
val last = min(adapter.itemCount - 1, selectedPage + 1)
|
|
304
|
+
for (index in first..last) {
|
|
305
|
+
recyclerViewForPage(index)?.let { recycler ->
|
|
306
|
+
applyRecyclerInsets(recycler, index)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun attachRecyclerObserver() {
|
|
312
|
+
if (selectedPage !in 0 until adapter.itemCount) return
|
|
313
|
+
val previous = observedRecyclerView
|
|
314
|
+
if (previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
315
|
+
detachRecyclerObserver()
|
|
316
|
+
// A replacement list starts at its first row, retaining only header collapse.
|
|
317
|
+
pageOffsets[currentPageKey()] = 0
|
|
318
|
+
}
|
|
319
|
+
val recycler = recyclerViewForPage(selectedPage) ?: return
|
|
320
|
+
if (observedRecyclerView === recycler) {
|
|
321
|
+
applyRecyclerInsets(recycler, selectedPage)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
detachRecyclerObserver()
|
|
326
|
+
observedRecyclerView = recycler
|
|
327
|
+
applyRecyclerInsets(recycler, selectedPage)
|
|
328
|
+
val listener = object : RecyclerView.OnScrollListener() {
|
|
329
|
+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
330
|
+
pageOffsets[currentPageKey()] = recyclerView.computeVerticalScrollOffset()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
observedScrollListener = listener
|
|
334
|
+
recycler.addOnScrollListener(listener)
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun detachRecyclerObserver() {
|
|
339
|
+
storeObservedPageOffset()
|
|
340
|
+
val recycler = observedRecyclerView
|
|
341
|
+
val listener = observedScrollListener
|
|
342
|
+
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
343
|
+
observedRecyclerView = null
|
|
344
|
+
observedScrollListener = null
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private fun storeObservedPageOffset() {
|
|
348
|
+
observedRecyclerView?.let { recycler ->
|
|
349
|
+
pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private fun applyRecyclerInsets(recycler: RecyclerView, pageIndex: Int) {
|
|
354
|
+
val original = originalRecyclerPadding.getOrPut(recycler) {
|
|
355
|
+
RecyclerPadding(
|
|
356
|
+
recycler.paddingLeft,
|
|
357
|
+
recycler.paddingTop,
|
|
358
|
+
recycler.paddingRight,
|
|
359
|
+
recycler.paddingBottom,
|
|
360
|
+
recycler.clipToPadding,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
val topInset = headerHeightPx + stickyHeaderHeightPx
|
|
364
|
+
recycler.clipToPadding = false
|
|
365
|
+
val top = original.top + topInset
|
|
366
|
+
// The taller pager already accounts for the translated header. Adding it
|
|
367
|
+
// again leaves a full header-sized blank footer at the end of long lists.
|
|
368
|
+
val bottom = original.bottom
|
|
369
|
+
if (recycler.paddingLeft != original.left || recycler.paddingTop != top ||
|
|
370
|
+
recycler.paddingRight != original.right || recycler.paddingBottom != bottom) {
|
|
371
|
+
recycler.setPadding(original.left, top, original.right, bottom)
|
|
372
|
+
}
|
|
373
|
+
val key = pageKey(pageIndex)
|
|
374
|
+
if (restoredRecyclerKeys[recycler] != key) {
|
|
375
|
+
restoredRecyclerKeys[recycler] = key
|
|
376
|
+
recycler.post {
|
|
377
|
+
if (restoredRecyclerKeys[recycler] != key) return@post
|
|
378
|
+
val saved = pageOffsets[key] ?: 0
|
|
379
|
+
val current = recycler.computeVerticalScrollOffset()
|
|
380
|
+
if (saved != current) recycler.scrollBy(0, saved - current)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private fun reapplyRecyclerInsets() {
|
|
386
|
+
prepareAdjacentPages()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private fun restoreRecyclerInsets(recycler: RecyclerView) {
|
|
390
|
+
val original = originalRecyclerPadding.remove(recycler) ?: return
|
|
391
|
+
restoredRecyclerKeys.remove(recycler)
|
|
392
|
+
recycler.clipToPadding = original.clipToPadding
|
|
393
|
+
recycler.setPadding(original.left, original.top, original.right, original.bottom)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private fun isCurrentPageTarget(target: View): Boolean {
|
|
397
|
+
if (selectedPage !in 0 until adapter.itemCount) return false
|
|
398
|
+
return isDescendant(target, adapter.pageAt(selectedPage))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// RN ScrollView uses the platform nested-scroll callbacks; RecyclerView
|
|
402
|
+
// uses the AndroidX variants with an explicit input type.
|
|
403
|
+
override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean =
|
|
404
|
+
onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH)
|
|
405
|
+
|
|
406
|
+
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) =
|
|
407
|
+
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH)
|
|
408
|
+
|
|
409
|
+
override fun onStopNestedScroll(target: View) =
|
|
410
|
+
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH)
|
|
411
|
+
|
|
412
|
+
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) =
|
|
413
|
+
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH)
|
|
414
|
+
|
|
415
|
+
override fun onNestedScroll(
|
|
416
|
+
target: View,
|
|
417
|
+
dxConsumed: Int,
|
|
418
|
+
dyConsumed: Int,
|
|
419
|
+
dxUnconsumed: Int,
|
|
420
|
+
dyUnconsumed: Int,
|
|
421
|
+
) = onNestedScroll(
|
|
422
|
+
target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean =
|
|
426
|
+
axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 && isCurrentPageTarget(target)
|
|
427
|
+
|
|
428
|
+
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
429
|
+
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
override fun onStopNestedScroll(target: View, type: Int) {
|
|
433
|
+
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
434
|
+
onHeaderOffsetChanged?.invoke()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
|
|
438
|
+
if (!isCurrentPageTarget(target)) return
|
|
439
|
+
if (dy > 0 && headerOffsetPx < headerHeightPx) {
|
|
440
|
+
val used = min(dy, headerHeightPx - headerOffsetPx)
|
|
441
|
+
setHeaderOffset(headerOffsetPx + used)
|
|
442
|
+
consumed[1] += used
|
|
443
|
+
} else if (dy < 0 && headerOffsetPx > 0 && !target.canScrollVertically(-1)) {
|
|
444
|
+
val used = max(dy, -headerOffsetPx)
|
|
445
|
+
setHeaderOffset(headerOffsetPx + used)
|
|
446
|
+
consumed[1] += used
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
override fun onNestedScroll(
|
|
451
|
+
target: View,
|
|
452
|
+
dxConsumed: Int,
|
|
453
|
+
dyConsumed: Int,
|
|
454
|
+
dxUnconsumed: Int,
|
|
455
|
+
dyUnconsumed: Int,
|
|
456
|
+
type: Int,
|
|
457
|
+
consumed: IntArray,
|
|
458
|
+
) {
|
|
459
|
+
if (!isCurrentPageTarget(target) || dyUnconsumed >= 0 || headerOffsetPx <= 0) return
|
|
460
|
+
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
461
|
+
setHeaderOffset(headerOffsetPx + used)
|
|
462
|
+
consumed[1] += used
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
override fun onNestedScroll(
|
|
466
|
+
target: View,
|
|
467
|
+
dxConsumed: Int,
|
|
468
|
+
dyConsumed: Int,
|
|
469
|
+
dxUnconsumed: Int,
|
|
470
|
+
dyUnconsumed: Int,
|
|
471
|
+
type: Int,
|
|
472
|
+
) {
|
|
473
|
+
onNestedScroll(
|
|
474
|
+
target,
|
|
475
|
+
dxConsumed,
|
|
476
|
+
dyConsumed,
|
|
477
|
+
dxUnconsumed,
|
|
478
|
+
dyUnconsumed,
|
|
479
|
+
type,
|
|
480
|
+
IntArray(2),
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
override fun getNestedScrollAxes() = nestedScrollingParentHelper.nestedScrollAxes
|
|
485
|
+
|
|
486
|
+
override fun onAttachedToWindow() {
|
|
487
|
+
super.onAttachedToWindow()
|
|
488
|
+
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
override fun onDetachedFromWindow() {
|
|
492
|
+
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
493
|
+
detachRecyclerObserver()
|
|
494
|
+
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
495
|
+
restoreRecyclerInsets(recycler)
|
|
496
|
+
}
|
|
497
|
+
super.onDetachedFromWindow()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
companion object {
|
|
501
|
+
const val PAGE_SLOT_OFFSET = 2
|
|
502
|
+
}
|
|
503
|
+
}
|