@onekeyfe/react-native-pager-view 3.0.113 → 3.0.115

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.
@@ -5,7 +5,8 @@ import android.graphics.Rect
5
5
  import android.view.View
6
6
  import android.view.ViewGroup
7
7
  import android.view.ViewTreeObserver
8
- import android.widget.FrameLayout
8
+ // OneKey patch: FrameLayout is inherited through NestedScrollableHost.
9
+ // import android.widget.FrameLayout
9
10
  import androidx.core.view.NestedScrollingParent3
10
11
  import androidx.core.view.NestedScrollingParentHelper
11
12
  import androidx.core.view.ViewCompat
@@ -15,6 +16,7 @@ import androidx.viewpager2.widget.ViewPager2
15
16
  import java.util.WeakHashMap
16
17
  import kotlin.math.max
17
18
  import kotlin.math.min
19
+ import kotlin.math.roundToInt
18
20
 
19
21
  internal class CollapsiblePagerAdapter : RecyclerView.Adapter<ViewPagerViewHolder>() {
20
22
  private val pages = ArrayList<View>()
@@ -67,13 +69,16 @@ internal class CollapsiblePagerAdapter : RecyclerView.Adapter<ViewPagerViewHolde
67
69
  * vertical scroll view remains the content scroll source and horizontal gestures
68
70
  * remain owned by ViewPager2.
69
71
  */
70
- class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrollingParent3 {
72
+ // OneKey patch: Match the standard pager's nested horizontal gesture host.
73
+ // Original: class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrollingParent3 {
74
+ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), NestedScrollingParent3 {
71
75
  private data class RecyclerPadding(
72
- val left: Int,
73
- val top: Int,
74
- val right: Int,
75
- val bottom: Int,
76
- val clipToPadding: Boolean,
76
+ var left: Int,
77
+ var top: Int,
78
+ var right: Int,
79
+ var bottom: Int,
80
+ var clipToPadding: Boolean,
81
+ var appliedTopInset: Int = 0,
77
82
  )
78
83
 
79
84
  val pager = ViewPager2(context)
@@ -85,33 +90,38 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
85
90
  private val pageOffsets = HashMap<String, Int>()
86
91
  private var observedRecyclerView: RecyclerView? = null
87
92
  private var observedScrollListener: RecyclerView.OnScrollListener? = null
93
+ private var attachmentGeneration = 0
88
94
  private var headerView: View? = null
89
95
  private var stickyHeaderView: View? = null
90
96
  private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
97
+ if (width > 0 && height > 0 && isShown) layoutPagerIfRequested()
91
98
  // Fabric can mount a retained list without another Android layout pass.
92
99
  // Apply its header insets before that list first draws.
93
100
  prepareAdjacentPages()
94
101
  attachRecyclerObserver()
102
+ // OneKey patch: a filtered short page can reduce the existing collapse range.
103
+ setHeaderOffset(headerOffsetPx)
95
104
  true
96
105
  }
97
106
 
98
107
  var selectedPage = 0
99
108
  private set
100
109
  var pendingInitialPage = 0
110
+ private var hasAppliedInitialPage = false
101
111
  var headerHeightPx = 0
102
112
  set(value) {
103
113
  if (field == value) return
104
114
  field = max(0, value)
105
115
  headerOffsetPx = min(headerOffsetPx, field)
106
116
  reapplyRecyclerInsets()
107
- requestLayout()
117
+ updateHeaderLayout()
108
118
  }
109
119
  var stickyHeaderHeightPx = 0
110
120
  set(value) {
111
121
  if (field == value) return
112
122
  field = max(0, value)
113
123
  reapplyRecyclerInsets()
114
- requestLayout()
124
+ updateHeaderLayout()
115
125
  }
116
126
  var headerOffsetPx = 0
117
127
  private set
@@ -184,6 +194,7 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
184
194
  stickyHeaderView = null
185
195
  logicalChildren.clear()
186
196
  adapter.clear()
197
+ hasAppliedInitialPage = false
187
198
  }
188
199
 
189
200
  fun reactChildCount() = logicalChildren.size
@@ -194,14 +205,29 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
194
205
  if (adapter.itemCount == 0) return
195
206
  detachRecyclerObserver()
196
207
  selectedPage = position.coerceIn(0, adapter.itemCount - 1)
197
- post {
208
+ val savedOffset = pageOffsets[currentPageKey()]
209
+ ?: recyclerViewForPage(selectedPage)?.computeVerticalScrollOffset()
210
+ ?: 0
211
+ if (savedOffset > 0) setHeaderOffset(headerHeightPx)
212
+ postForCurrentAttachment {
198
213
  prepareAdjacentPages()
199
214
  attachRecyclerObserver()
200
215
  }
201
216
  }
