@onekeyfe/react-native-pager-view 3.0.123 → 3.0.124

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.
@@ -9,6 +9,7 @@ import android.view.ViewConfiguration
9
9
  import android.view.ViewGroup
10
10
  import android.view.ViewTreeObserver
11
11
  import android.widget.HorizontalScrollView
12
+ import android.widget.ScrollView
12
13
  // OneKey patch: FrameLayout is inherited through NestedScrollableHost.
13
14
  // import android.widget.FrameLayout
14
15
  import androidx.core.view.NestedScrollingParent3
@@ -17,6 +18,7 @@ import androidx.core.view.ViewCompat
17
18
  import androidx.recyclerview.widget.LinearLayoutManager
18
19
  import androidx.recyclerview.widget.RecyclerView
19
20
  import androidx.viewpager2.widget.ViewPager2
21
+ import com.facebook.react.R as ReactR
20
22
  import com.facebook.react.uimanager.events.NativeGestureUtil
21
23
  import com.margelo.nitro.nativelogger.OneKeyLog
22
24
  import java.util.WeakHashMap
@@ -94,6 +96,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
94
96
  var appliedTopInset: Int = 0,
95
97
  )
96
98
 
99
+ private data class ScrollerPadding(
100
+ var left: Int,
101
+ var top: Int,
102
+ var right: Int,
103
+ var bottom: Int,
104
+ var clipToPadding: Boolean,
105
+ var appliedTopInset: Int = 0,
106
+ )
107
+
97
108
  val pager = ViewPager2(context)
98
109
  internal val adapter = CollapsiblePagerAdapter()
99
110
  private val nativeTabBarView = CollapsiblePagerNativeTabBarView(context)
@@ -102,9 +113,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
102
113
  private val nestedScrollingParentHelper = NestedScrollingParentHelper(this)
103
114
  private val originalRecyclerPadding = WeakHashMap<RecyclerView, RecyclerPadding>()
104
115
  private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
116
+ private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
117
+ private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
105
118
  private val pageOffsets = HashMap<String, Int>()
106
119
  private var observedRecyclerView: RecyclerView? = null
107
120
  private var observedScrollListener: RecyclerView.OnScrollListener? = null
121
+ private var observedNativeScroller: ScrollView? = null
122
+ private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
108
123
  private var attachmentGeneration = 0
109
124
  private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
110
125
  private val hostIdentity = System.identityHashCode(this)
@@ -117,7 +132,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
117
132
  private var headerDownY = 0f
118
133
  private var headerDownEvent: MotionEvent? = null
119
134
  private var headerHasHorizontalChild = false
120
- private var forwardedRecycler: RecyclerView? = null
135
+ private var forwardedScrollable: View? = null
121
136
  private var nativeGestureStarted = false
122
137
  private var pressCancelled = false
123
138
  private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
@@ -125,7 +140,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
125
140
  // Fabric can mount a retained list without another Android layout pass.
126
141
  // Apply its header insets before that list first draws.
127
142
  prepareAdjacentPages()
128
- attachRecyclerObserver()
143
+ attachPrimaryScrollObserver()
129
144
  // OneKey patch: a filtered short page can reduce the existing collapse range.
130
145
  setHeaderOffset(headerOffsetPx)
131
146
  true
@@ -140,14 +155,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
140
155
  if (field == value) return
141
156
  field = max(0, value)
142
157
  headerOffsetPx = min(headerOffsetPx, field)
143
- reapplyRecyclerInsets()
158
+ reapplyScrollableInsets()
144
159
  updateHeaderLayout()
145
160
  }
146
161
  var stickyHeaderHeightPx = 0
147
162
  set(value) {
148
163
  if (field == value) return
149
164
  field = max(0, value)
150
- reapplyRecyclerInsets()
165
+ reapplyScrollableInsets()
151
166
  updateHeaderLayout()
152
167
  }