202
217
 
218
+ private fun postForCurrentAttachment(block: () -> Unit) {
219
+ val generation = attachmentGeneration
220
+ post {
221
+ if (isAttachedToWindow && generation == attachmentGeneration) block()
222
+ }
223
+ }
224
+
203
225
  fun applyPendingInitialPage() {
226
+ // OneKey patch: Fabric can resend initialPage while retained pages change.
227
+ // Reapplying it interrupts the current gesture and can leave ViewPager2 settling.
228
+ if (hasAppliedInitialPage) return
204
229
  if (pendingInitialPage !in 0 until adapter.itemCount) return
230
+ hasAppliedInitialPage = true
205
231
  if (pager.currentItem != pendingInitialPage) {
206
232
  pager.setCurrentItem(pendingInitialPage, false)
207
233
  }
@@ -212,9 +238,40 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
212
238
 
213
239
  fun observedScrollableCount(): Int = originalRecyclerPadding.size
214
240
 
241
+ private fun layoutPagerIfRequested() {
242
+ val recycler = pager.getChildAt(0) as? RecyclerView ?: return
243
+ if (!recycler.isLayoutRequested || recycler.isComputingLayout || recycler.width <= 0 || recycler.height <= 0) return
244
+ // OneKey patch: a non-animated page command queues RecyclerView layout, but RN
245
+ // can stop that request after navigation. Drain it before showing the page.
246
+ recycler.measure(
247
+ MeasureSpec.makeMeasureSpec(recycler.width, MeasureSpec.EXACTLY),
248
+ MeasureSpec.makeMeasureSpec(recycler.height, MeasureSpec.EXACTLY),
249
+ )
250
+ recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
251
+ }
252
+
253
+ private fun updateHeaderLayout() {
254
+ requestLayout()
255
+ if (width <= 0 || height <= 0) return
256
+ // OneKey patch: React Native ancestors can stop requestLayout after header props change.
257
+ // Commit the custom native slots at their existing bounds before the next draw.
258
+ forceLayout()
259
+ measure(
260
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
261
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
262
+ )
263
+ layout(left, top, right, bottom)
264
+ }
265
+
215
266
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
216
267
  val width = MeasureSpec.getSize(widthMeasureSpec)
217
268
  val height = MeasureSpec.getSize(heightMeasureSpec)
269
+ // OneKey patch: a detached Fabric page has a zero viewport. Measuring ViewPager2
270
+ // at width zero spuriously selects page zero and replaces the retained JS pages.
271
+ if (width <= 0 || height <= 0) {
272
+ setMeasuredDimension(width, height)
273
+ return
274
+ }
218
275
  val pagerHeight = height + headerHeightPx
219
276
  pager.measure(
220
277
  MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
@@ -236,6 +293,7 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
236
293
  val height = bottom - top
237
294
  // RN ancestors allow overflow; keep translated header content inside this viewport.
238
295
  clipBounds = Rect(0, 0, width, height)
296
+ if (width <= 0 || height <= 0) return
239
297
  pager.layout(0, 0, width, height + headerHeightPx)
240
298
  headerView?.layout(0, 0, width, headerHeightPx)
241
299
  stickyHeaderView?.layout(
@@ -245,14 +303,26 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
245
303
  headerHeightPx + stickyHeaderHeightPx,
246
304
  )
247
305
  applyHeaderOffset()
248
- post {
306
+ postForCurrentAttachment {
249
307
  prepareAdjacentPages()
250
308
  attachRecyclerObserver()
251
309
  }
252
310
  }
253
311
 
312
+ private fun maximumHeaderOffset(): Int {
313
+ val recycler = observedRecyclerView ?: return headerHeightPx
314
+ if (recycler.childCount == 0 || recycler.canScrollVertically(-1) || recycler.canScrollVertically(1)) {
315
+ return headerHeightPx
316
+ }
317
+ // OneKey patch: short pages use the original rounded-DIP minimum content height.
318
+ val density = resources.displayMetrics.density
319
+ val contentHeight = ((height / density).roundToInt() + (headerHeightPx / density).roundToInt()) * density
320
+ return (contentHeight.roundToInt() - height).coerceIn(0, headerHeightPx)
321
+ }
322
+
254
323
  private fun setHeaderOffset(offset: Int) {
255
- val next = offset.coerceIn(0, headerHeightPx)
324
+ // val next = offset.coerceIn(0, headerHeightPx)
325
+ val next = offset.coerceIn(0, maximumHeaderOffset())
256
326
  if (headerOffsetPx == next) return
257
327
  headerOffsetPx = next
258
328
  applyHeaderOffset()
@@ -327,7 +397,13 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
327
397
  applyRecyclerInsets(recycler, selectedPage)
328
398
  val listener = object : RecyclerView.OnScrollListener() {
329
399
  override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
330
- pageOffsets[currentPageKey()] = recyclerView.computeVerticalScrollOffset()
400
+ val contentOffset = recyclerView.computeVerticalScrollOffset()
401
+ pageOffsets[currentPageKey()] = contentOffset
402
+ // Nested touch scrolling moves list content only after the header has
403
+ // collapsed. Programmatic list jumps bypass those parent callbacks.
404
+ if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
405
+ setHeaderOffset(headerHeightPx)
406
+ }
331
407
  }
332
408
  }
333
409
  observedScrollListener = listener
@@ -360,6 +436,7 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
360
436
  recycler.clipToPadding,
361
437
  )
362
438
  }
439
+ updateRecyclerPaddingOwnership(recycler, original)
363
440
  val topInset = headerHeightPx + stickyHeaderHeightPx
364
441
  recycler.clipToPadding = false
365
442
  val top = original.top + topInset
@@ -368,13 +445,39 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
368
445
  val bottom = original.bottom
369
446
  if (recycler.paddingLeft != original.left || recycler.paddingTop != top ||
370
447
  recycler.paddingRight != original.right || recycler.paddingBottom != bottom) {
448
+ // OneKey patch: LinearLayoutManager otherwise anchors children at their old
449
+ // screen coordinates when a page changes the sticky header height.
450
+ val manager = recycler.layoutManager as? LinearLayoutManager
451
+ val first = manager?.takeIf { it.orientation == RecyclerView.VERTICAL }
452
+ ?.findFirstVisibleItemPosition() ?: RecyclerView.NO_POSITION
453
+ val anchorOffset = if (first != RecyclerView.NO_POSITION) {
454
+ manager?.findViewByPosition(first)?.let { manager.getDecoratedTop(it) - recycler.paddingTop }
455
+ } else null
371
456
  recycler.setPadding(original.left, top, original.right, bottom)
457
+ if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
458
+ // React Native can stop this nested requestLayout at its React-owned parent.
459
+ // Commit the changed inset before drawing instead of waiting for a data update.
460
+ if (recycler.width > 0 && recycler.height > 0 && !recycler.isComputingLayout) {
461
+ recycler.forceLayout()
462
+ recycler.measure(
463
+ MeasureSpec.makeMeasureSpec(recycler.width, MeasureSpec.EXACTLY),
464
+ MeasureSpec.makeMeasureSpec(recycler.height, MeasureSpec.EXACTLY),
465
+ )
466
+ recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
467
+ }
372
468
  }
469
+ original.appliedTopInset = topInset
373
470
  val key = pageKey(pageIndex)
374
471
  if (restoredRecyclerKeys[recycler] != key) {
375
472
  restoredRecyclerKeys[recycler] = key
473
+ val generation = attachmentGeneration
376
474
  recycler.post {
377
- if (restoredRecyclerKeys[recycler] != key) return@post
475
+ if (!isAttachedToWindow || generation != attachmentGeneration ||
476
+ restoredRecyclerKeys[recycler] != key ||
477
+ originalRecyclerPadding[recycler] !== original) {
478
+ if (restoredRecyclerKeys[recycler] == key) restoredRecyclerKeys.remove(recycler)
479
+ return@post
480
+ }
378
481
  val saved = pageOffsets[key] ?: 0
379
482
  val current = recycler.computeVerticalScrollOffset()
380
483
  if (saved != current) recycler.scrollBy(0, saved - current)
@@ -382,12 +485,29 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
382
485
  }
383
486
  }
384
487
 
488
+ private fun updateRecyclerPaddingOwnership(
489
+ recycler: RecyclerView,
490
+ original: RecyclerPadding,
491
+ ) {
492
+ val expectedTop = original.top + original.appliedTopInset
493
+ if (recycler.paddingLeft != original.left) original.left = recycler.paddingLeft
494
+ if (recycler.paddingTop != expectedTop) {
495
+ // A React-owned list can replace its content padding while it remains
496
+ // attached. Remove our inset before retaining that value as the new base.
497
+ original.top = (recycler.paddingTop - original.appliedTopInset).coerceAtLeast(0)
498
+ }
499
+ if (recycler.paddingRight != original.right) original.right = recycler.paddingRight
500
+ if (recycler.paddingBottom != original.bottom) original.bottom = recycler.paddingBottom
501
+ if (recycler.clipToPadding) original.clipToPadding = true
502
+ }
503
+
385
504
  private fun reapplyRecyclerInsets() {
386
505
  prepareAdjacentPages()
387
506
  }