153
168
  var headerOffsetPx = 0
@@ -306,7 +321,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
306
321
  stickyHeaderView = null
307
322
  }
308
323
  else -> {
309
- findVerticalRecyclerView(child)?.let(::restoreRecyclerInsets)
324
+ when (val scrollable =
325
+ findExplicitNativeScrollerInView(child) ?: findVerticalScrollableView(child)) {
326
+ is RecyclerView -> restoreRecyclerInsets(scrollable)
327
+ is ScrollView -> restoreNativeScrollerInsets(scrollable)
328
+ }
310
329
  adapter.removePage(child)
311
330
  log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
312
331
  }
@@ -314,10 +333,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
314
333
  }
315
334
 
316
335
  fun removeAllReactChildren() {
317
- detachRecyclerObserver()
336
+ detachPrimaryScrollObserver()
318
337
  for (recycler in originalRecyclerPadding.keys.toList()) {
319
338
  restoreRecyclerInsets(recycler)
320
339
  }
340
+ for (scroller in originalScrollerPadding.keys.toList()) {
341
+ restoreNativeScrollerInsets(scroller)
342
+ }
321
343
  headerView?.let { view -> super.removeView(view) }
322
344
  stickyHeaderView?.let { view -> super.removeView(view) }
323
345
  headerView = null
@@ -334,15 +356,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
334
356
 
335
357
  fun selectPage(position: Int) {
336
358
  if (adapter.itemCount == 0) return
337
- detachRecyclerObserver()
359
+ detachPrimaryScrollObserver()
338
360
  selectedPage = position.coerceIn(0, adapter.itemCount - 1)
339
361
  val savedOffset = pageOffsets[currentPageKey()]
340
- ?: recyclerViewForPage(selectedPage)?.computeVerticalScrollOffset()
362
+ ?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
341
363
  ?: 0
342
364
  if (savedOffset > 0) setHeaderOffset(headerHeightPx)
343
365
  postForCurrentAttachment {
344
366
  prepareAdjacentPages()
345
- attachRecyclerObserver()
367
+ attachPrimaryScrollObserver()
346
368
  }
347
369
  }
348
370
 
@@ -367,7 +389,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
367
389
 
368
390
  fun attachedPageCount(): Int = (pager.getChildAt(0) as? RecyclerView)?.childCount ?: 0
369
391
 
370
- fun observedScrollableCount(): Int = originalRecyclerPadding.size
392
+ fun observedScrollableCount(): Int =
393
+ originalRecyclerPadding.size + originalScrollerPadding.size
371
394
 
372
395
  private fun layoutPagerIfRequested() {
373
396
  val recycler = pager.getChildAt(0) as? RecyclerView ?: return
@@ -458,13 +481,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
458
481
  applyHeaderOffset()
459
482
  postForCurrentAttachment {
460
483
  prepareAdjacentPages()
461
- attachRecyclerObserver()
484
+ attachPrimaryScrollObserver()
462
485
  }
463
486
  }
464
487
 
465
488
  private fun maximumHeaderOffset(): Int {
466
- val recycler = observedRecyclerView ?: return headerHeightPx
467
- if (recycler.childCount == 0 || recycler.canScrollVertically(-1) || recycler.canScrollVertically(1)) {
489
+ val scrollable = observedRecyclerView
490
+ ?: observedNativeScroller
491
+ ?: primaryScrollableForPage(selectedPage)
492
+ ?: return headerHeightPx
493
+ val childCount = (scrollable as? ViewGroup)?.childCount ?: 0
494
+ if (childCount == 0 || scrollable.canScrollVertically(-1) || scrollable.canScrollVertically(1)) {
468
495
  return headerHeightPx
469
496
  }
470
497
  // OneKey patch: short pages use the original rounded-DIP minimum content height.
@@ -503,51 +530,96 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
503
530
  return false
504
531
  }
505
532
 
506
- private fun findVerticalRecyclerView(view: View): RecyclerView? {
507
- if (view is RecyclerView) {
508
- val manager = view.layoutManager
509
- if (manager !is LinearLayoutManager || manager.orientation == RecyclerView.VERTICAL) {
510
- return view
533
+ private fun isVerticalRecyclerView(view: RecyclerView): Boolean {
534
+ val manager = view.layoutManager
535
+ return manager !is LinearLayoutManager || manager.orientation == RecyclerView.VERTICAL
536
+ }
537
+
538
+ private fun isExplicitNativeScroller(view: View): Boolean {
539
+ val nativeId = view.getTag(ReactR.id.view_tag_native_id) as? String ?: return false
540
+ return nativeId.startsWith(NATIVE_SCROLLER_ID_PREFIX)
541
+ }
542
+
543
+ private fun findExplicitNativeScrollerInView(view: View): View? {
544
+ if (view !== this && view is CollapsiblePagerHost) {
545
+ return view.currentPrimaryScrollableForParent()
546
+ }
547
+ if (isExplicitNativeScroller(view) &&
548
+ (view is ScrollView || view is RecyclerView && isVerticalRecyclerView(view))) {
549
+ return view
550
+ }
551
+ if (view is ViewGroup) {
552
+ for (index in 0 until view.childCount) {
553
+ findExplicitNativeScrollerInView(view.getChildAt(index))?.let { return it }
511
554
  }
512
555
  }
556
+ return null
557
+ }
558
+
559
+ private fun findVerticalScrollableView(view: View): View? {
560
+ if (view !== this && view is CollapsiblePagerHost) {
561
+ return view.currentPrimaryScrollableForParent()
562
+ }
563
+ if (view is RecyclerView && isVerticalRecyclerView(view)) return view
564
+ if (view is ScrollView) return view
513
565
  if (view is ViewGroup) {
514
566
  for (index in 0 until view.childCount) {
515
- findVerticalRecyclerView(view.getChildAt(index))?.let { return it }
567
+ findVerticalScrollableView(view.getChildAt(index))?.let { return it }
516
568
  }
517
569
  }
518
570
  return null
519
571
  }
520
572
 
521
- private fun recyclerViewForPage(index: Int): RecyclerView? {
573
+ private fun primaryScrollableForPage(index: Int): View? {
522
574
  if (index !in 0 until adapter.itemCount) return null
523
- return findVerticalRecyclerView(adapter.pageAt(index))
575
+ val page = adapter.pageAt(index)
576
+ return findExplicitNativeScrollerInView(page) ?: findVerticalScrollableView(page)
577
+ }
578
+
579
+ internal fun currentPrimaryScrollableForParent(): View? =
580
+ primaryScrollableForPage(selectedPage)
581
+
582
+ private fun recyclerViewForPage(index: Int): RecyclerView? =
583
+ primaryScrollableForPage(index) as? RecyclerView
584
+
585
+ private fun verticalScrollOffset(view: View): Int = when (view) {
586
+ is RecyclerView -> view.computeVerticalScrollOffset()
587
+ is ScrollView -> view.scrollY
588
+ else -> 0
524
589
  }
525
590
 
526
591
  private fun prepareAdjacentPages() {
527
592
  val first = max(0, selectedPage - 1)
528
593
  val last = min(adapter.itemCount - 1, selectedPage + 1)
529
594
  for (index in first..last) {
530
- recyclerViewForPage(index)?.let { recycler ->
531
- applyRecyclerInsets(recycler, index)
595
+ when (val scrollable = primaryScrollableForPage(index)) {
596
+ is RecyclerView -> applyRecyclerInsets(scrollable, index)
597
+ is ScrollView -> applyNativeScrollerInsets(scrollable, index)
532
598
  }
533
599
  }
534
600
  }
535
601
 
536
- private fun attachRecyclerObserver() {
602
+ private fun attachPrimaryScrollObserver() {
537
603
  if (selectedPage !in 0 until adapter.itemCount) return
538
- val previous = observedRecyclerView
604
+ val previous = observedRecyclerView ?: observedNativeScroller
539
605
  if (previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
540
- detachRecyclerObserver()
541
- // A replacement list starts at its first row, retaining only header collapse.
606
+ detachPrimaryScrollObserver()
607
+ // A replacement scroller starts at the beginning while retaining header collapse.
542
608
  pageOffsets[currentPageKey()] = 0
543
609
  }
544
- val recycler = recyclerViewForPage(selectedPage) ?: return
610
+ when (val scrollable = primaryScrollableForPage(selectedPage)) {
611
+ is RecyclerView -> attachRecyclerObserver(scrollable)
612
+ is ScrollView -> attachNativeScrollerObserver(scrollable)
613
+ }
614
+ }
615
+
616
+ private fun attachRecyclerObserver(recycler: RecyclerView) {
545
617
  if (observedRecyclerView === recycler) {
546
618
  applyRecyclerInsets(recycler, selectedPage)
547
619
  return
548
620
  }
549
621
 
550
- detachRecyclerObserver()
622
+ detachPrimaryScrollObserver()
551
623
  observedRecyclerView = recycler
552
624
  applyRecyclerInsets(recycler, selectedPage)
553
625
  val listener = object : RecyclerView.OnScrollListener() {
@@ -570,9 +642,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
570
642
  }
571
643
 
572
644
  private fun detachRecyclerObserver() {
573
- storeObservedPageOffset()
574
645
  val recycler = observedRecyclerView
575
646
  val listener = observedScrollListener
647
+ if (recycler != null) {
648
+ pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
649
+ }
576
650
  if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
577
651
  if (recycler != null) {
578
652
  log(
@@ -584,10 +658,50 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
584
658
  observedScrollListener = null
585
659
  }
586
660
 
587
- private fun storeObservedPageOffset() {
588
- observedRecyclerView?.let { recycler ->
589
- pageOffsets[currentPageKey()] = recycler.computeVerticalScrollOffset()
661
+ private fun attachNativeScrollerObserver(scroller: ScrollView) {
662
+ if (observedNativeScroller === scroller) {
663
+ applyNativeScrollerInsets(scroller, selectedPage)
664
+ return
590
665
  }
666
+
667
+ detachPrimaryScrollObserver()
668
+ observedNativeScroller = scroller
669
+ applyNativeScrollerInsets(scroller, selectedPage)
670
+ val listener = ViewTreeObserver.OnScrollChangedListener {
671
+ val contentOffset = scroller.scrollY
672
+ pageOffsets[currentPageKey()] = contentOffset
673
+ if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
674
+ setHeaderOffset(headerHeightPx)
675
+ }
676
+ }
677
+ observedNativeScrollerListener = listener
678
+ scroller.viewTreeObserver.addOnScrollChangedListener(listener)
679
+ log(
680
+ "scroller-observer-attach inner=$selectedPage key=${currentPageKey()} " +
681
+ "scroller=${System.identityHashCode(scroller)}",
682
+ )
683
+ }
684
+
685
+ private fun detachNativeScrollerObserver() {
686
+ val scroller = observedNativeScroller
687
+ val listener = observedNativeScrollerListener
688
+ if (scroller != null) pageOffsets[currentPageKey()] = scroller.scrollY
689
+ if (scroller != null && listener != null && scroller.viewTreeObserver.isAlive) {
690
+ scroller.viewTreeObserver.removeOnScrollChangedListener(listener)
691
+ }
692
+ if (scroller != null) {
693
+ log(
694
+ "scroller-observer-detach inner=$selectedPage key=${currentPageKey()} " +
695
+ "scroller=${System.identityHashCode(scroller)}",
696
+ )
697
+ }
698
+ observedNativeScroller = null
699
+ observedNativeScrollerListener = null
700
+ }
701
+
702
+ private fun detachPrimaryScrollObserver() {
703
+ detachRecyclerObserver()
704
+ detachNativeScrollerObserver()
591
705
  }
592
706
 
593
707
  private fun applyRecyclerInsets(recycler: RecyclerView, pageIndex: Int) {
@@ -649,6 +763,57 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
649
763
  }
650
764
  }
651
765
 
766
+ private fun applyNativeScrollerInsets(scroller: ScrollView, pageIndex: Int) {
767
+ val original = originalScrollerPadding.getOrPut(scroller) {
768
+ ScrollerPadding(
769
+ scroller.paddingLeft,
770
+ scroller.paddingTop,
771
+ scroller.paddingRight,
772
+ scroller.paddingBottom,
773
+ scroller.clipToPadding,
774
+ )
775
+ }
776
+ updateScrollerPaddingOwnership(scroller, original)
777
+ val topInset = headerHeightPx + stickyHeaderHeightPx
778
+ val top = original.top + topInset
779
+ scroller.clipToPadding = false
780
+ if (scroller.paddingLeft != original.left || scroller.paddingTop != top ||
781
+ scroller.paddingRight != original.right || scroller.paddingBottom != original.bottom) {
782
+ scroller.setPadding(original.left, top, original.right, original.bottom)
783
+ }
784
+ original.appliedTopInset = topInset
785
+
786
+ val key = pageKey(pageIndex)
787
+ if (restoredScrollerKeys[scroller] != key) {
788
+ restoredScrollerKeys[scroller] = key
789
+ val generation = attachmentGeneration
790
+ scroller.post {
791
+ if (!isAttachedToWindow || generation != attachmentGeneration ||
792
+ restoredScrollerKeys[scroller] != key ||
793
+ originalScrollerPadding[scroller] !== original) {
794
+ if (restoredScrollerKeys[scroller] == key) restoredScrollerKeys.remove(scroller)
795
+ return@post
796
+ }
797
+ val saved = pageOffsets[key] ?: 0
798
+ if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
799
+ }
800
+ }
801
+ }
802
+
803
+ private fun updateScrollerPaddingOwnership(
804
+ scroller: ScrollView,
805
+ original: ScrollerPadding,
806
+ ) {
807
+ val expectedTop = original.top + original.appliedTopInset
808
+ if (scroller.paddingLeft != original.left) original.left = scroller.paddingLeft
809
+ if (scroller.paddingTop != expectedTop) {
810
+ original.top = (scroller.paddingTop - original.appliedTopInset).coerceAtLeast(0)
811
+ }
812
+ if (scroller.paddingRight != original.right) original.right = scroller.paddingRight
813
+ if (scroller.paddingBottom != original.bottom) original.bottom = scroller.paddingBottom
814
+ if (scroller.clipToPadding) original.clipToPadding = true
815
+ }
816
+
652
817
  private fun updateRecyclerPaddingOwnership(
653
818
  recycler: RecyclerView,
654
819
  original: RecyclerPadding,
@@ -665,7 +830,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
665
830
  if (recycler.clipToPadding) original.clipToPadding = true
666
831
  }
667
832
 
668
- private fun reapplyRecyclerInsets() {
833
+ private fun reapplyScrollableInsets() {
669
834
  prepareAdjacentPages()
670
835
  }
671
836
 
@@ -677,6 +842,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
677
842
  recycler.setPadding(original.left, original.top, original.right, original.bottom)
678
843
  }
679
844
 
845
+ private fun restoreNativeScrollerInsets(scroller: ScrollView) {
846
+ val original = originalScrollerPadding.remove(scroller) ?: return
847
+ updateScrollerPaddingOwnership(scroller, original)
848
+ restoredScrollerKeys.remove(scroller)
849
+ scroller.clipToPadding = original.clipToPadding
850
+ scroller.setPadding(original.left, original.top, original.right, original.bottom)
851
+ }
852
+
680
853
  private fun headerRegionAt(y: Float): String? {
681
854
  val headerBottom = headerHeightPx - headerOffsetPx
682
855
  val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
@@ -736,26 +909,26 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
736
909
  )
737
910
  }
738
911
 
739
- private fun dispatchToRecycler(recycler: RecyclerView, event: MotionEvent): Boolean {
912
+ private fun dispatchToScrollable(scrollable: View, event: MotionEvent): Boolean {
740
913
  val hostLocation = IntArray(2)
741
- val recyclerLocation = IntArray(2)
914
+ val scrollableLocation = IntArray(2)
742
915
  getLocationOnScreen(hostLocation)
743
- recycler.getLocationOnScreen(recyclerLocation)
916
+ scrollable.getLocationOnScreen(scrollableLocation)
744
917
  val copy = MotionEvent.obtain(event)
745
918
  copy.offsetLocation(
746
- (hostLocation[0] - recyclerLocation[0]).toFloat(),
747
- (hostLocation[1] - recyclerLocation[1]).toFloat(),
919
+ (hostLocation[0] - scrollableLocation[0]).toFloat(),
920
+ (hostLocation[1] - scrollableLocation[1]).toFloat(),
748
921
  )
749
- val handled = recycler.dispatchTouchEvent(copy)
922
+ val handled = scrollable.dispatchTouchEvent(copy)
750
923
  copy.recycle()
751
924
  return handled
752
925
  }
753
926
 
754
- private fun beginForwardingToRecycler(event: MotionEvent): Boolean {
755
- val recycler = recyclerViewForPage(selectedPage) ?: return false
756
- forwardedRecycler = recycler
757
- headerDownEvent?.let { down -> dispatchToRecycler(recycler, down) }
758
- return dispatchToRecycler(recycler, event)
927
+ private fun beginForwardingToPrimaryScrollable(event: MotionEvent): Boolean {
928
+ val scrollable = primaryScrollableForPage(selectedPage) ?: return false
929
+ forwardedScrollable = scrollable
930
+ headerDownEvent?.let { down -> dispatchToScrollable(scrollable, down) }
931
+ return dispatchToScrollable(scrollable, event)
759
932
  }
760
933
 
761
934
  private fun finishHeaderGesture(event: MotionEvent) {
@@ -776,7 +949,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
776
949
  headerTouchRegion = "none"
777
950
  headerGestureOwner = HeaderGestureOwner.NONE
778
951
  headerHasHorizontalChild = false
779
- forwardedRecycler = null
952
+ forwardedScrollable = null
780
953
  pressCancelled = false
781
954
  }
782
955
 
@@ -789,7 +962,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
789
962
  headerDownEvent?.recycle()
790
963
  headerDownEvent = null
791
964
  headerGestureOwner = HeaderGestureOwner.NONE
792
- forwardedRecycler = null
965
+ forwardedScrollable = null
793
966
  pressCancelled = false
794
967
  nativeGestureStarted = false
795
968
  nestedNativeGestureStarted = false
@@ -832,10 +1005,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
832
1005
  return when (headerGestureOwner) {
833
1006
  HeaderGestureOwner.LIST -> {
834
1007
  cancelHeaderPress(event, "list")
835
- val recycler = forwardedRecycler
836
- if (recycler != null) {
837
- dispatchToRecycler(recycler, event)
838
- } else if (beginForwardingToRecycler(event)) {
1008
+ val scrollable = forwardedScrollable
1009
+ if (scrollable != null) {
1010
+ dispatchToScrollable(scrollable, event)
1011
+ } else if (beginForwardingToPrimaryScrollable(event)) {
839
1012
  true
840
1013
  } else {
841
1014
  headerGestureOwner = HeaderGestureOwner.HEADER_GUARD
@@ -855,8 +1028,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
855
1028
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
856
1029
  if (!headerTouchActive) return super.dispatchTouchEvent(event)
857
1030
  val handled = when (headerGestureOwner) {
858
- HeaderGestureOwner.LIST -> forwardedRecycler?.let { recycler ->
859
- dispatchToRecycler(recycler, event)
1031
+ HeaderGestureOwner.LIST -> forwardedScrollable?.let { scrollable ->
1032
+ dispatchToScrollable(scrollable, event)
860
1033
  } ?: true
861
1034
  HeaderGestureOwner.HEADER_GUARD -> true
862
1035
  else -> super.dispatchTouchEvent(event)
@@ -966,6 +1139,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
966
1139
  super.onAttachedToWindow()
967
1140
  attachmentGeneration += 1
968
1141
  restoredRecyclerKeys.clear()
1142
+ restoredScrollerKeys.clear()
969
1143
  viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
970
1144
  log(
971
1145
  "host-attach generation=$attachmentGeneration inner=$selectedPage " +
@@ -978,15 +1152,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
978
1152
  headerDownEvent?.recycle()
979
1153
  headerDownEvent = null
980
1154
  headerTouchActive = false
981
- forwardedRecycler = null
1155
+ forwardedScrollable = null
982
1156
  pressCancelled = false
983
1157
  nativeGestureStarted = false
984
1158
  nestedNativeGestureStarted = false
985
1159
  viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
986
- detachRecyclerObserver()
1160
+ detachPrimaryScrollObserver()
987
1161
  for (recycler in originalRecyclerPadding.keys.toList()) {
988
1162
  restoreRecyclerInsets(recycler)
989
1163
  }
1164
+ for (scroller in originalScrollerPadding.keys.toList()) {
1165
+ restoreNativeScrollerInsets(scroller)
1166
+ }
990
1167
  log(
991
1168
  "host-detach generation=$attachmentGeneration inner=$selectedPage " +
992
1169
  "outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
@@ -995,6 +1172,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
995
1172
  }
996
1173
 
997
1174
  companion object {
1175
+ const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
998
1176
  const val PAGE_SLOT_OFFSET = 2
999
1177
  }
1000
1178
  }
@@ -181,6 +181,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
181
181
  // OneKey patch: CollapsiblePagerHost always coordinates nested pagers on Android.
182
182
  }
183
183
 
184
+ @ReactProp(name = "nativeSmoothHeaderScrollEnabled", defaultBoolean = false)
184
185
  override fun setNativeSmoothHeaderScrollEnabled(
185
186
  view: CollapsiblePagerHost?,
186
187
  value: Boolean,
@@ -210,16 +211,19 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
210
211
  view?.retainedPages = value ?: "[]"
211
212
  }
212
213
 
214
+ @ReactProp(name = "nativeTabBarItems")
213
215
  override fun setNativeTabBarItems(view: CollapsiblePagerHost?, value: String?) {
214
216
  view?.updateNativeTabBarItems(value)
215
217
  }
216
218
 
219
+ @ReactProp(name = "nativeTabBarHeight")
217
220
  override fun setNativeTabBarHeight(view: CollapsiblePagerHost?, value: Double) {
218
221
  view ?: return
219
222
  view.nativeTabBarHeight = value
220
223
  view.applyNativeHeaderStyle()
221
224
  }
222
225
 
226
+ @ReactProp(name = "nativeTabBarContentPaddingHorizontal")
223
227
  override fun setNativeTabBarContentPaddingHorizontal(
224
228
  view: CollapsiblePagerHost?,
225
229
  value: Double,
@@ -229,64 +233,75 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
229
233
  view.applyNativeHeaderStyle()
230
234
  }
231
235
 
236
+ @ReactProp(name = "nativeTabBarItemSpacing")
232
237
  override fun setNativeTabBarItemSpacing(view: CollapsiblePagerHost?, value: Double) {
233
238
  view ?: return
234
239
  view.nativeTabBarItemSpacing = value
235
240
  view.applyNativeHeaderStyle()
236
241
  }
237
242
 
243
+ @ReactProp(name = "nativeTabBarFontSize")
238
244
  override fun setNativeTabBarFontSize(view: CollapsiblePagerHost?, value: Double) {
239
245
  view ?: return
240
246
  view.nativeTabBarFontSize = value
241
247
  view.applyNativeHeaderStyle()
242
248
  }
243
249
 
250
+ @ReactProp(name = "nativeTabBarFontFamily")
244
251
  override fun setNativeTabBarFontFamily(view: CollapsiblePagerHost?, value: String?) {
245
252
  view ?: return
246
253
  view.nativeTabBarFontFamily = value
247
254
  view.applyNativeHeaderStyle()
248
255
  }
249
256
 
257
+ @ReactProp(name = "nativeTabBarBackgroundColor", customType = "Color")
250
258
  override fun setNativeTabBarBackgroundColor(view: CollapsiblePagerHost?, value: Int?) {
251
259
  view ?: return
252
260
  view.nativeTabBarBackgroundColor = value ?: Color.TRANSPARENT
253
261
  view.applyNativeHeaderStyle()
254
262
  }
255
263
 
264
+ @ReactProp(name = "nativeTabBarActiveTextColor", customType = "Color")
256
265
  override fun setNativeTabBarActiveTextColor(view: CollapsiblePagerHost?, value: Int?) {
257
266
  view ?: return
258
267
  view.nativeTabBarActiveTextColor = value ?: Color.BLACK
259
268
  view.applyNativeHeaderStyle()
260
269
  }
261
270
 
271
+ @ReactProp(name = "nativeTabBarInactiveTextColor", customType = "Color")
262
272
  override fun setNativeTabBarInactiveTextColor(view: CollapsiblePagerHost?, value: Int?) {
263
273
  view ?: return
264
274
  view.nativeTabBarInactiveTextColor = value ?: Color.GRAY
265
275
  view.applyNativeHeaderStyle()
266
276
  }
267
277
 
278
+ @ReactProp(name = "nativeTabBarIndicatorColor", customType = "Color")
268
279
  override fun setNativeTabBarIndicatorColor(view: CollapsiblePagerHost?, value: Int?) {
269
280
  view ?: return
270
281
  view.nativeTabBarIndicatorColor = value ?: view.nativeTabBarActiveTextColor
271
282
  view.applyNativeHeaderStyle()
272
283
  }
273
284
 
285
+ @ReactProp(name = "nativeTabBarIndicatorHeight")
274
286
  override fun setNativeTabBarIndicatorHeight(view: CollapsiblePagerHost?, value: Double) {
275
287
  view ?: return
276
288
  view.nativeTabBarIndicatorHeight = value
277
289
  view.applyNativeHeaderStyle()
278
290
  }
279
291
 
292
+ @ReactProp(name = "nativeTabBarIndicatorBottom")
280
293
  override fun setNativeTabBarIndicatorBottom(view: CollapsiblePagerHost?, value: Double) {
281
294
  view ?: return
282
295
  view.nativeTabBarIndicatorBottom = value
283
296
  view.applyNativeHeaderStyle()
284
297
  }
285
298
 
299
+ @ReactProp(name = "nativeSubHeaderConfig")
286
300
  override fun setNativeSubHeaderConfig(view: CollapsiblePagerHost?, value: String?) {
287
301
  view?.updateNativeSubHeader(value)
288
302
  }
289
303
 
304
+ @ReactProp(name = "nativeSubHeaderSelectedBackgroundColor", customType = "Color")
290
305
  override fun setNativeSubHeaderSelectedBackgroundColor(
291
306
  view: CollapsiblePagerHost?,
292
307
  value: Int?,