388
507
 
389
508
  private fun restoreRecyclerInsets(recycler: RecyclerView) {
390
509
  val original = originalRecyclerPadding.remove(recycler) ?: return
510
+ updateRecyclerPaddingOwnership(recycler, original)
391
511
  restoredRecyclerKeys.remove(recycler)
392
512
  recycler.clipToPadding = original.clipToPadding
393
513
  recycler.setPadding(original.left, original.top, original.right, original.bottom)
@@ -436,8 +556,12 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
436
556
 
437
557
  override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
438
558
  if (!isCurrentPageTarget(target)) return
439
- if (dy > 0 && headerOffsetPx < headerHeightPx) {
440
- val used = min(dy, headerHeightPx - headerOffsetPx)
559
+ // OneKey patch: consume only the distance the short-page header can actually move.
560
+ val maximumOffset = maximumHeaderOffset()
561
+ // if (dy > 0 && headerOffsetPx < headerHeightPx) {
562
+ // val used = min(dy, headerHeightPx - headerOffsetPx)
563
+ if (dy > 0 && headerOffsetPx < maximumOffset) {
564
+ val used = min(dy, maximumOffset - headerOffsetPx)
441
565
  setHeaderOffset(headerOffsetPx + used)
442
566
  consumed[1] += used
443
567
  } else if (dy < 0 && headerOffsetPx > 0 && !target.canScrollVertically(-1)) {
@@ -485,10 +609,13 @@ class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrol
485
609
 
486
610
  override fun onAttachedToWindow() {
487
611
  super.onAttachedToWindow()
612
+ attachmentGeneration += 1
613
+ restoredRecyclerKeys.clear()
488
614
  viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
489
615
  }
490
616
 
491
617
  override fun onDetachedFromWindow() {
618
+ attachmentGeneration += 1
492
619
  viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
493
620
  detachRecyclerObserver()
494
621
  for (recycler in originalRecyclerPadding.keys.toList()) {
@@ -19,6 +19,7 @@ import com.reactnativepagerview.event.PageScrollEvent
19
19
  import com.reactnativepagerview.event.PageScrollStateChangedEvent
20
20
  import com.reactnativepagerview.event.PageSelectedEvent
21
21
  import org.json.JSONArray
22
+ import kotlin.math.roundToInt
22
23
 
23
24
  @ReactModule(name = CollapsiblePagerViewManager.NAME)
24
25
  class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
@@ -145,14 +146,20 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
145
146
  }
146
147
  }
147
148
 
149
+ @ReactProp(name = "nestedScrollEnabled", defaultBoolean = false)
150
+ override fun setNestedScrollEnabled(view: CollapsiblePagerHost?, value: Boolean) {
151
+ // OneKey patch: CollapsiblePagerHost always coordinates nested pagers on Android.
152
+ }
153
+
154
+ // OneKey patch: round Yoga header dimensions to the nearest physical pixel.
148
155
  @ReactProp(name = "headerHeight", defaultInt = 0)
149
156
  override fun setHeaderHeight(view: CollapsiblePagerHost?, value: Int) {
150
- view?.headerHeightPx = PixelUtil.toPixelFromDIP(value.toDouble()).toInt()
157
+ view?.headerHeightPx = PixelUtil.toPixelFromDIP(value.toDouble()).roundToInt()
151
158
  }
152
159
 
153
160
  @ReactProp(name = "stickyHeaderHeight", defaultInt = 0)
154
161
  override fun setStickyHeaderHeight(view: CollapsiblePagerHost?, value: Int) {
155
- view?.stickyHeaderHeightPx = PixelUtil.toPixelFromDIP(value.toDouble()).toInt()
162
+ view?.stickyHeaderHeightPx = PixelUtil.toPixelFromDIP(value.toDouble()).roundToInt()
156
163
  }
157
164
 
158
165
  @ReactProp(name = "pageKeys")
@@ -21,7 +21,9 @@ import kotlin.math.sign
21
21
  * Supports multiple levels of nested PagerViews by re-asserting
22
22
  * requestDisallowInterceptTouchEvent after child dispatch.
23
23
  */
24
- class NestedScrollableHost : FrameLayout {
24
+ // OneKey patch: Let the collapsible pager reuse same-direction nested pager coordination.
25
+ // Original: class NestedScrollableHost : FrameLayout {
26
+ open class NestedScrollableHost : FrameLayout {
25
27
  constructor(context: Context) : super(context)
26
28
  constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
27
29
  public var initialIndex: Int? = null